[
  {
    "path": ".gitattributes",
    "content": ""
  },
  {
    "path": ".gitignore",
    "content": "*.class\r\n*.out\r\n*.o\r\n*.pyc\r\n*~\r\n*.log\r\n*.la\r\n*.so\r\n*.iml\r\nMETA-INF/\r\n.DS_Store\r\n\r\n# Binary Files #\r\n*.jar\r\n!dbMaker-*.jar\r\n\r\n# ignore all xdb except the one in ./data/\r\n*.xdb\r\n!/data/*.xdb\r\n\r\n# Package Files #\r\n.settings/\r\n.classpath\r\n.project\r\n\r\n# vim swp file #\r\n*.swp\r\n.idea\r\n.vscode\r\n\r\n# binding\r\n/binding/java/classes/\r\n/binding/java/doc/\r\n/binding/java/target/\r\n/binding/java/*.jar\r\n\r\n# python\r\n/**/__pycache__\r\n\r\n# clang\r\n/binding/c/xdb_searcher\r\n/binding/c/test_util\r\n/binding/c/cmake-build-debug\r\n\r\n# lua/luc_c\r\n/binding/lua_c/cmake-build-debug\r\n\r\n# golang\r\n/binding/golang/searcher\r\n/binding/golang/xdb_searcher\r\n/binding/golang/golang\r\n\r\n# rust\r\nCargo.lock\r\ntarget\r\n\r\n\r\n# VS ignore cases\r\n/**/*.sln\r\n/binding/c#/**/.vs/\r\n/binding/c#/**/packages\r\n/binding/c#/**/bin\r\n/binding/c#/**/obj\r\n\r\n# Nodejs\r\n/binding/nodejs/tests/unitTests/__snapshots__\r\n/binding/nodejs/coverage\r\n/binding/nodejs/node_modules\r\n/binding/nodejs/.nyc_output\r\n/binging/nodejs/package-lock.json\r\n\r\n# Javascript\r\n/binding/javascript/tests/unitTests/__snapshots__\r\n/binding/javascript/coverage\r\n/binding/javascript/node_modules\r\n/binding/javascript/.nyc_output\r\n/binding/javascript/package-lock.json\r\n\r\n# maker\r\n## golang\r\n/maker/golang/dbmaker\r\n/maker/golang/xdb_maker\r\n/maker/golang/golang\r\n\r\n#erlang\r\n/binding/erlang/_build\r\n/binding/erlang/doc\r\n\r\n#vscode\r\n.vscode\r\nbuild\r\n"
  },
  {
    "path": "LICENSE.md",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n==========================================================================\nThe following license applies to the ip2region library\n--------------------------------------------------------------------------\nCopyright (c) 2015 Lionsoul<chenxin619315@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region\n\n[ip2region](https://ip2region.net) - is an offline IP address localization library and IP localization data management framework. It supports both `IPv4` and `IPv6` with query efficiency at the 10-microsecond level. It provides `xdb` data generation and query client implementations for many mainstream programming languages.\n\n\n# Features\n\n### 1. Offline Localization Library\n\nThe project itself provides raw data for both IPv4 (`data/ipv4_source.txt`) and IPv6 (`data/ipv6_source.txt`), along with corresponding xdb files (`data/ip2region_v4.xdb` and `data/ip2region_v6.xdb`) to achieve city-level query localization. The field format is: `Country|Province|City|ISP|iso-alpha2-code`. Localization information for China is entirely in Chinese, while regional information for non-China areas is entirely in English.\n\n### 2. Data Management Framework\n\n`xdb` supports hundreds of millions of IP data segment rows. Region information supports full customization. The region information of the built-in data is fixed in the format: `Country|Province|City|ISP|iso-alpha2-Code`. You can append data for specific business needs to the region, such as: GPS information/International Standard Regional Codes/Zip codes, etc. In other words, you can fully use ip2region to manage your own IP localization data.\n\n### 3. Data Deduplication and Compression\n\nThe `xdb` format generation program automatically processes the input raw data, checks and completes the merging of adjacent IP segments, and performs deduplication and compression of identical regional information.\n\n### 4. High-Speed Query Response\n\nEven for queries based entirely on the `xdb` file, the single query response time is at the 10-microsecond level. Memory-accelerated queries can be enabled through the following two methods:\n\n1. `vIndex` Index Caching: Uses a fixed `512KiB` of memory to cache vector index data, reducing one disk IO operation and maintaining average query efficiency within 100 microseconds.\n2. Entire `xdb` File Caching: Loads the entire `xdb` file into memory. Memory usage is equal to the `xdb` file size. There is no disk IO operation, maintaining 10-microsecond level query efficiency.\n\n### 5. Unified Query Interface\n\n`xdb` provides version-compatible query implementations. A unified API can simultaneously provide queries for both IPv4 and IPv6 data and return unified data.\n\n\n# `xdb` Query\n\nFor API introductions, usage documentation, and test programs, please refer to the README introduction under the corresponding `searcher` query client. All query binding implementations are as follows:\n\n| Language | Description | IPv4 Support | IPv6 Support |\n| --- | --- | --- | --- |\n| [Golang](binding/golang/README.md)         | golang query client          | :white_check_mark: | :white_check_mark: |\n| [PHP](binding/php/README.md)               | php query client             | :white_check_mark: | :white_check_mark: |\n| [Java](binding/java/README.md)             | java query client            | :white_check_mark: | :white_check_mark: |\n| [C](binding/c/README.md)                   | C[std=c99] query client      | :white_check_mark: | :white_check_mark: |\n| [Lua_c](binding/lua_c/README.md)           | lua c extension query client | :white_check_mark: | :white_check_mark: |\n| [Lua](binding/lua/README.md)               | lua query client             | :white_check_mark: | :white_check_mark: |\n| [Rust](binding/rust/README.md)             | rust query client            | :white_check_mark: | :white_check_mark: |\n| [Python](binding/python/README.md)         | python query client          | :white_check_mark: | :white_check_mark: |\n| [Javascript](binding/javascript/README.md) | javascript query client      | :white_check_mark: | :white_check_mark: |\n| [Csharp](binding/csharp)                   | csharp query client          | :white_check_mark: | :white_check_mark: |\n| [Erlang](binding/erlang/README.md)         | erlang query client          | :white_check_mark: | :x:                |\n| [Nginx](binding/nginx)                     | nginx extension query client | :white_check_mark: | :white_check_mark: |\n| [C++](binding/cpp/README.md)               | C++ query client             | :white_check_mark: | :white_check_mark: |\n\nThe following toolchain implementations are contributed by community developers via third-party repositories:\n\n| Language | Description |\n| --- | --- |\n| [ip2region-composer](https://github.com/zoujingli/ip2region)    | php composer management client       |\n| [ip2region-ts](https://github.com/Steven-Qiang/ts-ip2region2)   | node.js addon management client      |\n| [ruby-ip2region](https://github.com/jicheng1014/ruby-ip2region) | ruby xdb query client implementation |\n| [Ip2regionTool](https://github.com/orestonce/Ip2regionTool)     | ip2region data conversion tool       |\n\n\n# `xdb` Generation\n\nFor API introductions, usage documentation, and test programs, please refer to the README documents under the following `maker` generation programs:\n\n| Language | Description | IPv4 Support | IPv6 Support |\n| --- | --- | --- | --- |\n| [Golang](maker/golang/README.md) | golang xdb generation program | :white_check_mark: | :white_check_mark: |\n| [Java](maker/java/README.md)     | java xdb generation program   | :white_check_mark: | :white_check_mark: |\n| [Python](maker/python/README.md) | python xdb generation program | :white_check_mark: | :x:                |\n| [Csharp](maker/csharp/README.md) | csharp xdb generation program | :white_check_mark: | :x:                |\n| [Rust](maker/rust/README.md)     | rust xdb generation program   | :white_check_mark: | :white_check_mark: |\n| [C++](maker/cpp)                 | C++ xdb generation program    | :white_check_mark: | :white_check_mark: |\n\n\n# `xdb` Update\n\nThe core of the ip2region project lies in **researching the design and implementation of IP data storage and fast querying**. The raw data `./data/ipv4_source.txt` and `./data/ipv6_source.txt` included in the project are updated irregularly. For scenarios with high requirements for data accuracy and update frequency, it is recommended to purchase commercial offline data from the [Ip2Region Community](https://ip2region.net/products/offline) or third-party vendors. You can try to update the data yourself using the following methods:\n\n### Manual Editing and Updating\n\nYou can modify the data yourself based on the raw IP data provided by ip2region in `./data/ipv4_source.txt` and `./data/ipv6_source.txt` using the editing tools provided by ip2region. Currently, the data sources include:\n\n1. Data provided by the ip2region community (please refer to the official account at the bottom for community notifications)\n2. Project Issues tagged with `[Data_Updates]`\n3. Other custom data: e.g., data provided by customers, data obtained through GPS and WIFI positioning, or legal and compliant data from other platforms.\n\nFor instructions on using the raw IP data editing tools, please refer to the README documents under the following `maker` generation programs:\n\n| Language | Description | IPv4 Support | IPv6 Support |\n| --- | --- | --- | --- |\n| [Golang](maker/golang/README.md#xdb-data-editing) | golang IP raw data editor | :white_check_mark: | :white_check_mark: |\n| [C++](maker/cpp/README.md)                        | C++ IP raw data editor    | :white_check_mark: | :white_check_mark: |\n\n### Detection Automatic Update\n\nIf you want to update data via your own API or data source, you can refer to the update algorithm based on the \"Detection Algorithm\" shared in the following videos to write your own update program:\n\n1. [Data Update Implementation Video Sharing - part1](https://www.bilibili.com/video/BV1934y1E7Q5/)\n2. [Data Update Implementation Video Sharing - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)\n\n\n# Official Community\n\nThe Ip2Region official community was officially launched on `2025/06/12`. On one hand, it provides stable [commercial offline data](https://ip2region.net/products/offline) services. On the other hand, it facilitates the strengthening of the IP toolchain and data services outside the core code, such as [usage documentation](https://ip2region.net/doc/), [query testing](https://ip2region.net/search/demo), and data correction. For more information and services regarding the community, please visit the [Ip2Region Official Community](https://ip2region.net/).\n\n\n# Related Remarks\n\n### 1. xdb Technical Documents:\n\n1. xdb Data Structure Analysis: [\"ip2region xdb - Data Structure Description\"](https://ip2region.net/doc/xdb/structure)\n2. xdb Query Process Analysis: [\"ip2region xdb - Query Process Description\"](https://ip2region.net/doc/xdb/search)\n3. xdb Generation Process Analysis: [\"ip2region xdb - Generation Process Description\"](https://ip2region.net/doc/xdb/generate)\n4. xdb File Generation Tutorial: [\"ip2region xdb - File Generation Tutorial\"](https://ip2region.net/doc/data/xdb_make)\n5. xdb Concurrent Safety Query: [\"ip2region xdb - Concurrent Safety Query\"](https://ip2region.net/doc/xdb/concurrent)\n6. xdb Data Update Method: [\"ip2region Data Update and Use of xdb Data Editor\"](https://mp.weixin.qq.com/s/cZH5qIn4E5rQFy6N32RCzA)\n\n### 3. Technical Information Blogs\n\n1. WeChat Official Account - lionsoul-org, the author's active technical sharing channel\n2. [Ip2Region Official Community](https://ip2region.net)\n"
  },
  {
    "path": "README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\r\n\r\n# ip2region\r\n\r\n[ip2region](https://ip2region.net) - 是一个离线IP地址定位库和IP定位数据管理框架，同时支持 `IPv4` 和 `IPv6` ，10微秒级别的查询效率，提供了众多主流编程语言的 `xdb` 数据生成和查询客户端实现。\r\n\r\n\r\n# 项目特性\r\n\r\n### 1、离线定位库\r\n\r\n项目本身同时了提供了一份 IPv4 (`data/ipv4_source.txt`) 和 IPv6 (`data/ipv6_source.txt`) 的原始数据和对应的 xdb 文件(`data/ip2region_v4.xdb` 和 `data/ip2region_v6.xdb`) 用于实现精确到城市的查询定位功能，字段格式为：`国家|省份|城市|ISP|iso-alpha2-code(国家两字母简称)`，中国的定位信息全部为中文，非中国地区的地域信息全部为英文。\r\n\r\n### 2、数据管理框架\r\n\r\n`xdb` 支持亿级别的 IP 数据段行数，region 信息支持完全自定义，自带数据的 region 信息固定了格式为：`国家|省份|城市|ISP|iso-alpha2-Code`，你可以在 region 中追加特定业务需求的数据，例如：GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。\r\n\r\n### 3、数据去重和压缩\r\n\r\n`xdb` 格式生成程序会自动处理输入的原始数据，检查并且完成相连 IP 段的的合并以及相同地域信息的去重和压缩。\r\n\r\n### 4、极速查询响应\r\n\r\n即使是完全基于 `xdb` 文件的查询，单次查询响应时间在十微秒级别，可通过如下两种方式开启内存加速查询：\r\n\r\n1. `vIndex` 索引缓存 ：使用固定的 `512KiB` 的内存空间缓存 vector index 数据，减少一次 IO 磁盘操作，保持平均查询效率稳定在100微秒之内。\r\n2. `xdb` 整个文件缓存：将整个 `xdb` 文件全部加载到内存，内存占用等同于 `xdb` 文件大小，无磁盘 IO 操作，保持10微秒级别的查询效率。\r\n\r\n### 5、统一的查询接口\r\n\r\n`xdb` 提供了版本兼容的查询实现，一个统一的 API 可以同时提供对 IPv4 和 IPv6 数据的查询并且返回统一的数据。\r\n\r\n\r\n# `xdb` 查询\r\n\r\nAPI 介绍，使用文档和测试程序请参考对应 `searcher` 查询客户端下的 README 介绍，全部查询 binding 实现情况如下：\r\n| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |\r\n| --- | --- | --- | --- |\r\n| [Golang](binding/golang/README_zh.md)         | golang 查询客户端       | :white_check_mark: | :white_check_mark: |\r\n| [PHP](binding/php/README_zh.md)               | php 查询客户端          | :white_check_mark: | :white_check_mark: |\r\n| [Java](binding/java/README_zh.md)             | java 查询客户端         | :white_check_mark: | :white_check_mark: |\r\n| [C](binding/c/README_zh.md)                   | C[std=c99] 查询客户端   | :white_check_mark: | :white_check_mark: |\r\n| [Lua_c](binding/lua_c/README_zh.md)           | lua c 扩展查询客户端    | :white_check_mark: | :white_check_mark: |\r\n| [Lua](binding/lua/README_zh.md)               | lua 查询客户端          | :white_check_mark: | :white_check_mark: |\r\n| [Rust](binding/rust/README_zh.md)             | rust 查询客户端         | :white_check_mark: | :white_check_mark: |\r\n| [Python](binding/python/README_zh.md)         | python 查询客户端       | :white_check_mark: | :white_check_mark: |\r\n| [Javascript](binding/javascript/README_zh.md) | javascript 查询客户端   | :white_check_mark: | :white_check_mark: |\r\n| [Csharp](binding/csharp)                      | csharp 查询客户端       | :white_check_mark: | :white_check_mark: |\r\n| [Erlang](binding/erlang/README_zh.md)         | erlang 查询客户端       | :white_check_mark: | :x:                |\r\n| [Nginx](binding/nginx)                        | nginx 扩展查询客户端    | :white_check_mark: | :white_check_mark: |\r\n| [C++](binding/cpp/README_zh.md)               | C++ 查询客户端          | :white_check_mark: | :white_check_mark: |\r\n\r\n\r\n以下工具链实现由社区开发者通过第三方仓库贡献：\r\n| 编程语言 | 描述 |\r\n| --- | --- |\r\n| [ip2region-composer](https://github.com/zoujingli/ip2region)    | php composer 管理客户端 |\r\n| [ip2region-ts](https://github.com/Steven-Qiang/ts-ip2region2)   | node.js addon 管理客户端|\r\n| [ruby-ip2region](https://github.com/jicheng1014/ruby-ip2region) | ruby xdb 查询客户端实现 |\r\n| [Ip2regionTool](https://github.com/orestonce/Ip2regionTool)     | ip2region 数据转换工具  |\r\n\r\n\r\n# `xdb` 生成\r\n\r\nAPI 介绍，使用文档和测试程序请参考如下 `maker` 生成程序下的 README 文档：\r\n\r\n| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |\r\n| --- | --- | --- | --- |\r\n| [Golang](maker/golang/README_zh.md) | golang xdb 生成程序  | :white_check_mark: | :white_check_mark: |\r\n| [Java](maker/java/README_zh.md)     | java xdb 生成程序    | :white_check_mark: | :white_check_mark: |\r\n| [Python](maker/python/README_zh.md) | python xdb 生成程序  | :white_check_mark: | :x:                |\r\n| [Csharp](maker/csharp/README_zh.md) | csharp xdb 生成程序  | :white_check_mark: | :x:                |\r\n| [Rust](maker/rust/README_zh.md)     | rust xdb 生成程序    | :white_check_mark: | :white_check_mark: |\r\n| [C++](maker/cpp)                    | C++ xdb 生成程序     | :white_check_mark: | :white_check_mark: |\r\n\r\n\r\n# `xdb` 更新\r\n\r\nip2region 项目的核心在于 <b>研究 IP 数据的存储和快速查询的设计和实现</b>， 项目自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始数据不定期更新，对于数据精度和更新频率要求很高的使用场景建议到 [Ip2Region社区](https://ip2region.net/products/offline) 或者第三方购买商用离线数据，你可以使用如下几种方式来尝试自己更新数据：\r\n\r\n### 手动编辑更新\r\n你可以基于 ip2region 自带的 `./data/ipv4_source.txt` 和 `./data/ipv6_source.txt` 原始 IP 数据用 ip2region 提供的编辑工具来自己修改，目前数据源有如下几种方式：\r\n1. ip2region 社区提供的数据（请参考地底部的公众号关注社区通知）\r\n2. ip2region Github/Gitee 中带有 `[数据源补充]` 标签的 Issue\r\n3. 其他自定义数据：例如客户提供的数据，或者通过 GPS 和 WIFI 定位得到的数据，或者来自其他平台的合法合规的数据\r\n\r\n原始 IP 数据编辑工具使用方法请参考如下的 `maker` 生成程序下的 README 文档：\r\n| 编程语言 | 描述 | IPv4 支持 | IPv6 支持 |\r\n| --- | --- | --- | --- |\r\n| [Golang](maker/golang/README_zh.md#xdb-数据编辑) | golang IP 原始数据编辑器 | :white_check_mark: | :white_check_mark: |\r\n| [C++](maker/cpp)                                 | C++ IP 原始数据编辑器    | :white_check_mark: | :white_check_mark: |\r\n\r\n\r\n### 检测自动更新\r\n如果你想通过你自己的 API 或数据源来更新数据，你可以参考以下视频分享的 `基于检测算法` 的更新算法来自己编写一个更新程序：\r\n1. [数据更新实现视频分享 - part1](https://www.bilibili.com/video/BV1934y1E7Q5/)\r\n2. [数据更新实现视频分享 - part2](https://www.bilibili.com/video/BV1pF411j7Aw/)\r\n\r\n\r\n# 官方社区\r\nIp2Region 官方社区正式上线于 `2025/06/12` 日，一方面提供了稳定的 [商用离线数据](https://ip2region.net/products/offline) 服务，另一方面便于在核心代码外强化 IP 工具链和数据服务，例如 [使用文档](https://ip2region.net/doc/)，[查询测试](https://ip2region.net/search/demo)，数据纠错等，更多关于社区的信息和服务请访问 [Ip2Region 官方社区](https://ip2region.net/)。\r\n\r\n\r\n# 相关备注\r\n\r\n### 1、xdb 技术文档：\r\n1. xdb 数据结构分析：[“ip2region xdb-数据结构描述“](https://ip2region.net/doc/xdb/structure)\r\n2. xdb 查询过程分析：[“ip2region xdb-查询过程描述”](https://ip2region.net/doc/xdb/search)\r\n3. xdb 生成过程分析：[“ip2region xdb-生成过程描述”](https://ip2region.net/doc/xdb/generate)\r\n4. xdb 文件生成教程：[“ip2region xdb-文件生成教程”](https://ip2region.net/doc/data/xdb_make)\r\n5. xdb 并发安全查询：[“ip2region xdb-并发安全查询”](https://ip2region.net/doc/xdb/concurrent)\r\n6. xdb 数据更新方法：[“ip2region 数据更新和 xdb 数据编辑器的使用”](https://mp.weixin.qq.com/s/cZH5qIn4E5rQFy6N32RCzA)\r\n\r\n### 3、技术信息博客\r\n1. 微信公众号 - lionsoul-org，作者活跃的技术分享渠道\r\n2. [Ip2Region 官方社区](https://ip2region.net)\r\n"
  },
  {
    "path": "binding/c/Makefile",
    "content": "all: xdb_searcher test_util\n\nxdb_searcher: xdb_api.h xdb_util.c xdb_searcher.c main.c\n\tgcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher\n\ntest_util: xdb_api.h xdb_util.c test_util.c\n\tgcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util\n\nxdb_searcher.o: xdb_searcher.c\n\tgcc -std=c99 -Wall -c xdb_searcher.c\n\nxdb_util.o: xdb_util.c\n\tgcc -std=c99 -Wall -c xdb_util.c\n\nxdb_searcher_lib: xdb_util.o xdb_searcher.o\n\tmkdir -p build/lib\n\tmkdir -p build/include\n\tar -rc build/lib/libxdb.a `find . -name \"*.o\"`\n\tcp xdb_api.h build/include\n\nclean:\n\tfind ./ -name \\*.o  | xargs rm -f\n\tfind ./ -name test_util | xargs rm -f\n\tfind ./ -name xdb_searcher | xargs rm -f\n\trm -rf build\n\n.PHONY: all clean xdb_searcher test_util\n"
  },
  {
    "path": "binding/c/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region c Query Client\n\n# Usage\n\n### About Query API\n\nThe prototype of the Query API is as follows:\n\n```c\n// Query via string IP\nint xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);\n// Query via binary IP returned by xdb_parse_ip\nint xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);\n```\n\nIf the query fails, a non-`0` error code will be returned. If the query is successful, the `region` information string can be obtained from `xdb_region_buffer_t`. If the input IP cannot be found, `xdb_region_buffer_t` will receive an empty string `\"\"`.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. The usage is as follows:\n\n```c\n#include \"xdb_api.h\";\n\n// For IPv4: Set xdb path to the v4 xdb file, specify IP version as IPv4\nconst char *db_path  = \"../../data/ip2region_v4.xdb\";  // or your ipv4 xdb path\nxdb_version_t *version = XDB_IPv4;\n\n// For IPv6: Set xdb path to the v6 xdb file, specify IP version as IPv6\nconst char *db_path  = \"../../data/ip2region_v6.xdb\";  // or your ipv6 xdb path\nxdb_version_t *version = XDB_IPv6;\n\n// The IP version of the xdb specified by db_path must be consistent with the version, otherwise an error will occur during query execution\n// Note: The following demonstration directly uses db_path and version variables\n```\n\n### XDB File Verification\n\nIt is recommended that you proactively verify the applicability of the xdb file, as some future new features may cause the current Searcher version to be incompatible with the xdb file you are using. Verification can avoid unpredictable errors during runtime. You do not need to verify every time; for example, verify when the service starts or manually call a command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```c\n#include \"xdb_api.h\";\n\nint errcode = xdb_verify(db_path);\nif ($err != 0) {\n    // Applicability verification failed!!!\n    // The current query client implementation is not suitable for querying the xdb file specified by db_path.\n    // You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with db_path.\n    printf(\"failed to verify xdb file `%s`, errcode: %d\\n\", db_path, errcode);\n    return;\n}\n\n// Verification passed, the current Searcher can be safely used for query operations on the xdb pointed to by dbPath\n```\n\n### File-Based Query\n\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_searcher_t searcher;\n    char region_buffer[512] = {'\\0'};\n    xdb_region_buffer_t region;\n\n    // Initialize region_buffer_t using region_buffer from stack space\n    int err = xdb_region_buffer_init(&region, region_buffer, sizeof(region_buffer));\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 1;\n    }\n\n    // Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1. Initialize xdb query object from db_path.\n    // @Note: Use the db_path and version described above to create the searcher\n    err = xdb_new_with_file_only(version, &searcher, db_path);\n    if (err != 0) {\n        printf(\"failed to create xdb searcher from `%s` with errno=%d\\n\", db_path, err);\n        return 1;\n    }\n\n    // 2. Call search API to query, both IPv4 and IPv6 are supported.\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // Clean up memory resources for region info; must be called after every search\n    xdb_region_buffer_free(&region);\n\n    // Note: For concurrent use, each thread needs to define and initialize its own searcher query object independently.\n\n    // 3. Close xdb searcher\n    xdb_close(&searcher);\n    xdb_clean_winsock();    // Call on Windows\n    return 0;\n}\n```\n\n### Caching `VectorIndex`\n\nWe can pre-load VectorIndex data from the xdb file and cache it globally. Using the global VectorIndex cache every time a Searcher object is created can reduce a fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_vector_index_t *v_index;\n    xdb_searcher_t searcher;\n    xdb_region_buffer_t region;\n\n    // Initialize region_buffer with NULL to let it manage memory allocation automatically\n    int err = xdb_region_buffer_init(&region, NULL, 0);\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 0;\n    }\n\n    // Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1. Load VectorIndex from the db_path described above.\n    // Obtain v_index to create a global cache for subsequent repeated use.\n    // Note: v_index does not need to be loaded every time; it is recommended to load it once at service startup as a global resource.\n    v_index = xdb_load_vector_index_from_file(db_path);\n    if (v_index == NULL) {\n        printf(\"failed to load vector index from `%s`\\n\", db_path);\n        return 1;\n    }\n\n    // 2. Use the global VectorIndex variable to create an xdb searcher with VectorIndex cache.\n    // @Note: Use the db_path and version described above to create the searcher\n    err = xdb_new_with_vector_index(version, &searcher, db_path, v_index);\n    if (err != 0) {\n        printf(\"failed to create vector index cached searcher with errcode=%d\\n\", err);\n        return 2;\n    }\n\n\n    // 3. Call search API to query, both IPv4 and IPv6 are supported\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // Clean up memory resources for region info; must be called after every search\n    xdb_region_buffer_free(&region);\n\n\n    // Note: For concurrent use, each thread needs to define and initialize its own searcher query object independently.\n\n    // 4. Close xdb searcher; if the service is being shut down, the memory for v_index also needs to be freed.\n    xdb_close(&searcher);\n    xdb_close_vector_index(v_index);\n    xdb_clean_winsock();\n    return 0;\n}\n```\n\n### Caching the Entire `xdb` File\n\nWe can also pre-load the entire xdb file into memory and then create a query object based on this data to achieve fully memory-based queries, similar to the previous memory search.\n\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_content_t *c_buffer;\n    xdb_searcher_t searcher;\n    xdb_region_buffer_t region;\n\n    // Initialize region_buffer with NULL to let it manage memory allocation automatically\n    int err = xdb_region_buffer_init(&region, NULL, 0);\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 0;\n    }\n\n\n    // Initialize winsock when the service starts; no need to call repeatedly, only needed on Windows systems\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1. Load the entire xdb data from the db_path described above.\n    c_buffer = xdb_load_content_from_file(db_path);\n    if (v_index == NULL) {\n        printf(\"failed to load xdb content from `%s`\\n\", db_path);\n        return 1;\n    }\n\n    // 2. Use the global c_buffer variable to create a fully memory-based xdb query object.\n    // @Note: Use the version described above to create the searcher.\n    err = xdb_new_with_buffer(version, &searcher, c_buffer);\n    if (err != 0) {\n        printf(\"failed to create content cached searcher with errcode=%d\\n\", err);\n        return 2;\n    }\n\n    // 3. Call search API to query, both IPv4 and IPv6 are supported\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // Clean up memory resources for region info; must be called after every search\n    xdb_region_buffer_free(&region);\n\n\n    // Note: For concurrent use, xdb query objects created this way can be safely used for concurrency.\n    // It is recommended to create them when the service starts and then use them safely in parallel until the service shuts down.\n\n    // 4. Close xdb searcher; memory for c_buffer needs to be freed when shutting down the service.\n    xdb_close(&searcher);\n    xdb_close_content(c_buffer);\n    xdb_clean_winsock();\n    return 0;\n}\n```\n\n### About Storage of Location Information\n\nIn older implementations, search-related functions relied on a specified `region_buffer` memory to store location information, which had significant limitations.\nThe new implementation provides an `xdb_region_buffer_t` object to manage these memory allocations. You can still specify a fixed `region_buffer` to create memory management for the region; this is suitable when the maximum length of your location information is known, which helps reduce memory fragmentation during runtime. If the length of the location information is uncertain or if your program is not suited for pre-allocating a block of memory, you can initialize `xdb_region_buffer_t` by specifying `NULL`. In this case, the object will automatically manage memory allocation, making it suitable for storing location information of any length, though this approach will certainly increase memory fragmentation over long-term operation.\n\n```c\n// 1. Create region_buffer by specifying a memory block\nchar buffer[512];\nxdb_region_buffer_t region;\nint err = xdb_region_buffer_init(&region, buffer, sizeof(buffer));\nif (err != 0) {\n    // Initialization failed\n    printf(\"failed to init region buffer width errcode=%d\", err);\n    return;\n}\n\n// 2. Create region_buffer by specifying NULL to let it allocate memory as needed automatically\nxdb_region_buffer_t region;\nint err = xdb_region_buffer_init(&region, NULL, 0);\nif (err != 0) {\n    // Initialization failed\n    printf(\"failed to init region buffer width errcode=%d\", err);\n    return;\n}\n\n\n// Note: After each query call, you must manually call the function to free memory.\n// The search function will report an error if used with uncleaned region info.\nxdb_region_buffer_free(&region);\n```\n\n# Compiling the Test Program\n\nCompile and obtain the `xdb_searcher` executable as follows:\n\n```bash\n# cd to the c binding root directory\n➜  c git:(master) ✗ make\ngcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher\ngcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util\n```\n\n# Query Testing\n\nTest queries against xdb via the `xdb_searcher search` command:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search\n./xdb_searcher search [command options]\noptions:\n --db string              ip2region binary xdb file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\nExample: performing IPv4 query testing using the default data/ip2region_v4.xdb:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 39 μs}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 13 μs}\n```\n\nExample: performing IPv6 query testing using the default data/ip2region_v6.xdb:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , io_count: 1, took: 38 μs}\nip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff\n{region: United States|Florida|Miami|velia.net Internetdienste GmbH|US, io_count: 14, took: 76 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 42 μs}\n```\n\nEnter an IP to perform a query; enter `quit` to exit the test program. You can also set `cache-policy` to file/vectorIndex/content respectively to test the efficiency of the three different cache implementations.\n\n# bench Testing\n\nPerform bench testing via the `xdb_searcher bench` command. This ensures there are no errors in the query program and the `xdb` file, while also providing average query performance through a large number of queries:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench                                  \n./xdb_searcher bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\nExample: performing IPv4 bench testing via the default data/ip2region_v4.xdb and data/ipv4_source.txt:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\nBench finished, {cache_policy: vectorIndex, total: 1367686, took: 7.640s, cost: 5 μs/op}\n```\n\nExample: performing IPv6 bench testing via the default data/ip2region_v6.xdb and data/ipv6_source.txt:\n\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\nBench finished, {cache_policy: vectorIndex, total: 34159862, took: 857.750s, cost: 24 μs/op}\n```\n\nYou can set the `cache-policy` parameter to test the efficiency of different cache mechanisms (file/vectorIndex/content). @Note: Please ensure that the `src` file used for benching is the same source file used to generate the corresponding `xdb` file.\n"
  },
  {
    "path": "binding/c/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region c 查询客户端\n\n\n# 使用方式\n\n### 关于查询 API\n查询 API 的原型如下：\n```c\n// 通过字符串 IP 进行查询\nint xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);\n// 通过 xdb_parse_ip 返回的二进制 IP 进行查询\nint xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);\n```\n如果查询失败将会返回非 `0` 的错误代码，如果查询成功 xdb_region_buffer_t 可以获取到字符串的 `region` 信息，如果输入的 IP 找不到相关的信息，xdb_region_buffer_t 将会得到一个空的字符串 `\"\"`。\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```c\n#include \"xdb_api.h\";\n\n// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 IPv4\nconst char *db_path  = \"../../data/ip2region_v4.xdb\";  // 或者你的 ipv4 xdb 的路径\nxdb_version_t *version = XDB_IPv4;\n\n// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 IPv6\nconst char *db_path  = \"../../data/ip2region_v6.xdb\";  // 或者你的 ipv6 xdb 路径\nxdb_version_t *version = XDB_IPv6;\n\n// db_path 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n// 备注：以下演示直接使用 db_path 和 version 变量\n```\n\n### XDB 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```c\n#include \"xdb_api.h\";\n\nint errcode = xdb_verify(db_path);\nif ($err != 0) {\n    // 适用性验证失败！！！\n    // 当前查询客户端实现不适用于 db_path 指定的 xdb 文件的查询.\n    // 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 db_path 的 Searcher 实现。\n    printf(\"failed to verify xdb file `%s`, errcode: %d\\n\", db_path, errcode);\n    return;\n}\n\n// 验证通过，当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_searcher_t searcher;\n    char region_buffer[512] = {'\\0'};\n    xdb_region_buffer_t region;\n\n    // 使用栈空间的 region_buffer 初始化 region_buffer_t\n    int err = xdb_region_buffer_init(&region, region_buffer, sizeof(region_buffer));\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 1;\n    }\n\n    // 在服务启动的时候初始化 winsock，不需要重复调用，只需要在 windows 系统下调用\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1、从 db_path 初始化 xdb 查询对象.\n    // @Note: 使用顶部描述的 db_path 和 version 来创建 searcher\n    err = xdb_new_with_file_only(version, &searcher, db_path);\n    if (err != 0) {\n        printf(\"failed to create xdb searcher from `%s` with errno=%d\\n\", db_path, err);\n        return 1;\n    }\n\n    // 2、调用 search API 查询，IPv4 和 IPv6 都支持.\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // 清理 region 信息的内存资源，每次 search 之后都得调用\n    xdb_region_buffer_free(&region);\n\n    // 备注：并发使用，每一个线程需要单独定义并且初始化一个 searcher 查询对象。\n\n    // 3、关闭 xdb 查询器\n    xdb_close(&searcher);\n    xdb_clean_winsock();    // windows 下调用\n    return 0;\n}\n```\n\n### 缓存 `VectorIndex` 索引\n\n我们可以提前从 xdb 文件中加载出来 VectorIndex 数据，然后全局缓存，每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作，从而加速查询，减少 IO 压力。\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_vector_index_t *v_index;\n    xdb_searcher_t searcher;\n    xdb_region_buffer_t region;\n\n    // 使用 NULL 初始化 region_buffer，让其自动管理内存的分配\n    int err = xdb_region_buffer_init(&region, NULL, 0);\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 0;\n    }\n\n    // 在服务启动的时候初始化 winsock，不需要重复调用，只需要在 windows 系统下调用\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1、从顶部描述的 db_path 加载 VectorIndex 索引。\n    // 得到 v_index 做成全局缓存，便于后续反复使用。\n    // 注意：v_index 不需要每次都加载，建议在服务启动的时候加载一次，然后做成全局资源。\n    v_index = xdb_load_vector_index_from_file(db_path);\n    if (v_index == NULL) {\n        printf(\"failed to load vector index from `%s`\\n\", db_path);\n        return 1;\n    }\n\n    // 2、使用全局的 VectorIndex 变量创建带 VectorIndex 缓存的 xdb 查询对象.\n    // @Note: 使用顶部描述的 db_path 和 version 来创建 searcher\n    err = xdb_new_with_vector_index(version, &searcher, db_path, v_index);\n    if (err != 0) {\n        printf(\"failed to create vector index cached searcher with errcode=%d\\n\", err);\n        return 2;\n    }\n\n\n    // 3、调用 search API 查询，IPv4 和 IPv6 都支持\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // 清理 region 信息的内存资源，每次 search 之后都得调用\n    xdb_region_buffer_free(&region);\n\n\n    // 备注：并发使用，每一个线程需要单独定义并且初始化一个 searcher 查询对象。\n\n    // 4、关闭 xdb 查询器，如果是要关闭服务，也需要释放 v_index 的内存。\n    xdb_close(&searcher);\n    xdb_close_vector_index(v_index);\n    xdb_clean_winsock();\n    return 0;\n}\n```\n\n### 缓存整个 `xdb` 文件\n\n我们也可以预先加载整个 xdb 文件到内存，然后基于这个数据创建查询对象来实现完全基于内存的查询，类似之前的 memory search。\n```c\n#include <stdio.h>\n#include \"xdb_api.h\"\n\nint main(int argc, char *argv[]) {\n    xdb_content_t *c_buffer;\n    xdb_searcher_t searcher;\n    xdb_region_buffer_t region;\n\n    // 使用 NULL 初始化 region_buffer，让其自动管理内存的分配\n    int err = xdb_region_buffer_init(&region, NULL, 0);\n    if (err != 0) {\n        printf(\"failed to init the region buffer with errcode=%d\\n\", err);\n        return 0;\n    }\n\n\n    // 在服务启动的时候初始化 winsock，不需要重复调用，只需要在 windows 系统下调用\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n\n    // 1、从 顶部描述的 db_path 加载整个 xdb 的数据。\n    c_buffer = xdb_load_content_from_file(db_path);\n    if (v_index == NULL) {\n        printf(\"failed to load xdb content from `%s`\\n\", db_path);\n        return 1;\n    }\n\n    // 2、使用全局的 c_buffer 变量创建一个完全基于内存的 xdb 查询对象.\n    // @Note: 使用顶部描述的 version 来创建 searcher.\n    err = xdb_new_with_buffer(version, &searcher, c_buffer);\n    if (err != 0) {\n        printf(\"failed to create content cached searcher with errcode=%d\\n\", err);\n        return 2;\n    }\n\n    // 3、调用 search API 查询，IPv4 和 IPv6 都支持\n    const char *ip_string = \"1.2.3.4\";\n    // ip_string = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n\n    long cost_time = 0, s_time = xdb_now();\n    err = xdb_search_by_string(&searcher, ip_string, &region);\n    cost_time = (int) (xdb_now() - s_time);\n    if (err != 0) {\n        printf(\"failed search(%s) with errno=%d\\n\", ip_string, err);\n    } else {\n        printf(\"{region: %s, took: %d μs}\", region.value, cost_time);\n    }\n\n    // 清理 region 信息的内存资源，每次 search 之后都得调用\n    xdb_region_buffer_free(&region);\n\n\n    // 备注：并发使用，使用这种方式创建的 xdb 查询对象可以安全用于并发。\n    // 建议在服务启动的时候创建好，然后一直安全并发使用，直到服务关闭。\n\n    // 4、关闭 xdb 查询器，关闭服务的时候需要释放 c_buffer 的内存。\n    xdb_close(&searcher);\n    xdb_close_content(c_buffer);\n    xdb_clean_winsock();\n    return 0;\n}\n```\n\n### 关于定位信息的存储\n在旧版本的实现中，search相关的函数都是依靠指定一个 `region_buffer` 内存来用于存储地域信息，这种方式还是有很大的局限性。\n新的实现提供了一个 `xdb_region_buffer_t` 对象来管理这些内存的分配，你依然可以指定一个固定的 `region_buffer` 来创建 region 的内存管理，这个情况适合当你的地域信息的最大长度是可知的，这种方式可以减少运行过程中内存的碎片堆积。如果地域信息的长度不确定或者你的程序不适合提前分配一块内存来管理，你可以通过指定 NULL 的方式来初始化 `xdb_region_buffer_t`，这样对象会自动管理内存的分配，也适合任意长度的地域信息的存储，不过这种方式在长期的运行过程中肯定会增加内存碎片的堆积。\n```c\n// 1, 通过指定一块内存来创建 region_buffer\nchar buffer[512];\nxdb_region_buffer_t region;\nint err = xdb_region_buffer_init(&region, buffer, sizeof(buffer));\nif (err != 0) {\n    // 初始化失败\n    printf(\"failed to init region buffer width errcode=%d\", err);\n    return;\n}\n\n// 2，通过指定 NULL 来创建 region_buffer，让其自动按需分配内存\nxdb_region_buffer_t region;\nint err = xdb_region_buffer_init(&region, NULL, 0);\nif (err != 0) {\n    // 初始化失败\n    printf(\"failed to init region buffer width errcode=%d\", err);\n    return;\n}\n\n\n// 备注：在每次调用 search 完成 IP 定位信息的查询后，你需要手动调用函数来释放内存 .\n// search 函数使用未经清理的 region 信息会报错。\nxdb_region_buffer_free(&region);\n```\n\n\n# 测试程序编译\n\n通过如下方式编译得到 xdb_searcher 可执行程序：\n```bash\n# cd 到 c binding 根目录\n➜  c git:(master) ✗ make\ngcc -std=c99 -Wall -O2 -I./ xdb_util.c xdb_searcher.c main.c -o xdb_searcher\ngcc -std=c99 -Wall -O2 -I./ xdb_util.c test_util.c -o test_util\n```\n\n\n# 查询测试\n\n通过 `xdb_searcher search` 命令来测试对 xdb 的查询：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search\n./xdb_searcher search [command options]\noptions:\n --db string              ip2region binary xdb file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 进行 IPv4 查询测试：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 39 μs}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 13 μs}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 进行 IPv6 查询测试：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher search --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , io_count: 1, took: 38 μs}\nip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff\n{region: United States|Florida|Miami|velia.net Internetdienste GmbH|US, io_count: 14, took: 76 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 42 μs}\n```\n\n输入 ip 即可进行查询，输入 quit 即可退出测试程序。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同的缓存实现的效率。\n\n\n\n# bench 测试\n\n通过 `xdb_searcher bench` 命令来进行 bench 测试，一方面确保查询程序和 `xdb` 文件没有错误，另一方面可以通过大量的查询得到平均的查询性能：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench                                  \n./xdb_searcher bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 来进行 IPv4 的 bench 测试：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\nBench finished, {cache_policy: vectorIndex, total: 1367686, took: 7.640s, cost: 5 μs/op}\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 来进行 IPv6 的 bench 测试：\n```bash\n➜  c git:(fr_c_ipv6) ✗ ./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\nBench finished, {cache_policy: vectorIndex, total: 34159862, took: 857.750s, cost: 24 μs/op}\n```\n\n可以设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 不同缓存实现机制的效率。 @Note：请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件相同的源文件。\n"
  },
  {
    "path": "binding/c/main.c",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/28\n\n#include \"stdio.h\"\n#include \"xdb_api.h\"\n\nstruct searcher_test_entry {\n    xdb_searcher_t searcher;\n    xdb_vector_index_t *v_index;\n    xdb_content_t *c_buffer;\n\n    // xdb region buffer\n    // char region_buffer[256];\n    xdb_region_buffer_t region;\n};\ntypedef struct searcher_test_entry searcher_test_t;\n\nint init_searcher_test(searcher_test_t *test, char *db_path, char *cache_policy) {\n    int err, errcode = 0;\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return -1;\n    }\n\n    // auto detect the version from the xdb header\n    xdb_header_t *header = xdb_load_header(handle);\n    if (header == NULL) {\n        printf(\"failed to load header from `%s`\\n\", db_path);\n        errcode = 1;\n        goto defer;\n    }\n\n    // verify the current xdb\n    err = xdb_verify_from_header(handle, header);\n    if (err != 0) {\n        printf(\"failed to verify xdb file `%s` with errno=%d\\n\", db_path, err);\n        errcode = 2;\n        goto defer;\n    }\n\n    xdb_version_t *version = xdb_version_from_header(header);\n    if (version == NULL) {\n        printf(\"failed to load version from header\\n\");\n        errcode = 3;\n        goto defer;\n    }\n\n    test->v_index = NULL;\n    test->c_buffer = NULL;\n\n    if (strcmp(cache_policy, \"file\") == 0) {\n        err = xdb_new_with_file_only(version, &test->searcher, db_path);\n        if (err != 0) {\n            printf(\"failed to create searcher with errcode=%d\\n\", err);\n            errcode = 4;\n            goto defer;\n        }\n    } else if (strcmp(cache_policy, \"vectorIndex\") == 0) {\n        test->v_index = xdb_load_vector_index_from_file(db_path);\n        if (test->v_index == NULL) {\n            printf(\"failed to load vector index from `%s`\\n\", db_path);\n            errcode = 4;\n            goto defer;\n        }\n\n        err = xdb_new_with_vector_index(version, &test->searcher, db_path, test->v_index);\n        if (err != 0) {\n            printf(\"failed to create vector index cached searcher with errcode=%d\\n\", err);\n            errcode = 5;\n            goto defer;\n        }\n    } else if (strcmp(cache_policy, \"content\") == 0) {\n        test->c_buffer = xdb_load_content_from_file(db_path);\n        if (test->c_buffer == NULL) {\n            printf(\"failed to load xdb content from `%s`\\n\", db_path);\n            errcode = 4;\n            goto defer;\n        }\n\n        err = xdb_new_with_buffer(version, &test->searcher, test->c_buffer);\n        if (err != 0) {\n            printf(\"failed to create content cached searcher with errcode=%d\\n\", err);\n            errcode = 5;\n            goto defer;\n        }\n    } else {\n        printf(\"invalid cache policy `%s`, options: file/vectorIndex/content\\n\", cache_policy);\n        errcode = 6;\n        goto defer;\n    }\n\n    // init the region buffer\n    // err = xdb_region_buffer_init(&test->region, test->region_buffer, sizeof(test->region_buffer));\n    err = xdb_region_buffer_init(&test->region, NULL, 0);\n    if (err != 0) {\n        printf(\"failed to init the region buffer with err=%d\\n\", err);\n        errcode = 7;\n        goto defer;\n    }\n\ndefer:\n    if (header != NULL) {\n        xdb_free_header(header);\n    }\n\n    if (handle != NULL) {\n        fclose(handle);\n    }\n\n    return errcode;\n}\n\nvoid destroy_searcher_test(searcher_test_t *test) {\n    xdb_close(&test->searcher);\n\n    // check and free the vector index\n    if (test->v_index != NULL) {\n        xdb_free_vector_index(test->v_index);\n        test->v_index = NULL;\n    }\n\n    // check and free the content buffer\n    if (test->c_buffer != NULL) {\n        xdb_free_content(test->c_buffer);\n        test->c_buffer = NULL;\n    }\n}\n\n//read a line from a command line.\nstatic char *get_line(FILE *fp, char *__dst) {\n    register int c;\n    register char *cs;\n\n    cs = __dst;\n    while ( ( c = getc( fp ) ) != EOF ) {\n        if ( c == '\\n' ) break;\n        *cs++ = c;\n    }\n    *cs = '\\0';\n\n    return ( c == EOF && cs == __dst ) ? NULL : __dst;\n}\n\nvoid print_help(char *argv[]) {\n    printf(\"ip2region xdb searcher\\n\");\n    printf(\"%s [command] [command options]\\n\", argv[0]);\n    printf(\"Command: \\n\");\n    printf(\"  search    search input test\\n\");\n    printf(\"  bench     search bench test\\n\");\n}\n\nvoid test_search(int argc, char *argv[]) {\n    int i, n, err;\n\n    // for args parse\n    char *r, key[33] = {'\\0'}, val[256] = {'\\0'};\n    char db_file[256] = {'\\0'}, cache_policy[16] = {\"vectorIndex\"};\n\n    // for search\n    long s_time, c_time;\n    char line[512] = {'\\0'};\n\n    // ip parse\n    xdb_version_t *version;\n    bytes_ip_t ip_bytes[16] = {'\\0'};\n\n    searcher_test_t test;\n\n    for (i = 2; i < argc; i++) {\n        r = argv[i];\n        if (strlen(r) < 5) {\n            continue;\n        }\n\n        if (r[0] != '-' || r[1] != '-') {\n            continue;\n        }\n\n        if (strchr(r, '=') == NULL) {\n            printf(\"missing = for args pair '%s'\\n\", r);\n            return;\n        }\n\n        n = sscanf(r+2, \"%32[^=]=%255[^\\n]\", key, val);\n        if (n != 2) {\n            printf(\"invalid option flag `%s`\\n\", r);\n            return;\n        }\n\n        // printf(\"key=%s, val=%s\\n\", key, val);\n        if (strcmp(key, \"db\") == 0) {\n            snprintf(db_file, sizeof(db_file), \"%s\", val);\n        } else if (strcmp(key, \"cache-policy\") == 0) {\n            memcpy(cache_policy, val, sizeof(cache_policy) - 1);\n            // snprintf(cache_policy, sizeof(cache_policy), \"%s\", val);\n        } else {\n            printf(\"undefined option `%s`\\n\", r);\n            return;\n        }\n    }\n\n    if (strlen(db_file) < 1) {\n        printf(\"%s search [command options]\\n\", argv[0]);\n        printf(\"options:\\n\");\n        printf(\" --db string              ip2region binary xdb file path\\n\");\n        printf(\" --cache-policy string    cache policy: file/vectorIndex/content\\n\");\n        return;\n    }\n\n    // init the win sock\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return;\n    }\n\n    // printf(\"db_file=%s, cache_policy=%s\\n\", db_file, cache_policy);\n    err = init_searcher_test(&test, db_file, cache_policy);\n    if (err != 0) {\n        // init program will print the error reasons;\n        return;\n    }\n\n    printf(\"ip2region xdb searcher test program\\n\"\n            \"source xdb: %s (%s, %s)\\n\"\n            \"type 'quit' to exit\\n\", db_file, xdb_get_version(&test.searcher)->name, cache_policy);\n    while ( 1 ) {\n        printf(\"ip2region>> \");\n        get_line(stdin, line);\n        if ( strlen(line) < 2 ) {\n            continue;\n        }\n\n        if (strcmp(line, \"quit\") == 0 ) {\n            break;\n        }\n\n        version = xdb_parse_ip(line, ip_bytes, sizeof(ip_bytes));\n        if (version == NULL) {\n            printf(\"invalid ip address `%s`\\n\", line);\n            continue;\n        }\n\n        s_time = xdb_now();\n        err = xdb_search(&test.searcher, ip_bytes, version->bytes, &test.region);\n        if (err != 0) {\n            printf(\"{err: %d, io_count: %d}\\n\", err, xdb_get_io_count(&test.searcher));\n        } else {\n            c_time = xdb_now() - s_time;\n            printf(\"{region: %s, io_count: %d, took: %ld μs}\\n\", test.region.value, xdb_get_io_count(&test.searcher), c_time);\n        }\n\n        // free the region\n        xdb_region_buffer_free(&test.region);\n    }\n\n    destroy_searcher_test(&test);\n    xdb_clean_winsock();\n    printf(\"searcher test program exited, thanks for trying\\n\");\n}\n\nvoid test_bench(int argc, char *argv[]) {\n    int i, n, err;\n    char *r, key[33] = {'\\0'}, val[256] = {'\\0'};\n    char db_file[256] = {'\\0'}, src_file[256] = {'\\0'}, cache_policy[16] = {\"vectorIndex\"};\n\n    FILE *handle;\n    char line[1024] = {'\\0'}, sip_str[INET6_ADDRSTRLEN+1] = {'\\0'}, eip_str[INET6_ADDRSTRLEN+1] = {'\\0'};\n    char src_region[512] = {'\\0'};\n    int count = 0, took;\n    long s_time, t_time, c_time = 0;\n\n    // ip parse\n    xdb_version_t *s_version, *e_version;\n    bytes_ip_t sip_bytes[16] = {'\\0'}, eip_bytes[16] = {'\\0'};\n    string_ip_t ip_string[INET6_ADDRSTRLEN] = {'\\0'};\n    bytes_ip_t *ip_list[2];\n\n    searcher_test_t test;\n    for (i = 2; i < argc; i++) {\n        r = argv[i];\n        if (strlen(r) < 5) {\n            continue;\n        }\n\n        if (r[0] != '-' || r[1] != '-') {\n            continue;\n        }\n\n        if (strchr(r, '=') == NULL) {\n            printf(\"missing = for args pair '%s'\\n\", r);\n            return;\n        }\n\n        n = sscanf(r+2, \"%32[^=]=%255[^\\n]\", key, val);\n        if (n != 2) {\n            printf(\"invalid option flag `%s`\\n\", r);\n            return;\n        }\n\n        if (strcmp(key, \"db\") == 0) {\n            snprintf(db_file, sizeof(db_file), \"%s\", val);\n        } else if (strcmp(key, \"src\") == 0) {\n            snprintf(src_file, sizeof(src_file), \"%s\", val);\n        } else if (strcmp(key, \"cache-policy\") == 0) {\n            memcpy(cache_policy, val, sizeof(cache_policy) - 1);\n        } else {\n            printf(\"undefined option `%s`\\n\", r);\n            return;\n        }\n    }\n\n    if (strlen(db_file) < 1 || strlen(src_file) < 1) {\n        printf(\"%s bench [command options]\\n\", argv[0]);\n        printf(\"options:\\n\");\n        printf(\" --db string              ip2region binary xdb file path\\n\");\n        printf(\" --src string             source ip text file path\\n\");\n        printf(\" --cache-policy string    cache policy: file/vectorIndex/content\\n\");\n        return;\n    }\n\n    // init the win sock\n    err = xdb_init_winsock();\n    if (err != 0) {\n        printf(\"failed to init the winsock with errno=%d\\n\", err);\n        return;\n    }\n\n    // printf(\"db_file=%s, src_file=%s, cache_policy=%s\\n\", db_file, src_file, cache_policy);\n    s_time = xdb_now();\n    err = init_searcher_test(&test, db_file, cache_policy);\n    if (err != 0) {\n        // the init function will print the details;\n        return;\n    }\n\n    // open the source file\n    handle = fopen(src_file, \"r\");\n    if (handle == NULL) {\n        printf(\"failed to open source text file `%s`\\n\", src_file);\n        return;\n    }\n\n    while(fgets(line, sizeof(line), handle) != NULL) {\n        n = sscanf(line, \"%46[^|]|%46[^|]|%511[^\\n]\", sip_str, eip_str, src_region);\n        if (n != 3) {\n            printf(\"invalid ip segment line `%s`\\n\", line);\n            return;\n        }\n\n        s_version = xdb_parse_ip(sip_str, sip_bytes, sizeof(sip_bytes));\n        if (s_version == NULL) {\n            printf(\"invalid start ip `%s`\\n\", sip_str);\n            return;\n        }\n\n        e_version = xdb_parse_ip(eip_str, eip_bytes, sizeof(eip_bytes));\n        if (e_version == NULL) {\n            printf(\"invalid end ip `%s`\\n\", sip_str);\n            return;\n        }\n\n        if (s_version->id != e_version->id) {\n            printf(\"start ip and end ip version not match for line `%s`\\n\", line);\n            return;\n        }\n\n        if (xdb_ip_sub_compare(sip_bytes, s_version->bytes, (string_ip_t *) eip_bytes, 0) > 0) {\n            printf(\"start ip(%s) should not be greater than end ip(%s)\\n\", sip_str, eip_str);\n            return;\n        }\n\n        ip_list[0] = sip_bytes;\n        ip_list[1] = eip_bytes;\n        for (i = 0; i < 2; i++) {\n            t_time = xdb_now();\n            err = xdb_search(&test.searcher, ip_list[i], s_version->bytes, &test.region);\n            c_time += xdb_now() - t_time;\n            if (err != 0) {\n                xdb_ip_to_string(ip_list[i], s_version->bytes, ip_string, sizeof(ip_string));\n                printf(\"failed to search ip `%s` with errno=%d\\n\", ip_string, err);\n                return;\n            }\n\n            // check the region info\n            if (strcmp(test.region.value, src_region) != 0) {\n                xdb_ip_to_string(ip_list[i], s_version->bytes, ip_string, sizeof(ip_string));\n                printf(\"failed to search(%s) with (%s != %s)\\n\", ip_string, test.region.value, src_region);\n                return;\n            }\n\n            // free the region buffer\n            xdb_region_buffer_free(&test.region);\n\n            count++;\n        }\n    };\n\n    took = xdb_now() - s_time;\n    destroy_searcher_test(&test);\n    xdb_clean_winsock();\n    fclose(handle);\n    printf(\"Bench finished, {cache_policy: %s, total: %d, took: %.3fs, cost: %d μs/op}\\n\",\n           cache_policy, count, took/1e6, count == 0 ? 0 : (int)(c_time/count));\n}\n\nint main(int argc, char *argv[]) {\n    if (argc < 2) {\n        print_help(argv);\n        return 0;\n    }\n\n    char *opt = argv[1];\n    if (strcmp(opt, \"search\") == 0) {\n        test_search(argc, argv);\n    } else if (strcmp(opt, \"bench\") == 0) {\n        test_bench(argc, argv);\n    } else {\n        print_help(argv);\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "binding/c/test_util.c",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/27\n\n#include \"stdio.h\"\n#include \"xdb_api.h\"\n\ntypedef void (* test_func_ptr) ();\nstruct test_func_entry {\n    char *name;\n    test_func_ptr func;\n};\ntypedef struct test_func_entry test_func_t;\n\nvoid test_load_header() {\n    xdb_header_t *header = xdb_load_header_from_file(\"../../data/ip2region_v4.xdb\");\n    if (header == NULL) {\n        printf(\"failed to load header\");\n    } else {\n        printf(\"header loaded: {\\n\"\n           \"    version: %d, \\n\"\n           \"    index_policy: %d, \\n\"\n           \"    created_at: %u, \\n\"\n           \"    start_index_ptr: %d, \\n\"\n           \"    end_index_ptr: %d\\n\"\n           \"    ip_version: %d\\n\"\n           \"    runtime_ptr_bytes: %d\\n\"\n           \"    length: %d\\n\"\n           \"}\\n\",\n           header->version, header->index_policy, header->created_at,\n           header->start_index_ptr, header->end_index_ptr, \n           header->ip_version, header->runtime_ptr_bytes, header->length\n       );\n    }\n\n    xdb_free_header(header);\n}\n\nvoid test_load_vector_index() {\n    xdb_vector_index_t *v_index = xdb_load_vector_index_from_file(\"../../data/ip2region_v4.xdb\");\n    if (v_index == NULL) {\n        printf(\"failed to load vector index from file\\n\");\n    } else {\n        printf(\"vector index loaded from file, length=%d\\n\", v_index->length);\n    }\n\n    xdb_free_vector_index(v_index);\n}\n\nvoid test_load_content() {\n    xdb_content_t *content = xdb_load_content_from_file(\"../../data/ip2region_v4.xdb\");\n    if (content == NULL) {\n        printf(\"failed to load content from file\\n\");\n    } else {\n        printf(\"content loaded from file, length=%d\\n\", content->length);\n    }\n\n    xdb_free_content(content);\n}\n\nvoid test_parse_ip() {\n    const char *ip_list[] = {\n        \"1.0.0.0\", \"58.251.30.115\", \"192.168.1.100\",\n        \"::\", \"2c0f:fff0::\", \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"240e:982:e617:ffff:ffff:ffff:ffff:ffff\", \n        \"219.xx.xx.11\", \"::xx:ffff\",\n        NULL\n    };\n\n    int errcode;\n    xdb_version_t *version;\n    bytes_ip_t ip_bytes[16] = {'\\0'};\n    string_ip_t ip_string[INET6_ADDRSTRLEN] = {'\\0'};\n\n    // init the sock env (for windows)\n    if ((errcode = xdb_init_winsock()) != 0) {\n        printf(\"failed to init the winsock\");\n        return;\n    }\n\n    for (int i = 0;; i++) {\n        if (ip_list[i] == NULL) {\n            break;\n        }\n\n        version = xdb_parse_ip(ip_list[i], ip_bytes, sizeof(ip_bytes));\n        if (version == NULL) {\n            printf(\"failed to parse ip `%s`\\n\", ip_list[i]);\n            continue;\n        }\n\n        xdb_ip_to_string(ip_bytes, version->bytes, ip_string, sizeof(ip_string));\n        printf(\"ip: %s (version=v%d), toString: %s\\n\", ip_list[i], version->id, ip_string);\n    }\n\n    // clean up the winsock\n    xdb_clean_winsock();\n}\n\nstruct ip_pair {\n    const char *sip;\n    const char *eip;\n};\nvoid test_ip_compare() {\n    struct ip_pair ip_pair_list[] = {\n        {\"1.0.0.0\", \"1.0.0.1\"},\n        {\"192.168.1.101\", \"192.168.1.90\"},\n        {\"219.133.111.87\", \"114.114.114.114\"},\n        {\"1.0.4.0\", \"1.0.1.0\"},\n        {\"1.0.4.0\", \"1.0.3.255\"},\n        {\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"},\n        {\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\"},\n        {\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\"},\n        {NULL, NULL}\n    };\n\n    struct ip_pair *pair_ptr = NULL;\n    bytes_ip_t sip_bytes[16] = {'\\0'};\n    bytes_ip_t eip_bytes[16] = {'\\0'};\n    xdb_version_t *s_version, *e_version;\n    int errcode;\n\n    // init the sock env (for windows)\n    if ((errcode = xdb_init_winsock()) != 0) {\n        printf(\"failed to init the winsock\");\n        return;\n    }\n\n    for (int i = 0; ;i++) {\n        pair_ptr = &ip_pair_list[i];\n        if (pair_ptr->sip == NULL) {\n            break;\n        }\n\n        s_version = xdb_parse_ip(pair_ptr->sip, sip_bytes, sizeof(sip_bytes));\n        if (s_version == NULL) {\n            printf(\"failed to parse sip `%s`\", pair_ptr->sip);\n            continue;\n        }\n\n        e_version = xdb_parse_ip(pair_ptr->eip, eip_bytes, sizeof(eip_bytes));\n        if (e_version == NULL) {\n            printf(\"failed to parse eip `%s`\", pair_ptr->eip);\n            continue;\n        }\n\n        if (s_version->id != e_version->id) {\n            printf(\"sip and eip version not match `%s` != `%s`\\n\", s_version->name, e_version->name);\n            continue;\n        }\n\n        printf(\n            \"ip_sub_compare(%s, %s): %d\\n\", \n            pair_ptr->sip, pair_ptr->eip, \n            xdb_ip_sub_compare(sip_bytes, s_version->bytes, (string_ip_t *) eip_bytes, 0)\n        );\n    }\n\n    // clean up the winsock\n    xdb_clean_winsock();\n}\n\n// please register your function heare\nstatic test_func_t _test_function_list[] = {\n    // xdb buffer\n    {\"test_load_header\", test_load_header},\n    {\"test_load_vector_index\", test_load_vector_index},\n    {\"test_load_content\", test_load_content},\n\n    // ip utils\n    {\"test_parse_ip\", test_parse_ip},\n    {\"test_ip_compare\", test_ip_compare},\n\n    {NULL, NULL}\n};\n\n// valgrind --tool=memcheck --leak-check=full ./a.out\nint main(int argc, char *argv[]) {\n    int i;\n    char *name;\n\n    // check and call the function\n    if (argc < 2) {\n        printf(\"please specified the function name to call\\n\");\n        return 1;\n    }\n\n    name = argv[1];\n    test_func_ptr func = NULL;\n    for (i = 0; ; i++) {\n        if (_test_function_list[i].name == NULL) {\n            break;\n        }\n\n        if (strcmp(name, _test_function_list[i].name) == 0) {\n            func = _test_function_list[i].func;\n            break;\n        }\n    }\n\n    if (func == NULL) {\n        printf(\"can't find test function `%s`\\n\", name);\n        return 1;\n    }\n    \n    // call the function\n    func();\n\n    return 0;\n}\n"
  },
  {
    "path": "binding/c/xdb_api.h",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/27\n\n#ifndef C_IP2REGION_XDB_H\n#define C_IP2REGION_XDB_H\n\n// @Note: \n// this define must be put before any header include\n// force the LFS for ftell\n#define _FILE_OFFSET_BITS 64\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#if ( defined(WIN32) || defined(_WIN32) || defined(__WINDOWS_) || defined(WINNT) )\n#   define XDB_PUBLIC(type)    extern __declspec(dllexport) type\n#   define XDB_PRIVATE(type)   static type\n#   define XDB_WINDOWS\n#include <windows.h>\n#include <winsock2.h>\n#include <ws2tcpip.h>\n#pragma comment(lib, \"ws2_32.lib\")\n#elif (defined(linux) || defined(_UNIX) || defined(__APPLE__) || defined(unix) || defined(__unix) || defined(__unix__) || defined(__linux__) || defined(linux) || defined(__linux))\n#   define XDB_PUBLIC(type)    extern type\n#   define XDB_PRIVATE(type)   static inline type\n#   define XDB_LINUX\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <arpa/inet.h>\n#else\n#   define XDB_PUBLIC(type) type\n#   define XDB_PRIVATE(type) static type\n#endif\n\n#define xdb_calloc( _blocks, _bytes )  calloc( _blocks, _bytes )\n#define xdb_malloc( _bytes )           malloc( _bytes )\n#define xdb_free( _ptr )               free( _ptr )\n\n// public constants define\n#define xdb_structure_20 2\n#define xdb_structure_30 3\n#define xdb_header_info_length 256\n#define xdb_vector_index_rows  256\n#define xdb_vector_index_cols  256\n#define xdb_vector_index_size  8\n#define xdb_v4_index_size 14    // 4 + 4 + 2 + 4\n#define xdb_v6_index_size 38    // 16 + 16 + 2 + 4\n\n// --- ip version info\n#define xdb_ipv4_id 4\n#define xdb_ipv6_id 6\n#define xdb_ipv4_bytes 4\n#define xdb_ipv6_bytes 16\n// cache of vector_index_row × vector_index_rows × vector_index_size\n#define xdb_vector_index_length 524288\n\n// --- xdb buffer functions\n\n// use the following buffer struct to wrap the binary buffer data\n// since the buffer data could not be operated with the string API.\nstruct xdb_header {\n    unsigned short version;\n    unsigned short index_policy;\n    unsigned int created_at;\n    unsigned int start_index_ptr;\n    unsigned int end_index_ptr;\n    \n    // since 3.0+ with IPv6 supporting\n    unsigned short ip_version;\n    unsigned short runtime_ptr_bytes;\n\n    // the original buffer\n    unsigned int length;\n    char buffer[xdb_header_info_length];\n};\ntypedef struct xdb_header xdb_header_t;\n\nXDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *);\n\nXDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *);\n\nXDB_PUBLIC(void) xdb_free_header(void *);\n\n\n// --- vector index buffer\nstruct xdb_vector_index {\n    unsigned int length;\n    char buffer[xdb_vector_index_length];\n};\ntypedef struct xdb_vector_index xdb_vector_index_t;\n\nXDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *);\n\nXDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *);\n\nXDB_PUBLIC(void) xdb_free_vector_index(void *);\n\n\n// --- content buffer\nstruct xdb_content {\n    unsigned int length;\n    char *buffer;\n};\ntypedef struct xdb_content xdb_content_t;\n\nXDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *);\n\nXDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *);\n\nXDB_PUBLIC(void) xdb_free_content(void *);\n\n// --- xdb verify\n\n// Verify if the current Searcher could be used to search the specified xdb file.\n// Why do we need this check ?\n// The future features of the xdb impl may cause the current searcher not able to work properly.\n//\n// @Note: You Just need to check this ONCE when the service starts\n// Or use another process (eg, A command) to check once Just to confirm the suitability.\nXDB_PUBLIC(int) xdb_verify(FILE *);\n\nXDB_PUBLIC(int) xdb_verify_from_header(FILE *handle, xdb_header_t *);\n\nXDB_PUBLIC(int) xdb_verify_from_file(const char *);\n\n// --- End xdb buffer\n\n\n// types type define\ntypedef char string_ip_t;\ntypedef unsigned char bytes_ip_t;\n\n// --- ip version\n#define XDB_IPv4 (xdb_version_v4())\n#define XDB_IPv6 (xdb_version_v6())\ntypedef int (* ip_compare_fn_t) (const bytes_ip_t *, int, const char *, int);\nstruct xdb_ip_version_entry {\n    int id;                 // version id\n    char *name;             // version name\n    int bytes;              // ip bytes number\n    int segment_index_size; // segment index size in bytes\n\n    // function to compare two ips\n    ip_compare_fn_t ip_compare;\n};\ntypedef struct xdb_ip_version_entry xdb_version_t;\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_v4();\nXDB_PUBLIC(xdb_version_t *) xdb_version_v6();\n\nXDB_PUBLIC(int) xdb_version_is_v4(const xdb_version_t *);\nXDB_PUBLIC(int) xdb_version_is_v6(const xdb_version_t *);\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_from_name(char *);\nXDB_PUBLIC(xdb_version_t *) xdb_version_from_header(xdb_header_t *);\n\n// --- END ip version\n\n// --- xdb util functions\n\n// to compatiable with the windows\n// returns: 0 for ok and -1 for failed\nXDB_PUBLIC(int) xdb_init_winsock();\nXDB_PUBLIC(void) xdb_clean_winsock();\n\n// get the current time in microseconds\nXDB_PUBLIC(long) xdb_now();\n\n// get unsigned long (4bytes) from a specified buffer start from the specified offset with little-endian\nXDB_PUBLIC(unsigned int) xdb_le_get_uint32(const char *, int);\n\n// get unsigned short (2bytes) from a specified buffer start from the specified offset with little-endian\nXDB_PUBLIC(int) xdb_le_get_uint16(const char *, int);\n\n\n// parse the specified IP address to byte array.\n// returns: xdb_version_t for valid ipv4 / ipv6, or NULL for failed\nXDB_PUBLIC(xdb_version_t *) xdb_parse_ip(const string_ip_t *, bytes_ip_t *, size_t);\n\n// parse the specified IPv4 address to byte array\n// returns: xdb_version_t for valid ipv4, or NULL for failed\nXDB_PUBLIC(xdb_version_t *) xdb_parse_v4_ip(const string_ip_t *, bytes_ip_t *, size_t);\n\n// parse the specified IPv6 address to byte array\n// returns: xdb_version_t for valid ipv6, or NULL for failed\nXDB_PUBLIC(xdb_version_t *) xdb_parse_v6_ip(const string_ip_t *, bytes_ip_t *, size_t);\n\n// convert a specified ip bytes to humen-readable string.\n// returns: 0 for success or -1 for failed.\nXDB_PUBLIC(int) xdb_ip_to_string(const bytes_ip_t *, int, char *, size_t);\n\n// ipv4 bytes to string\nXDB_PUBLIC(int) xdb_v4_ip_to_string(const bytes_ip_t *, char *, size_t);\n\n// ipv6 bytes to string\nXDB_PUBLIC(int) xdb_v6_ip_to_string(const bytes_ip_t *, char *, size_t);\n\n// compare the specified ip bytes with another ip bytes in the specified buff from offset.\n// ip args must be the return value from #xdb_parse_ip.\n// returns: -1 if ip1 < ip2, 1 if ip1 > ip2 or 0\nXDB_PUBLIC(int) xdb_ip_sub_compare(const bytes_ip_t *, int, const char *, int);\n\n// large file seek and tell\nXDB_PUBLIC(int) xdb_fseek(FILE *, long long, int);\n\nXDB_PUBLIC(long long) xdb_ftell(FILE *);\n\n// --- END xdb utils\n\n\n// --- xdb searcher api\n\n// xdb region info structure\n#define xdb_region_buffer_wrapper 1\n#define xdb_region_buffer_auto    2\nstruct xdb_region_buffer_entry {\n    int type;           // buffer type\n    char *value;        // region value\n    size_t length;      // buffer length\n};\ntypedef struct xdb_region_buffer_entry xdb_region_buffer_t;\n\n// wrapper the region from a local stack buffer.\n// returns: 0 for succeed or failed\nXDB_PUBLIC(int) xdb_region_buffer_init(xdb_region_buffer_t *, char *, size_t);\n\n// do the buffer alloc.\n// returns: 0 for ok or failed\nXDB_PUBLIC(int) xdb_region_buffer_alloc(xdb_region_buffer_t *, int);\n\n// empty alloc - empty string\n// returns:  0 - always\nXDB_PUBLIC(int) xdb_region_buffer_empty(xdb_region_buffer_t *);\n\nXDB_PUBLIC(void) xdb_region_buffer_free(xdb_region_buffer_t *);\n\n// xdb searcher structure\nstruct xdb_searcher_entry {\n    // ip version\n    xdb_version_t *version;\n\n    // xdb file handle\n    FILE *handle;\n\n    // header info\n    const char *header;\n    int io_count;\n\n    // vector index buffer cache.\n    // preload the vector index will reduce the number of IO operations\n    // thus speedup the search process.\n    const xdb_vector_index_t *v_index;\n\n    // content buffer.\n    // cache the whole xdb content.\n    const xdb_content_t *content;\n};\ntypedef struct xdb_searcher_entry xdb_searcher_t;\n\n// xdb searcher new api define\nXDB_PUBLIC(int) xdb_new_with_file_only(xdb_version_t *, xdb_searcher_t *, const char *);\n\nXDB_PUBLIC(int) xdb_new_with_vector_index(xdb_version_t *, xdb_searcher_t *, const char *, const xdb_vector_index_t *);\n\nXDB_PUBLIC(int) xdb_new_with_buffer(xdb_version_t *, xdb_searcher_t *, const xdb_content_t *);\n\nXDB_PUBLIC(void) xdb_close(void *);\n\n// xdb searcher search api define\nXDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *, const string_ip_t *, xdb_region_buffer_t *);\n\nXDB_PUBLIC(int) xdb_search(xdb_searcher_t *, const bytes_ip_t *, int, xdb_region_buffer_t *);\n\nXDB_PUBLIC(xdb_version_t *) xdb_get_version(xdb_searcher_t *);\n\nXDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *);\n\n// --- END xdb searcher api\n\n#endif // C_IP2REGION_XDB_H\n"
  },
  {
    "path": "binding/c/xdb_searcher.c",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/27\n\n#include \"xdb_api.h\"\n\n// --- region buffer\nXDB_PUBLIC(int) xdb_region_buffer_init(xdb_region_buffer_t *region, char *buffer, size_t length) {\n    if (buffer == NULL) {\n        region->type   = xdb_region_buffer_auto;\n        region->length = 0;\n    } else if (length <= 0) {\n        return 1;\n    } else {\n        region->type   = xdb_region_buffer_wrapper;\n        region->length = length;\n        memset(buffer, 0x00, length);   // zero-fill the buffer\n    }\n\n    region->value = buffer;\n    return 0;\n}\n\nXDB_PUBLIC(int) xdb_region_buffer_alloc(xdb_region_buffer_t *region, int length) {\n    if (length <= 0) {\n        return 1;\n    }\n\n    // no allocation supports for the buffer wapper\n    if (region->type == xdb_region_buffer_wrapper) {\n        if (length >= region->length) {\n            return 2;\n        }\n\n        region->value[length] = '\\0';\n        return 0;\n    }\n\n    // ensure that the value were freed\n    // by calling #xdb_region_buffer_free\n    if (region->value != NULL) {\n        return 3;\n    }\n\n    char *ptr = (char *) xdb_malloc(length + 1);\n    if (ptr == NULL) {\n        return 4;\n    }\n\n    ptr[length]    = '\\0';  // NULL-end\n    region->value  = ptr;\n    region->length = length;\n    return 0;\n}\n\n// fixed internal empty string ptr\nstatic char * _empty_region_string = \"\\0\";\n\nXDB_PUBLIC(int) xdb_region_buffer_empty(xdb_region_buffer_t *region) {\n    // no allocation supports for the buffer wapper\n    if (region->type == xdb_region_buffer_wrapper) {\n        region->value[0] = '\\0';\n        return 0;\n    }\n\n    // ensure that the value were freed\n    // by calling #xdb_region_buffer_free\n    if (region->value != NULL) {\n        return 3;\n    }\n\n    region->value  = _empty_region_string;\n    region->length = 0;\n    return 0;\n}\n\nXDB_PUBLIC(void) xdb_region_buffer_free(xdb_region_buffer_t *region) {\n    if (region->type == xdb_region_buffer_auto) {\n        // empty string interception\n        if (region->length == 0 \n            || region->value == _empty_region_string) {\n            // do nothing for empty string\n        } else {\n            xdb_free(region->value);\n        }\n\n        // reset the value\n        region->value = NULL;\n    }\n}\n\n// --- END region buffer\n\n// internal function prototype define\nXDB_PRIVATE(int) read(xdb_searcher_t *, long offset, char *, size_t length);\n\nXDB_PRIVATE(int) xdb_new_base(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index, const xdb_content_t *c_buffer) {\n    memset(xdb, 0x00, sizeof(xdb_searcher_t));\n\n    // set the version\n    xdb->version = version;\n\n    // check the content buffer first\n    if (c_buffer != NULL) {\n        xdb->v_index = NULL;\n        xdb->content = c_buffer;\n        return 0;\n    }\n\n    // open the xdb binary file\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return 1;\n    }\n\n    xdb->handle = handle;\n    xdb->v_index = v_index;\n\n    return 0;\n}\n\n// xdb searcher new api define\nXDB_PUBLIC(int) xdb_new_with_file_only(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path) {\n    return xdb_new_base(version, xdb, db_path, NULL, NULL);\n}\n\nXDB_PUBLIC(int) xdb_new_with_vector_index(xdb_version_t *version, xdb_searcher_t *xdb, const char *db_path, const xdb_vector_index_t *v_index) {\n    return xdb_new_base(version, xdb, db_path, v_index, NULL);\n}\n\nXDB_PUBLIC(int) xdb_new_with_buffer(xdb_version_t *version, xdb_searcher_t *xdb, const xdb_content_t *c_buffer) {\n    return xdb_new_base(version, xdb, NULL, NULL, c_buffer);\n}\n\nXDB_PUBLIC(void) xdb_close(void *ptr) {\n    xdb_searcher_t *xdb = (xdb_searcher_t *) ptr;\n    if (xdb->handle != NULL) {\n        fclose(xdb->handle);\n        xdb->handle = NULL;\n    }\n}\n\n// --- xdb searcher search api define\n\nXDB_PUBLIC(int) xdb_search_by_string(xdb_searcher_t *xdb, const string_ip_t *ip_string, xdb_region_buffer_t *region) {\n    bytes_ip_t ip_bytes[16] = {'\\0'};\n    xdb_version_t *version = xdb_parse_ip(ip_string, ip_bytes, sizeof(ip_bytes));\n    if (version == NULL) {\n        return 10;\n    } else {\n        return xdb_search(xdb, ip_bytes, version->bytes, region);\n    }\n}\n\nXDB_PUBLIC(int) xdb_search(xdb_searcher_t *xdb, const bytes_ip_t *ip_bytes, int ip_len, xdb_region_buffer_t *region) {\n    int il0, il1, idx, err, bytes, d_bytes;\n    register int seg_index_size, l, h, m, p;\n    unsigned int s_ptr, e_ptr, data_ptr, data_len;\n    char vector_buffer[xdb_vector_index_size];\n    char segment_buffer[xdb_v6_index_size];\n\n    // ip version check\n    if (ip_len != xdb->version->bytes) {\n        return -1;\n    }\n\n    // some resets\n    err = 0;\n    data_len = 0;\n    bytes   = xdb->version->bytes;\n    d_bytes = xdb->version->bytes << 1;\n    xdb->io_count = 0;\n\n    // locate the segment index block based on the vector index\n    il0 = (int) (ip_bytes[0]);\n    il1 = (int) (ip_bytes[1]);\n    idx = il0 * xdb_vector_index_cols * xdb_vector_index_size + il1 * xdb_vector_index_size;\n    if (xdb->v_index != NULL) {\n        s_ptr = xdb_le_get_uint32(xdb->v_index->buffer, idx);\n        e_ptr = xdb_le_get_uint32(xdb->v_index->buffer, idx + 4);\n    } else if (xdb->content != NULL) {\n        s_ptr = xdb_le_get_uint32(xdb->content->buffer, xdb_header_info_length + idx);\n        e_ptr = xdb_le_get_uint32(xdb->content->buffer, xdb_header_info_length + idx + 4);\n    } else {\n        err = read(xdb, xdb_header_info_length + idx, vector_buffer, sizeof(vector_buffer));\n        if (err != 0) {\n            return 10 + err;\n        }\n\n        s_ptr = xdb_le_get_uint32(vector_buffer, 0);\n        e_ptr = xdb_le_get_uint32(vector_buffer, 4);\n    }\n\n    // printf(\"s_ptr=%u, e_ptr=%u\\n\", s_ptr, e_ptr);\n    // @Note: ptr validate, zero ptr means source data missing\n    // so we could just stop here and return an empty string.\n    if (s_ptr == 0 || e_ptr == 0) {\n        xdb_region_buffer_empty(region);\n        return err;\n    }\n\n    // binary search to get the final region info\n    // segment_buffer = xdb_malloc(seg_index_size);\n    seg_index_size = xdb->version->segment_index_size;\n    data_len = 0, data_ptr = 0;\n    l = 0, h = ((int) (e_ptr - s_ptr)) / seg_index_size;\n    while (l <= h) {\n        m = (l + h) >> 1;\n        p = s_ptr + m * seg_index_size;\n\n        // read the segment index item\n        err = read(xdb, p, segment_buffer, seg_index_size);\n        if (err != 0) {\n            return 20 + err;\n        }\n\n        // decode the data fields as needed\n        if (xdb->version->ip_compare(ip_bytes, bytes, segment_buffer, 0) < 0) {\n            h = m - 1;\n        } else if (xdb->version->ip_compare(ip_bytes, bytes, segment_buffer, bytes) > 0) {\n            l = m + 1;\n        } else {\n            data_len = xdb_le_get_uint16(segment_buffer, d_bytes);\n            data_ptr = xdb_le_get_uint32(segment_buffer, d_bytes + 2);\n            break;\n        }\n    }\n\n    // printf(\"data_len=%u, data_ptr=%u\\n\", data_len, data_ptr);\n    if (data_len == 0) {\n        // return 100;\n        xdb_region_buffer_empty(region);\n        return err;\n    }\n\n    // buffer alloc checking\n    err = xdb_region_buffer_alloc(region, data_len);\n    if (err != 0) {\n        return 100 + err;\n    }\n\n    err = read(xdb, data_ptr, region->value, data_len);\n    if (err != 0) {\n        return 30 + err;\n    }\n\n    return err;\n}\n\nXDB_PRIVATE(int) read(xdb_searcher_t *xdb, long offset, char *buffer, size_t length) {\n    // check the xdb content cache first\n    if (xdb->content != NULL) {\n        memcpy(buffer, xdb->content->buffer + offset, length);\n        return 0;\n    }\n\n    // seek to the offset\n    if (fseek(xdb->handle, offset, SEEK_SET) == -1) {\n        return 1;\n    }\n\n    xdb->io_count++;\n    if (fread(buffer, 1, length, xdb->handle) != length) {\n        return 2;\n    }\n\n    return 0;\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_get_version(xdb_searcher_t *xdb) {\n    return xdb->version;\n}\n\nXDB_PUBLIC(int) xdb_get_io_count(xdb_searcher_t *xdb) {\n    return xdb->io_count;\n}\n"
  },
  {
    "path": "binding/c/xdb_util.c",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/27\n\n#include \"xdb_api.h\"\n#include <ctype.h>\n\n// for Linux\n#ifdef XDB_LINUX\n#include \"sys/time.h\"\n#endif\n\n#ifdef XDB_WINDOWS\n#include <time.h>\n#endif\n\n// @Note: since 2023/10/13 to compatible with the windows system\n#ifdef XDB_WINDOWS\nstatic int winsock_initialized = 0;\nXDB_PUBLIC(int) xdb_init_winsock() {\n    if (winsock_initialized == 1) {\n        return 0;\n    }\n\n    WSADATA wsaData;\n    if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {\n        return -1; \n    }\n    winsock_initialized = 1;\n    return 0;\n}\n\nXDB_PUBLIC(void) xdb_clean_winsock() {\n    if (winsock_initialized == 1) {\n        WSACleanup();\n        winsock_initialized = 0;\n    }\n}\n\nXDB_PRIVATE(int) gettimeofday(struct timeval* tp, void* tzp) {\n    time_t clock;\n    struct tm tm;\n    SYSTEMTIME wtm;\n    GetLocalTime(&wtm);\n    tm.tm_year = wtm.wYear - 1900;\n    tm.tm_mon = wtm.wMonth - 1;\n    tm.tm_mday = wtm.wDay;\n    tm.tm_hour = wtm.wHour;\n    tm.tm_min = wtm.wMinute;\n    tm.tm_sec = wtm.wSecond;\n    tm.tm_isdst = -1;\n    clock = mktime(&tm);\n    tp->tv_sec = clock;\n    tp->tv_usec = wtm.wMilliseconds * 1000;\n    return (0);\n}\n#else\nXDB_PUBLIC(int) xdb_init_winsock() {return 0;}\nXDB_PUBLIC(void) xdb_clean_winsock() {}\n#endif\n\n// --- xdb buffer function implementations\n\nXDB_PUBLIC(xdb_header_t *) xdb_load_header(FILE *handle) {\n    xdb_header_t *header;\n    unsigned int size = xdb_header_info_length;\n\n    // entry alloc\n    header = (xdb_header_t *) xdb_malloc(sizeof(xdb_header_t));\n    if (header == NULL) {\n        return NULL;\n    }\n\n    if (fseek(handle, 0, SEEK_SET) == -1) {\n        xdb_free(header);\n        return NULL;\n    }\n\n    if (fread(header->buffer, 1,size, handle) != size) {\n        xdb_free(header);\n        return NULL;\n    }\n\n    // fill the fields\n    header->length = size;\n    header->version = (unsigned short) xdb_le_get_uint16(header->buffer, 0);\n    header->index_policy = (unsigned short) xdb_le_get_uint16(header->buffer, 2);\n    header->created_at = xdb_le_get_uint32(header->buffer, 4);\n    header->start_index_ptr = xdb_le_get_uint32(header->buffer, 8);\n    header->end_index_ptr = xdb_le_get_uint32(header->buffer,12);\n\n    // since IPv6 supporting\n    header->ip_version = xdb_le_get_uint16(header->buffer, 16);\n    header->runtime_ptr_bytes = xdb_le_get_uint16(header->buffer, 18);\n\n    return header;\n}\n\nXDB_PUBLIC(xdb_header_t *) xdb_load_header_from_file(const char *db_path) {\n    xdb_header_t *header;\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return NULL;\n    }\n\n    header = xdb_load_header(handle);\n    fclose(handle);\n    return header;\n}\n\nXDB_PUBLIC(void) xdb_free_header(void *ptr) {\n    xdb_header_t *header = (xdb_header_t *) ptr;\n    if (header->length > 0) {\n        header->length = 0;\n        xdb_free(header);\n    }\n}\n\n// --- vector index\n\nXDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index(FILE *handle) {\n    xdb_vector_index_t *v_index;\n    unsigned int size = xdb_vector_index_length;\n\n    // seek to the vector index offset\n    if (fseek(handle, xdb_header_info_length, SEEK_SET) == -1) {\n        return NULL;\n    }\n\n    // do the buffer read\n    v_index = (xdb_vector_index_t *) xdb_malloc(sizeof(xdb_vector_index_t));\n    if (v_index == NULL) {\n        return NULL;\n    }\n\n    v_index->length = size;\n    if (fread(v_index->buffer, 1, size, handle) != size) {\n        xdb_free(v_index);\n        return NULL;\n    }\n\n    return v_index;\n}\n\nXDB_PUBLIC(xdb_vector_index_t *) xdb_load_vector_index_from_file(const char *db_path) {\n    xdb_vector_index_t *v_index;\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return NULL;\n    }\n\n    v_index = xdb_load_vector_index(handle);\n    fclose(handle);\n    return v_index;\n}\n\nXDB_PUBLIC(void) xdb_free_vector_index(void *ptr) {\n    xdb_vector_index_t *v_index = (xdb_vector_index_t *) ptr;\n    if (v_index->length > 0) {\n        v_index->length = 0;\n        xdb_free(v_index);\n    }\n}\n\n// --- content buffer\n\nXDB_PUBLIC(xdb_content_t *) xdb_load_content(FILE *handle) {\n    unsigned int size;\n    xdb_content_t *content;\n\n    // determine the file size\n    if (fseek(handle, 0, SEEK_END) == -1) {\n        return NULL;\n    }\n\n    size = (unsigned int) ftell(handle);\n    if (fseek(handle, 0, SEEK_SET) == -1) {\n        return NULL;\n    }\n\n    // do the file read\n    content = (xdb_content_t *) xdb_malloc(sizeof(xdb_content_t));\n    if (content == NULL) {\n        return NULL;\n    }\n\n    // do the buffer alloc\n    content->buffer = (char *) xdb_malloc(size);\n    if (content->buffer == NULL) {\n        xdb_free(content);\n        return NULL;\n    }\n\n    // read the content into the buffer\n    content->length = size;\n    if (fread(content->buffer, 1, size, handle) != size) {\n        xdb_free(content);\n        return NULL;\n    }\n\n    return content;\n}\n\nXDB_PUBLIC(xdb_content_t *) xdb_load_content_from_file(const char *db_path) {\n    xdb_content_t *content;\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return NULL;\n    }\n\n    content = xdb_load_content(handle);\n    fclose(handle);\n    return content;\n}\n\nXDB_PUBLIC(void) xdb_free_content(void *ptr) {\n    xdb_content_t *content = (xdb_content_t *) ptr;\n    if (content->length > 0) {\n        content->length = 0;\n        xdb_free(content->buffer);\n        content->buffer = NULL;\n        xdb_free(content);\n    }\n}\n\nXDB_PUBLIC(int) xdb_verify_from_header(FILE *handle, xdb_header_t *header) {\n    unsigned int runtime_ptr_bytes = 0;  // runtime ptr bytes\n    if (header->version == xdb_structure_20) {\n        runtime_ptr_bytes = 4;\n    } else if (header->version == xdb_structure_30) {\n        runtime_ptr_bytes = header->runtime_ptr_bytes;\n    } else {\n        return 2;\n    }\n\n    // 1, confirm the xdb file size.\n    // to ensure that the maximum file pointer does not overflow.\n    int err = fseek(handle, 0L, SEEK_END);\n    if (err != 0) {\n        return 3;\n    }\n\n    long long fileBytes = xdb_ftell(handle);\n    long long maxFilePtr = (1LL << (runtime_ptr_bytes * 8)) - 1;\n    // printf(\"fileBytes: %lld, maxFilePtr: %lld\\n\", fileBytes, maxFilePtr);\n    if (fileBytes > maxFilePtr) {\n        return 4;\n    }\n\n    return 0;\n}\n\nXDB_PUBLIC(int) xdb_verify(FILE *handle) {\n    xdb_header_t *header = xdb_load_header(handle);\n    if (header == NULL) {\n        return 1;\n    }\n\n    int errcode = xdb_verify_from_header(handle, header);\n    if (errcode != 0) {\n        goto done;\n    }\n\n    // what next ?\ndone:\n    xdb_free_header(header);\n    return errcode;\n}\n\nXDB_PUBLIC(int) xdb_verify_from_file(const char *db_path) {\n    FILE *handle = fopen(db_path, \"rb\");\n    if (handle == NULL) {\n        return -1;\n    }\n\n    int r = xdb_verify(handle);\n    fclose(handle);\n    return r;\n}\n\n// --- End content buffer\n\n\n// --- ip version\n\n// ip compare for IPv4\n// ip1 - with Big endian byte order parsed from an input\n// ip2 - with Little endian byte order read from the xdb index.\n// to compatiable with the Little Endian encoded IPv4 on xdb 2.0.\nXDB_PRIVATE(int) _ipv4_sub_compare(const bytes_ip_t *ip_bytes, int bytes, const char *buffer, int offset) {\n    register int i0, i1;\n    for (int i = 0, j = offset + bytes - 1; i < bytes; i++, j--) {\n        i0 = ip_bytes[i];\n        i1 = buffer[j] & 0xFF;\n        if (i0 > i1) {\n            return 1;\n        } else if (i0 < i1) {\n            return -1;\n        }\n    }\n    return 0;\n}\n\nstatic xdb_version_t _ip_version_list[] = {\n    // 14 = 4 + 4 + 2 + 4\n    {xdb_ipv4_id, \"IPv4\", xdb_ipv4_bytes, xdb_v4_index_size, _ipv4_sub_compare},\n\n    // 38 = 16 + 16 + 2 + 4\n    {xdb_ipv6_id, \"IPv6\", xdb_ipv6_bytes, xdb_v6_index_size, xdb_ip_sub_compare},\n\n    // END\n    {0, NULL, 0, 0, NULL}\n};\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_v4() {\n    return &_ip_version_list[0];\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_v6() {\n    return &_ip_version_list[1];\n}\n\nXDB_PUBLIC(int) xdb_version_is_v4(const xdb_version_t *version) {\n    return version->id == xdb_ipv4_id;\n}\n\nXDB_PUBLIC(int) xdb_version_is_v6(const xdb_version_t *version) {\n    return version->id == xdb_ipv6_id;\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_from_name(char *name) {\n    // to upper case the name\n    for (int i = 0; name[i] != '\\0'; i++) {\n        name[i] = toupper((unsigned char) name[i]);\n    }\n\n    if (strcmp(name, \"V4\") == 0 || strcmp(name, \"IPV4\") == 0) {\n        return xdb_version_v4();\n    } else if (strcmp(name, \"V6\") == 0 || strcmp(name, \"IPV6\") == 0) {\n        return xdb_version_v6();\n    } else {\n        return NULL;\n    }\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_version_from_header(xdb_header_t *header) {\n    // Old structure with ONLY IPv4 supports\n    if (header->version == xdb_structure_20) {\n        return xdb_version_v4();\n    }\n\n    // structure 3.0 with IPv6 supporting\n    if (header->version != xdb_structure_30) {\n        return NULL;\n    }\n\n    if (header->ip_version == xdb_ipv4_id) {\n        return xdb_version_v4();\n    } else if (header->ip_version == xdb_ipv6_id) {\n        return xdb_version_v6();\n    } else {\n        return NULL;\n    }\n}\n\n// --- END ip version\n\nXDB_PUBLIC(long) xdb_now() {\n    struct timeval c_time;\n    gettimeofday(&c_time, NULL);\n    return c_time.tv_sec * (int)1e6 + c_time.tv_usec;\n}\n\nXDB_PUBLIC(unsigned int) xdb_le_get_uint32(const char *buffer, int offset) {\n    return (\n        ((buffer[offset  ]) & 0x000000FF) |\n        ((buffer[offset+1] <<  8) & 0x0000FF00) |\n        ((buffer[offset+2] << 16) & 0x00FF0000) |\n        ((buffer[offset+3] << 24) & 0xFF000000)\n    );\n}\n\nXDB_PUBLIC(int) xdb_le_get_uint16(const char *buffer, int offset) {\n    return (\n        ((buffer[offset  ]) & 0x000000FF) |\n        ((buffer[offset+1] << 8) & 0x0000FF00)\n    );\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_parse_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {\n    char *d_ptr = strchr(ip_string, '.');\n    char *c_ptr = strchr(ip_string, ':');\n    // version check\n    if (d_ptr != NULL && c_ptr == NULL) {\n        return xdb_parse_v4_ip(ip_string, buffer, length);\n    } else if (c_ptr != NULL) {\n        return xdb_parse_v6_ip(ip_string, buffer, length);\n    }\n\n    return NULL;\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_parse_v4_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {\n    struct in_addr addr;\n\n    // buffer length checking\n    if (length < xdb_ipv4_bytes) {\n        return NULL;\n    }\n\n    if (inet_pton(AF_INET, ip_string, &addr) != 1) {\n        return NULL;\n    }\n\n    // encode the address to buffer with big endian byte bufffer.\n    buffer[0] = (addr.s_addr) & 0xFF;\n    buffer[1] = (addr.s_addr >> 8) & 0xFF;\n    buffer[2] = (addr.s_addr >> 16) & 0xFF;\n    buffer[3] = (addr.s_addr >> 24) & 0xFF;\n    return XDB_IPv4;\n}\n\nXDB_PUBLIC(xdb_version_t *) xdb_parse_v6_ip(const string_ip_t *ip_string, bytes_ip_t *buffer, size_t length) {\n    struct  in6_addr addr;\n\n    // buffer length checking\n    if (length < xdb_ipv6_bytes) {\n        return NULL;\n    }\n\n    if (inet_pton(AF_INET6, ip_string, &addr) != 1) {\n        return NULL;\n    }\n\n    memcpy(buffer, addr.s6_addr, xdb_ipv6_bytes);\n    return XDB_IPv6;\n}\n\nXDB_PUBLIC(int) xdb_ip_to_string(const bytes_ip_t *ip_bytes, int bytes, char *ip_string, size_t length) {\n    if (bytes == xdb_ipv4_bytes) {\n        return xdb_v4_ip_to_string(ip_bytes, ip_string, length);\n    } else if (bytes == xdb_ipv6_bytes) {\n        return xdb_v6_ip_to_string(ip_bytes, ip_string, length);\n    }\n\n    return -1;\n}\n\nXDB_PUBLIC(int) xdb_v4_ip_to_string(const bytes_ip_t *ip_bytes, char *ip_string, size_t length) {\n    if (!ip_bytes || !ip_string || length == 0) {\n        return -1;\n    }\n\n    // buffer length checking\n    if (length < INET_ADDRSTRLEN) {\n        return -1;\n    }\n\n    if (inet_ntop(AF_INET, ip_bytes, ip_string, length) == NULL) {\n        return -1;\n    }\n\n    return 0;\n}\n\nXDB_PUBLIC(int) xdb_v6_ip_to_string(const bytes_ip_t *ip_bytes, char *ip_string, size_t length) {\n    if (!ip_bytes || !ip_string || length == 0) {\n        return -1;\n    }\n\n    if (length < INET6_ADDRSTRLEN) {\n        return -1;\n    }\n\n    if (inet_ntop(AF_INET6, ip_bytes, ip_string, length) == NULL) {\n        return -1;\n    }\n\n    return 0;\n}\n\nXDB_PUBLIC(int) xdb_ip_sub_compare(const bytes_ip_t *ip1, int bytes, const char *buffer, int offset) {\n    register int i, i1, i2;\n    for (i = 0; i < bytes; i++) {\n        i1 = ip1[i];\n        i2 = buffer[offset + i] & 0xFF;\n        if (i1 > i2) {\n            return 1;\n        } else if (i1 < i2) {\n            return -1;\n        }\n    }\n    return 0;\n}\n\nXDB_PUBLIC(int) xdb_fseek(FILE *handle, long long offset, int whence) {\n    // we may have to use the large file solution later\n    // #if defined(XDB_LINUX)\n    //     return fseeko(handle, (off_t) offset, whence);\n    // #elif defined(XDB_WINDOWS)\n    //     return _fseeki64(handle, (__int64) offset, whence)\n    // #else\n    //     return fseek(handle, (long) offset, whence);\n    // #endif\n\n    return fseek(handle, (long) offset, whence);\n}\n\nXDB_PUBLIC(long long) xdb_ftell(FILE *handle) {\n    // we may have to use the large file solution later\n    // #if defined(XDB_LINUX)\n    //     return (long long) ftello(handle);\n    // #elif defined(XDB_WINDOWS)\n    //     return (long long) _ftelli64(handle);\n    // #else\n    //     // report error ?\n    //     return (long long) ftell(handle);\n    // #endif\n\n    return (long long) ftell(handle);\n}"
  },
  {
    "path": "binding/cpp/.gitignore",
    "content": "bin/\n"
  },
  {
    "path": "binding/cpp/Makefile",
    "content": "\nall: bin header search bench make edit\n\nFILES=$(wildcard src/*.cc)\n\nbin:\n\tmkdir -p bin\n\nheader: $(FILES) test/header.cc\n\tg++ -std=c++11 -O2 $^ -o bin/$@\n\nsearch: $(FILES) test/search.cc\n\tg++ -std=c++11 -O2 $^ -o bin/$@\n\nbench: $(FILES) test/bench.cc\n\tg++ -std=c++11 -O2 $^ -o bin/$@\n\nmake: $(FILES) test/make.cc\n\tg++ -std=c++11 -O2 $^ -o bin/$@\n\nedit: $(FILES)\n\tg++ -std=c++11 -O2 $^ test/edit_v4.cc -o bin/edit_v4\n\tg++ -std=c++11 -O2 $^ test/edit_v6.cc -o bin/edit_v6\n\nclean:\n\trm -rf bin\n\n"
  },
  {
    "path": "binding/cpp/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region C++ query client\n\n## 0. File Description\n\n```\nMakefile --------- Build\n\nsrc ------------------ Source directory\nsrc/base.* ----------- Constants and utility functions\nsrc/ip.* ------------- IP processing implementation\nsrc/header.* --------- xdb header parsing implementation\nsrc/search.* --------- xdb search implementation\nsrc/bench.* ---------- Search benchmarking implementation\nsrc/make.* ----------- xdb file generation implementation\nsrc/edit.* ----------- Raw data editing implementation\n\ntest ---------------- Test directory\ntest/header.cc ------ Test header\ntest/search.cc ------ Test search\ntest/bench.cc ------- Benchmarking\ntest/make.cc -------- Generate xdb file\ntest/edit_v4.cc ----- Test raw data editing (ipv4)\ntest/edit_v6.cc ----- Test raw data editing (ipv6)\n\n\nbin --------------- Executable directory (generated via make)\nbin/header -------- Test header\nbin/search -------- Test search\nbin/bench --------- Benchmarking\nbin/make ---------- Generate xdb file\nbin/edit_v4 ------- Test raw data editing (ipv4)\nbin/edit_v6 ------- Test raw data editing (ipv6)\n\nreadme.md --------- readme\n\n```\n\n## 1. Compilation\n\n```\n$ make\n```\n\n## 2. Search\n\n### 2.1 Example\n\n```cpp\n#include \"src/search.h\"\n\n// IP Version: xdb::ipv4 xdb::ipv6\n//    Policy: xdb::policy_file xdb::policy_vector xdb::policy_content\n//               No cache           Partial cache        Full cache\nint main() {\n    std::string xdb_name = \"../../data/ip2region_v6.xdb\";\n    int         version  = xdb::ipv6;\n    int         policy   = xdb::policy_content;\n    std::string ip       = \"2001:200:124::\";\n\n    xdb::search_t s(xdb_name, version, policy);\n    std::cout << s.search(ip) << std::endl;\n    return 0;\n}\n\n// $ g++ src/*.cc 1.cc --- Compile\n// $ ./a.out ------------- Test\n// Japan|Tokyo|Asagaya-minami|WIDE Project|JP\n```\n\n### 2.2 Test xdb Header\n\n```\n$ ./bin/header\nTest IPv4\nVersion: 3\nCache Policy: 1\nFile Generation Time: 2025-09-06 02:24:16\nIndex Start Address: 955933\nIndex End Address: 11042415\nIP Version: 4\nPointer Bytes: 4\n\nTest IPv6\nVersion: 3\nCache Policy: 1\nFile Generation Time: 2025-10-17 04:41:04\nIndex Start Address: 3094259\nIndex End Address: 36258303\nIP Version: 6\nPointer Bytes: 4\n```\n\n### 2.3 Test Search\n\n```\n$ ./bin/search\nTest IPv4   No cache: Success\nTest IPv4 Partial cache: Success\nTest IPv4 Full cache: Success\nTest IPv6   No cache: Success\nTest IPv6 Partial cache: Success\nTest IPv6 Full cache: Success\n```\n\n## 3. Benchmarking and Correctness Verification\n\n```\n./bin/bench\nTest IPv4,   No cache, total: 3910284, took:    27.60s, cost:   6.59μs/op, io count: 28227147\nTest IPv4, Partial cache, total: 3910284, took:    21.85s, cost:   5.15μs/op, io count: 24316863\nTest IPv4, Full cache, total: 3910284, took:     2.26s, cost:   0.25μs/op, io count: 0\nTest IPv6,   No cache, total: 4792520, took:   100.40s, cost:  20.22μs/op, io count: 80758866\nTest IPv6, Partial cache, total: 4792520, took:    93.06s, cost:  18.71μs/op, io count: 75966346\nTest IPv6, Full cache, total: 4792520, took:     6.24s, cost:   0.81μs/op, io count: 0\n```\n\n## 4. Generate xdb File\n\n### 4.1 Generate xdb File\n\n```\n$ ./bin/make\nGenerate ipv4 xdb file, took: 0.57s\nGenerate ipv6 xdb file, took: 1.24s\n```\n\n## 5. Raw Data Editing\n\n### 5.1. Instructions for Use\n\n* New IP attribution files can contain empty lines\n* New IP attribution files can be out of order; the program will automatically sort them\n* New IP attribution files can overlap; as long as there is no ambiguity, the program will automatically merge them\n* The final result will automatically merge adjacent lines with the same attribution\n* For the following tests, the original file uses the data file provided in the repository, and the new file uses 1.txt in the current directory\n"
  },
  {
    "path": "binding/cpp/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region C++ 查询客户端\n\n## 0. 文件说明\n```\nMakefile --------- 构建\n\nsrc ------------------ 源文件目录\nsrc/base.* ----------- 常量及工具函数\nsrc/ip.* ------------- 实现 IP 处理\nsrc/header.* --------- 实现 xdb 头部解析\nsrc/search.* --------- 实现 xdb 查找\nsrc/bench.* ---------- 实现 查找 测速\nsrc/make.* ----------- 实现 生成 xdb 文件\nsrc/edit.* ----------- 实现 原始数据编辑\n\ntest ---------------- 测试目录\ntest/header.cc ------ 测试 头部\ntest/search.cc ------ 测试 查找\ntest/bench.cc ------- 测速\ntest/make.cc -------- 生成 xdb 文件\ntest/edit_v4.cc ----- 测试 原始数据编辑(ipv4)\ntest/edit_v6.cc ----- 测试 原始数据编辑(ipv6)\n\n\nbin --------------- 可执行文件目录(通过 make 生成)\nbin/header -------- 测试 头部\nbin/search -------- 测试 查找\nbin/bench --------- 测速\nbin/make ---------- 生成 xdb 文件\nbin/edit_v4 ------- 测试 原始数据编辑(ipv4)\nbin/edit_v6 ------- 测试 原始数据编辑(ipv6)\n\nreadme.md --------- readme\n```\n\n## 1. 编译\n```\n$ make\n```\n\n## 2. 查找\n### 2.1 示例\n```cpp\n#include \"src/search.h\"\n\n// IP 版本: xdb::ipv4 xdb::ipv6\n//    策略: xdb::policy_file xdb::policy_vector xdb::policy_content\n//               不缓存           部分缓存           全部缓存\nint main() {\n    std::string xdb_name = \"../../data/ip2region_v6.xdb\";\n    int         version  = xdb::ipv6;\n    int         policy   = xdb::policy_content;\n    std::string ip       = \"2001:200:124::\";\n\n    xdb::search_t s(xdb_name, version, policy);\n    std::cout << s.search(ip) << std::endl;\n    return 0;\n}\n\n// $ g++ src/*.cc 1.cc --- 编译\n// $ ./a.out ------------- 测试\n// Japan|Tokyo|Asagaya-minami|WIDE Project|JP\n```\n\n### 2.2 测试 xdb 头部\n```\n$ ./bin/header\n测试 IPv4\n版本号: 3\n缓存策略: 1\n文件生成时间: 2025-09-06 02:24:16\n索引起始地址: 955933\n索引结束地址: 11042415\nIP版本: 4\n指针字节数: 4\n\n测试 IPv6\n版本号: 3\n缓存策略: 1\n文件生成时间: 2025-10-17 04:41:04\n索引起始地址: 3094259\n索引结束地址: 36258303\nIP版本: 6\n指针字节数: 4\n```\n\n### 2.3 测试查找\n```\n$ ./bin/search\n测试 IPv4   不缓存: 成功\n测试 IPv4 部分缓存: 成功\n测试 IPv4 全部缓存: 成功\n测试 IPv6   不缓存: 成功\n测试 IPv6 部分缓存: 成功\n测试 IPv6 全部缓存: 成功\n```\n\n## 3. 测速以及检验正确性\n```\n./bin/bench\n测试 IPv4,   不缓存, total: 3910284, took:    27.60s, cost:   6.59μs/op, io count: 28227147\n测试 IPv4, 部分缓存, total: 3910284, took:    21.85s, cost:   5.15μs/op, io count: 24316863\n测试 IPv4, 全部缓存, total: 3910284, took:     2.26s, cost:   0.25μs/op, io count: 0\n测试 IPv6,   不缓存, total: 4792520, took:   100.40s, cost:  20.22μs/op, io count: 80758866\n测试 IPv6, 部分缓存, total: 4792520, took:    93.06s, cost:  18.71μs/op, io count: 75966346\n测试 IPv6, 全部缓存, total: 4792520, took:     6.24s, cost:   0.81μs/op, io count: 0\n```\n\n## 4. 生成 xdb 文件\n### 4.1 生成 xdb 文件\n```\n$ ./bin/make\n生成 ipv4 的 xdb 文件, took: 0.57s\n生成 ipv6 的 xdb 文件, took: 1.24s\n```\n\n## 5. 原始数据编辑\n### 5.1. 使用说明\n* 新的IP归属地文件可以包含空行\n* 新的IP归属地文件顺序可以乱序, 程序会自动排序\n* 新的IP归属地文件顺序可以重叠, 只要无二义性, 程序会自动合并\n* 最终的结果会将相邻的且归属地相同的行自动合并\n* 以下测试, 原文件使用仓库自带的数据文件, 新文件使用当前目录下的 1.txt\n"
  },
  {
    "path": "binding/cpp/src/base.cc",
    "content": "\n#include \"base.h\"\n\nnamespace xdb {\n\nint ip_version;  // ip 版本\nint ip_size;     // ip 占的字节数\nint content_size;\n\nvoid init_xdb(int version) {\n    ip_version   = version;\n    ip_size      = version == ipv4 ? 4 : 16;\n    content_size = ip_size * 2 + 2 + 4;\n}\n\nvoid log_exit(const string &msg) {\n    std::cout << msg << std::endl;\n    exit(-1);\n}\n\nvoid read_bin(int index, char *buf, size_t len, FILE *db) {\n    fseek(db, index, SEEK_SET);\n    if (fread(buf, 1, len, db) != len)\n        log_exit(__func__);\n}\n\nunsigned to_uint(const char *buf) {\n    return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00) |\n           ((buf[2] << 16) & 0x00FF0000) | ((buf[3] << 24) & 0xFF000000);\n}\n\nunsigned to_ushort(const char *buf) {\n    return ((buf[0]) & 0x000000FF) | ((buf[1] << 8) & 0x0000FF00);\n}\n\nunsigned to_int(const char *buf, int n) {\n    return n == 2 ? to_ushort(buf) : to_uint(buf);\n}\n\nvoid write_uint(unsigned data, char buf[]) {\n    buf[0] = (data >> 0) & 0xFF;\n    buf[1] = (data >> 8) & 0xFF;\n    buf[2] = (data >> 16) & 0xFF;\n    buf[3] = (data >> 24) & 0xFF;\n}\n\nvoid write_uint(unsigned data, FILE *dst) {\n    char buf[4];\n    write_uint(data, buf);\n    fwrite(buf, 1, sizeof(buf), dst);\n}\n\nvoid write_ushort(unsigned data, char buf[]) {\n    buf[0] = (data >> 0) & 0xFF;\n    buf[1] = (data >> 8) & 0xFF;\n}\n\nvoid write_ushort(unsigned data, FILE *dst) {\n    char buf[2];\n    write_ushort(data, buf);\n    fwrite(buf, 1, sizeof(buf), dst);\n}\n\nvoid write_string(const char *buf, unsigned len, FILE *dst) {\n    fwrite(buf, 1, len, dst);\n}\n\nunsigned long long get_time() {\n    struct timeval tv1;\n    gettimeofday(&tv1, NULL);\n    return (unsigned long long)tv1.tv_sec * 1000 * 1000 + tv1.tv_usec;\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/base.h",
    "content": "#ifndef BASE_H\n#define BASE_H\n\n#include <arpa/inet.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/time.h>\n\n#include <algorithm>\n#include <iostream>\n#include <list>\n#include <map>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\nnamespace xdb {\n\nusing std::string;\n\nconstexpr int ipv4 = 4;\nconstexpr int ipv6 = 6;\n\nconstexpr int policy_file    = 0;\nconstexpr int policy_vector  = 1;\nconstexpr int policy_content = 2;\n\nconstexpr int length_header = 256;\nconstexpr int length_vector = 256 * 256 * 8;\n\nextern int ip_version;  // ip 版本\nextern int ip_size;     // ip 占的字节数\nextern int content_size;\n\nvoid init_xdb(int version);\n\nvoid log_exit(const string &msg);\n\nvoid read_bin(int index, char *buf, size_t len, FILE *db);\n\nunsigned to_uint(const char *buf);\nunsigned to_ushort(const char *buf);\nunsigned to_int(const char *buf, int n);\n\nvoid write_uint(unsigned data, char buf[]);\nvoid write_uint(unsigned data, FILE *dst);\n\nvoid write_ushort(unsigned data, char buf[]);\nvoid write_ushort(unsigned data, FILE *dst);\n\nvoid write_string(const char *buf, unsigned len, FILE *dst);\n\nunsigned long long get_time();\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/src/bench.cc",
    "content": "\n#include \"bench.h\"\n\nnamespace xdb {\n\nbench_t::bench_t(const std::string &file_name, int version, int policy)\n    : search(file_name, version, policy) {\n}\n\nvoid bench_t::test_one(const ip_t &ip, const string region) {\n    if (search.search(ip.to_string()) != region)\n        xdb::log_exit(\"failed: \" + ip.to_string() + \" \" + region);\n    sum_io_count += search.get_io_count();\n    sum_cost_time += search.get_cost_time();\n    sum_count++;\n}\n\nvoid bench_t::test_line(char *buf) {\n    size_t buf_len = strlen(buf);\n    if (buf_len == 0)\n        return;\n    buf[buf_len - 1] = '\\0';  // 去掉换行符\n\n    node_t node(buf);\n\n    // 只测五个\n    for (int i = 0; i < 5 && node.ip1 < node.ip2; ++i) {\n        test_one(node.ip1, node.region);\n        node.ip1 = node.ip1 + 1;\n    }\n    test_one(node.ip2, node.region);\n}\n\nvoid bench_t::test_file(const std::string &file_name) {\n    FILE *f = fopen(file_name.data(), \"r\");\n    if (f == NULL)\n        xdb::log_exit(\"can't open \" + file_name);\n    char buf[1024];\n    while (fgets(buf, sizeof(buf), f) != NULL)\n        test_line(buf);\n}\n\nvoid bench_t::test(const string &file_name) {\n    sum_io_count  = 0;\n    sum_cost_time = 0;\n    sum_count     = 0;\n\n    unsigned long long tv1 = xdb::get_time();\n    test_file(file_name);\n    unsigned long long tv2 = xdb::get_time();\n\n    double took = (tv2 - tv1) * 1.0 / 1000 / 1000;\n    double cost = sum_cost_time * 1.0 / sum_count;\n\n    printf(\n        \"total: %llu, took: %8.2fs, cost: %6.2fμs/op, io \"\n        \"count: \"\n        \"%llu\\n\",\n        sum_count,\n        took,\n        cost,\n        sum_io_count);\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/bench.h",
    "content": "#ifndef BENCH_H\n#define BENCH_H\n\n#include \"search.h\"\n\nnamespace xdb {\n\nclass bench_t {\npublic:\n    bench_t(const string &file_name, int version, int policy);\n\n    void test(const string &file_name);\n\nprivate:\n    void test_one(const ip_t &ip, const string region);\n    void test_line(char *buf);\n    void test_file(const std::string &file_name);\n\n    search_t search;\n\n    unsigned long long sum_io_count;\n    unsigned long long sum_cost_time;\n    unsigned long long sum_count;\n};\n\n}  // namespace xdb\n#endif\n"
  },
  {
    "path": "binding/cpp/src/edit.cc",
    "content": "\n#include \"edit.h\"\n\nnamespace xdb {\n\nvoid handle_ip_txt(const string& name, std::list<node_t>& regions) {\n    FILE* f = fopen(name.data(), \"r\");\n    if (f == NULL)\n        log_exit(\"can't open \" + name);\n\n    char buf[1024];\n    while (fgets(buf, sizeof(buf), f) != NULL) {\n        unsigned int buf_len = strlen(buf);\n        // 去掉多余的空\n        while (buf_len > 0 && isspace(buf[buf_len - 1]))\n            --buf_len;\n        if (buf_len == 0)\n            continue;\n        buf[buf_len] = '\\0';\n        regions.push_back(node_t(buf));\n    }\n\n    fclose(f);\n}\n\nvoid edit_t::handle_new_file(const std::string& file_name) {\n    handle_ip_txt(file_name, new_regions);  // 输入\n    new_regions.sort();                     // 排序\n\n    // 检验及其去重\n    auto it = new_regions.begin();\n\n    for (;;) {\n        if (it == new_regions.end())\n            break;\n        auto next = it;\n        ++next;\n        if (next == new_regions.end())\n            break;\n        if (it->ip1 > it->ip2)\n            it = new_regions.erase(it);  // 非法, 直接跳过\n        else if (it->ip1 == next->ip1 || next->ip1 <= it->ip2) {\n            // 数据重叠\n            if (it->region != next->region)\n                log_exit(\"数据有二义性: \" + it->to_string() + \", \" +\n                         next->to_string());\n            it->ip2 = std::max(it->ip2, next->ip2);\n            new_regions.erase(next);\n        } else if (it->ip2 + 1 == next->ip1 && it->region == next->region) {\n            // 数据连接\n            it->ip2 = next->ip2;\n            new_regions.erase(next);\n        } else {\n            ++it;\n        }\n    }\n}\n\nvoid edit_t::handle_old_file(const std::string& file_name) {\n    handle_ip_txt(file_name, old_regions);\n}\n\nvoid edit_t::merge() {\n    auto it1 = old_regions.begin();\n    auto it2 = new_regions.begin();\n\n    for (;;) {\n        if (it2 == new_regions.end())\n            break;\n        if (it2->ip1 > it2->ip2) {\n            ++it2;\n            continue;\n        }\n        // it1->ip1 it1->ip2 it2->ip1 it2->ip2\n        while (it1->ip2 < it2->ip1)\n            ++it1;\n        if (it1->ip2 <= it2->ip2) {\n            // it1->ip1 it2->ip1 it1->ip2 it2->ip2\n            node_t node;\n            node.ip1    = it2->ip1;\n            node.ip2    = it1->ip2;\n            node.region = it2->region;\n\n            it1->ip2 = node.ip1 - 1;\n            it2->ip1 = node.ip2 + 1;\n\n            ++it1;\n            it1 = old_regions.insert(it1, node);\n            ++it1;\n        } else {\n            // it1->ip1 it2->ip1 it2->ip2 it1->ip2\n            node_t node;\n            node.ip1    = it2->ip2 + 1;\n            node.ip2    = it1->ip2;\n            node.region = it1->region;\n\n            it1->ip2 = it2->ip1 - 1;\n\n            ++it1;\n            it1 = old_regions.insert(it1, *it2);\n\n            ++it1;\n            it1 = old_regions.insert(it1, node);\n\n            ++it2;\n        }\n    }\n}\n\nvoid edit_t::write_old_file(const std::string& file_name) {\n    FILE* f = fopen(file_name.data(), \"w\");\n    if (f == NULL)\n        log_exit(\"can't open \" + file_name);\n\n    auto it = old_regions.begin();\n\n    // 删除非法的数据\n    for (;;) {\n        if (it == old_regions.end())\n            break;\n        if (it->ip1 > it->ip2)\n            it = old_regions.erase(it);\n        else\n            ++it;\n    }\n\n    // 合并数据域相同的相邻数据\n    it = old_regions.begin();\n    for (;;) {\n        if (it == old_regions.end())\n            break;\n        auto next = it;\n        ++next;\n        if (next == old_regions.end())\n            break;\n        if (it->region == next->region) {\n            it->ip2 = next->ip2;\n            old_regions.erase(next);\n        } else {\n            ++it;\n        }\n    }\n\n    for (auto& d : old_regions) {\n        string res =\n            d.ip1.to_string() + \"|\" + d.ip2.to_string() + \"|\" + d.region + \"\\n\";\n        fputs(res.data(), f);\n    }\n\n    fclose(f);\n}\n\nedit_t::edit_t(const string& name_old, const string& name_new, int version) {\n    unsigned long long tv1 = get_time();\n\n    init_xdb(version);\n\n    handle_new_file(name_new);\n    handle_old_file(name_old);\n    merge();\n    write_old_file(name_old);\n\n    unsigned long long tv2 = get_time();\n\n    double took = (tv2 - tv1) * 1.0 / 1000 / 1000;\n\n    printf(\"took: %.2fs\\n\", took);\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/edit.h",
    "content": "#ifndef EDIT_H\n#define EDIT_H\n\n#include \"ip.h\"\n\nnamespace xdb {\n\nclass edit_t {\npublic:\n    edit_t(const string& old_name, const string& new_name, int version);\n\nprivate:\n    void handle_new_file(const string& file_name);\n    void handle_old_file(const string& file_name);\n    void merge();\n    void write_old_file(const string& file_name);\n\n    std::list<node_t> old_regions;\n    std::list<node_t> new_regions;\n};\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/src/header.cc",
    "content": "\n#include \"header.h\"\n\nnamespace xdb {\n\nheader_t::header_t(FILE* db) {\n    read_bin(0, header, sizeof(header), db);\n}\n\nheader_t::~header_t() {\n}\n\nint header_t::version() {\n    return to_int(header, 2);  // 版本号(2)\n}\n\nint header_t::index_policy() {\n    return to_int(header + 2, 2);  // 缓存策略(2)\n}\n\nint header_t::create_at() {\n    return to_int(header + 4, 4);  // 文件生成时间(4)\n}\n\nint header_t::index_start() {\n    return to_int(header + 8, 4);  // 索引起始地址(4)\n}\n\nint header_t::index_end() {\n    return to_int(header + 12, 4);  // 索引结束地址(4)\n}\n\nint header_t::ip_version() {\n    return to_int(header + 16, 2);  // IP 版本(2)\n}\n\nint header_t::ptr() {\n    return to_int(header + 18, 2);  // 指针字节数(2)\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/header.h",
    "content": "#ifndef HEADER_H\n#define HEADER_H\n\n#include \"base.h\"\n\nnamespace xdb {\n\nclass header_t {\npublic:\n    header_t(FILE* db);\n    virtual ~header_t();\n\n    int version();       // 版本号\n    int index_policy();  // 缓存策略\n    int create_at();     // 文件生成时间\n    int index_start();   // 索引起始地址\n    int index_end();     // 索引结束地址\n    int ip_version();    // IP 版本\n    int ptr();           // 指针字节数\n\nprotected:\n    char header[length_header];\n};\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/src/ip.cc",
    "content": "\n#include \"ip.h\"\n\nnamespace xdb {\n\nip_t::ip_t() {\n    memset(p, '\\0', sizeof(p));\n}\n\nip_t::ip_t(const ip_t& rhs, int val) {\n    memcpy(p, rhs.p, ip_size);\n\n    if (val == 0 || val == 255)\n        for (int i = 2; i < ip_size; ++i)\n            p[i] = val;\n}\n\nip_t::ip_t(const char* p) {\n    from_xdb(p);\n}\n\nbool ip_t::from_str(const string& str) {\n    int af_inet = ip_version == ipv4 ? AF_INET : AF_INET6;\n    return inet_pton(af_inet, str.data(), p) == 1;\n}\n\nvoid ip_t::from_xdb(const char str[16]) {\n    for (int i = 0; i < ip_size; ++i)\n        if (ip_version == ipv6)\n            p[i] = str[i];\n        else\n            p[i] = str[ip_size - 1 - i];\n}\n\nip_t& ip_t::operator=(const ip_t& rhs) {\n    memcpy(p, rhs.p, ip_size);\n    return *this;\n}\n\nint ip_t::compare(const ip_t& rhs) const {\n    for (int i = 0; i < ip_size; ++i) {\n        if ((unsigned char)p[i] > (unsigned char)rhs.p[i])\n            return 1;\n        if ((unsigned char)p[i] < (unsigned char)rhs.p[i])\n            return -1;\n    }\n    return 0;\n}\n\nbool ip_t::operator<(const ip_t& rhs) const {\n    return compare(rhs) < 0;\n}\n\nbool ip_t::operator<=(const ip_t& rhs) const {\n    return compare(rhs) <= 0;\n}\n\nbool ip_t::operator>(const ip_t& rhs) const {\n    return compare(rhs) > 0;\n}\n\nbool ip_t::operator>=(const ip_t& rhs) const {\n    return compare(rhs) >= 0;\n}\n\nbool ip_t::operator==(const ip_t& rhs) const {\n    return compare(rhs) == 0;\n}\n\nbool ip_t::operator!=(const ip_t& rhs) const {\n    return compare(rhs) != 0;\n}\n\nstring ip_t::to_string() const {\n    char buf[INET6_ADDRSTRLEN + 1];\n    int  af_inet = ip_version == ipv4 ? AF_INET : AF_INET6;\n    inet_ntop(af_inet, p, buf, sizeof(buf));\n    return string(buf);\n}\n\nstring ip_t::to_bit() const {\n    string str;\n    for (int i = 0; i < ip_size; ++i)\n        if (ip_version == ipv6)\n            str.push_back(p[i]);\n        else\n            str.push_back(p[ip_size - 1 - i]);\n    return str;\n}\n\nip_t operator+(const ip_t& lhs, int v) {\n    ip_t ip;\n\n    int i = ip_size;\n    while (--i >= 0) {\n        v += lhs.p[i];\n        ip.p[i] = v % 256;\n        v /= 256;\n    }\n    return ip;\n}\n\nip_t operator-(const ip_t& lhs, int v) {\n    ip_t ip;\n\n    int i = ip_size;\n    v     = -v;\n    while (--i >= 0) {\n        v += lhs.p[i];\n        if (v == -1)\n            ip.p[i] = 255;\n        else {\n            ip.p[i] = v;\n            v       = 0;\n        }\n    }\n    return ip;\n}\n\n// node_t\nnode_t::node_t() {\n}\n\nnode_t::node_t(char* buf) {\n    char* pos1 = strchr(buf, '|');\n\n    if (pos1 == NULL)\n        log_exit(\"invalid data: \" + std::string(buf));\n    char* pos2 = strchr(pos1 + 1, '|');\n    if (pos2 == NULL)\n        log_exit(\"invalid data: \" + std::string(buf));\n    *pos1 = '\\0';\n    *pos2 = '\\0';\n\n    region = pos2 + 1;\n\n    if (!ip1.from_str(buf) || !ip2.from_str(pos1 + 1) || ip2 < ip1 ||\n        region.empty()) {\n        *pos1 = *pos2 = '|';\n        log_exit(string(\"invalid data: \") + buf);\n    }\n}\n\nbool node_t::operator<(const node_t& rhs) const {\n    if (ip1 < rhs.ip1)\n        return true;\n    return ip2 < rhs.ip2;\n}\n\nstring node_t::to_string() const {\n    return ip1.to_string() + \"|\" + ip2.to_string() + \"|\" + region;\n}\n\nstring node_t::to_bit() const {\n    return ip1.to_bit() + ip2.to_bit();\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/ip.h",
    "content": "#ifndef IP_H\n#define IP_H\n\n#include \"base.h\"\n\nnamespace xdb {\n\nstruct ip_t {\n    unsigned char p[16];\n\n    ip_t();\n    ip_t(const char* p);\n    // val 为 0 或 255 时, 将 ip 的后几位置为 val\n    ip_t(const ip_t& rhs, int val = -1);\n\n    bool from_str(const string& str);\n    void from_xdb(const char str[16]);\n\n    ip_t& operator=(const ip_t& rhs);\n\n    int  compare(const ip_t& rhs) const;\n    bool operator<(const ip_t& rhs) const;\n    bool operator<=(const ip_t& rhs) const;\n    bool operator>(const ip_t& rhs) const;\n    bool operator>=(const ip_t& rhs) const;\n    bool operator==(const ip_t& rhs) const;\n    bool operator!=(const ip_t& rhs) const;\n\n    string to_string() const;\n    string to_bit() const;\n};\n\nip_t operator+(const ip_t& lhs, int v);\nip_t operator-(const ip_t& lhs, int v);\n\nstruct node_t {\n    ip_t   ip1;\n    ip_t   ip2;\n    string region;\n\n    node_t();\n    node_t(char* buf);\n\n    bool operator<(const node_t& rhs) const;\n\n    string to_string() const;\n    string to_bit() const;\n};\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/src/make.cc",
    "content": "\n#include \"make.h\"\n\nnamespace xdb {\n\nvoid make_t::vector_index_push_back(int row, int col, const node_t &node) {\n    vector_index[row][col].push_back(\n        std::make_pair<string, string>(node.to_bit(), string(node.region)));\n}\n\nvoid make_t::vector_index_push_back(node_t &node) {\n    ip_t ip1 = node.ip1;\n    ip_t ip2 = node.ip2;\n\n    unsigned ip1_1 = ip1.p[0];\n    unsigned ip1_2 = ip1.p[1];\n    unsigned ip2_1 = ip2.p[0];\n    unsigned ip2_2 = ip2.p[1];\n\n    if (ip1_1 == ip2_1 && ip1_2 == ip2_2) {\n        vector_index_push_back(ip1_1, ip1_2, node);\n        return;\n    }\n\n    node.ip1 = ip1;\n    node.ip2 = ip_t(ip1, 255);\n    vector_index_push_back(ip1_1, ip1_2, node);\n\n    node.ip1 = ip_t(ip2, 0);\n    node.ip2 = ip2;\n    vector_index_push_back(ip2_1, ip2_2, node);\n\n    for (;;) {\n        ++ip1_2;\n        if (ip1_2 == 256) {\n            ++ip1_1;\n            ip1_2 = 0;\n        }\n        if (ip1_1 == ip2_1 && ip1_2 == ip2_2)\n            break;\n        ip1.p[0] = ip1_1;\n        ip1.p[1] = ip1_2;\n        node.ip1 = ip_t(ip1, 0);\n        node.ip2 = ip_t(ip1, 255);\n        vector_index_push_back(ip1_1, ip1_2, node);\n    }\n}\n\nvoid make_t::handle_input_help(char *buf) {\n    // 去掉多余的空\n    unsigned int buf_len = strlen(buf);\n    while (buf_len > 0 && isspace(buf[buf_len - 1]))\n        --buf_len;\n    if (buf_len == 0)\n        return;\n    buf[buf_len] = '\\0';\n\n    node_t node(buf);\n\n    if (node.ip1 < next_ip) {\n        log_exit(\"ip 未排序: \" + node.ip1.to_string() + \", \" +\n                 next_ip.to_string());\n    }\n\n    next_ip = node.ip2 + 1;\n\n    if (region.find(node.region) == region.end()) {\n        region[node.region] = region_index;\n        region_index += node.region.size();\n    }\n\n    vector_index_push_back(node);\n}\n\nvoid make_t::handle_input(const std::string &file_name) {\n    FILE *src = fopen(file_name.data(), \"r\");\n    if (src == NULL)\n        log_exit(\"can't open \" + file_name);\n\n    char buf[1024];\n    while (fgets(buf, sizeof(buf), src) != NULL)\n        handle_input_help(buf);\n    fclose(src);\n}\n\nvoid make_t::handle_header() {\n    char buf[length_header];\n    memset(buf, 0, length_header);\n    write_ushort(3, buf);             // 版本号\n    write_ushort(1, buf + 2);         // 缓存策略\n    write_uint(time(NULL), buf + 4);  // 时间\n    // 索引\n    unsigned int content_left = length_header + length_vector;\n    for (auto &d : region)\n        content_left += d.first.size();\n    unsigned int content_right = content_left;\n\n    for (int i = 0; i < 256; ++i)\n        for (int j = 0; j < 256; ++j)\n            content_right += vector_index[i][j].size() * content_size;\n    content_right -= content_size;\n    write_uint(content_left, buf + 8);\n    write_uint(content_right, buf + 12);\n    write_ushort(ip_version, buf + 16);  // IP\n    write_ushort(4, buf + 18);           // 指针数\n\n    write_string(buf, length_header, db);\n}\n\nvoid make_t::handle_vector_index() {\n    unsigned index = length_header + length_vector;\n    for (auto &d : region)\n        index += d.first.size();\n    for (unsigned i = 0; i < 256; ++i)\n        for (unsigned j = 0; j < 256; ++j)\n            if (vector_index[i][j].size() == 0) {\n                write_uint(0, db);\n                write_uint(0, db);\n            } else {\n                write_uint(index, db);\n                index += content_size * vector_index[i][j].size();\n                write_uint(index, db);\n            }\n}\n\nvoid make_t::handle_region() {\n    for (auto &d : region) {\n        fseek(db, d.second, SEEK_SET);\n        write_string(d.first.data(), d.first.size(), db);\n    }\n}\n\nvoid make_t::handle_content() {\n    fseek(db, 0, SEEK_END);\n    for (unsigned i = 0; i < 256; ++i)\n        for (unsigned j = 0; j < 256; ++j)\n            for (auto d : vector_index[i][j]) {\n                write_string(d.first.data(), d.first.size(), db);\n                write_ushort(d.second.size(), db);\n                write_uint(region[d.second], db);\n            }\n}\n\nmake_t::make_t(const string &src, const string &dst, int version)\n    : region_index(length_vector + length_header) {\n    unsigned long long tv1 = get_time();\n\n    init_xdb(version);\n\n    handle_input(src);\n\n    db = fopen(dst.data(), \"w\");\n    if (db == NULL)\n        log_exit(\"can't open \" + dst);\n\n    handle_header();\n    handle_vector_index();\n    handle_region();\n    handle_content();\n\n    fclose(db);\n\n    unsigned long long tv2 = get_time();\n    printf(\"took: %.2fs\\n\", (tv2 - tv1) * 1.0 / 1000 / 1000);\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/make.h",
    "content": "#ifndef MAKE_H\n#define MAKE_H\n\n#include \"ip.h\"\n\nnamespace xdb {\n\nclass make_t {\npublic:\n    make_t(const string &src, const string &dst, int version);\n\nprivate:\n    void vector_index_push_back(int row, int col, const node_t &node);\n    void vector_index_push_back(node_t &node);\n    void handle_input_help(char buf[]);\n    void handle_input(const std::string &file_name);\n\n    void handle_header();\n    void handle_vector_index();\n    void handle_region();\n    void handle_content();\n\n    FILE *db = NULL;\n\n    std::vector<std::pair<string, string>> vector_index[256][256];\n\n    std::unordered_map<string, unsigned> region;\n\n    unsigned region_index;\n    ip_t     next_ip;\n};\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/src/search.cc",
    "content": "\n#include \"search.h\"\n\nnamespace xdb {\n\nsearch_t::search_t(const string &file, int version, int p)\n    : db(fopen(file.data(), \"r\")), header(db), policy(p) {\n    init_xdb(version);\n\n    if (db == NULL)\n        log_exit(\"can't open \" + file);\n    if (header.ip_version() != version)\n        log_exit(\"ip 版本不匹配\");\n\n    if (policy != policy_file) {\n        read_bin(length_header, vector, length_vector, db);\n        if (policy == policy_content) {\n            fseek(db, 0, SEEK_END);\n            int size = ftell(db) - length_vector - length_header;\n            content  = (char *)malloc(size);\n            read_bin(length_vector + length_header, content, size, db);\n        }\n    }\n}\n\nsearch_t::~search_t() {\n    fclose(db);\n    if (policy == policy_content)\n        free(content);\n}\n\nint search_t::get_io_count() {\n    return io_count;\n}\n\nint search_t::get_cost_time() {\n    return cost_time;\n}\n\nchar const *search_t::get_content_index_help(int index) {\n    if (policy != policy_file)\n        return vector + index;\n\n    ++io_count;\n    static char v[8];\n    read_bin(length_header + index, v, sizeof(v), db);\n    return v;\n}\n\nvoid search_t::get_content_index(const ip_t &ip, int &left, int &right) {\n    int index = ((unsigned char)ip.p[0] * 256 + (unsigned char)ip.p[1]) * 8;\n\n    const char *p = get_content_index_help(index);\n    left          = to_uint(p);\n    right         = to_uint(p + 4);\n}\n\nchar const *search_t::get_content_help(int index) {\n    if (policy == policy_content)\n        return content + index - length_header - length_vector;\n    ++io_count;\n    static char v[16 + 16 + 2 + 4];\n    read_bin(index, v, content_size, db);\n    return v;\n}\n\nstring search_t::get_region(int index, int len) {\n    if (policy == policy_content)\n        return string(content + index - length_header - length_vector, len);\n    ++io_count;\n    char *p = (char *)malloc(sizeof(char) * len);\n    read_bin(index, p, len, db);\n    string res(p, len);\n    free(p);\n    return res;\n}\n\nvoid search_t::get_content(int   index,\n                           ip_t &ip_left,\n                           ip_t &ip_right,\n                           int  &region_len,\n                           int  &region_index) {\n    const char *p = get_content_help(index);\n\n    ip_left.from_xdb(p);\n    ip_right.from_xdb(p + ip_size);\n\n    region_len   = to_ushort(p + ip_size * 2);\n    region_index = to_uint(p + ip_size * 2 + 2);\n}\n\nstring search_t::search(const ip_t &ip) {\n    io_count = 0;\n\n    int content_left, content_right;\n    get_content_index(ip, content_left, content_right);\n\n    if (content_left == 0 || content_right == 0)\n        return \"\";\n\n    ip_t ip_left, ip_right;\n    int  region_len;\n    int  region_index;\n\n    int left  = 0;\n    int right = (content_right - content_left) / content_size;\n\n    for (;;) {\n        int mid       = left + (right - left) / 2;\n        int mid_index = content_left + mid * content_size;\n        get_content(mid_index, ip_left, ip_right, region_len, region_index);\n\n        // ip ip_left ip_right\n        if (ip < ip_left)\n            right = mid - 1;\n        // ip_left ip_right ip\n        else if (ip_right < ip)\n            left = mid + 1;\n        else\n            return get_region(region_index, region_len);\n    }\n}\n\nstring search_t::search(const string &str) {\n    unsigned long long t1 = get_time();\n\n    ip_t ip;\n    if (ip.from_str(str) == false)\n        return \"invalid ipv\" + std::to_string(ip_version) + \": \" + str;\n    string region = search(ip);\n\n    unsigned long long t2 = get_time();\n    cost_time             = t2 - t1;\n    return region;\n}\n\n}  // namespace xdb\n"
  },
  {
    "path": "binding/cpp/src/search.h",
    "content": "#ifndef SEARCH_H\n#define SEARCH_H\n\n#include \"header.h\"\n#include \"ip.h\"\n\nnamespace xdb {\n\nclass search_t {\nprotected:\n    FILE *db;\n\n    header_t header;\n\n    int policy;\n\n    int io_count;\n    int cost_time;\n\n    char  vector[length_vector];\n    char *content;\n\npublic:\n    search_t(const string &file_name, int version, int policy);\n    virtual ~search_t();\n\n    int get_io_count();\n    int get_cost_time();\n\n    string search(const string &str);\n\nprotected:\n    string search(const ip_t &ip);\n\n    void get_content_index(const ip_t &ip1, int &left, int &right);\n    void get_content(int   index,\n                     ip_t &left,\n                     ip_t &right,\n                     int  &region_len,\n                     int  &region_index);\n\n    char const *get_content_index_help(int index);\n    char const *get_content_help(int index);\n    string      get_region(int index, int len);\n};\n\n}  // namespace xdb\n\n#endif\n"
  },
  {
    "path": "binding/cpp/test/bench.cc",
    "content": "\n#include \"../src/bench.h\"\n\nstd::map<int, std::string> prompt;\n\nvoid test_ipv4(int policy) {\n    std::cout << \"测试 IPv4, \" << prompt[policy];\n    xdb::bench_t(\"../../data/ip2region_v4.xdb\", xdb::ipv4, policy)\n        .test(\"../../data/ipv4_source.txt\");\n}\n\nvoid test_ipv6(int policy) {\n    std::cout << \"测试 IPv6, \" << prompt[policy];\n    xdb::bench_t(\"../../data/ip2region_v6.xdb\", xdb::ipv6, policy)\n        .test(\"../../data/ipv6_source.txt\");\n}\n\nint main() {\n    prompt[xdb::policy_file]    = \"  不缓存, \";\n    prompt[xdb::policy_vector]  = \"部分缓存, \";\n    prompt[xdb::policy_content] = \"全部缓存, \";\n\n    test_ipv4(xdb::policy_file);\n    test_ipv4(xdb::policy_vector);\n    test_ipv4(xdb::policy_content);\n\n    test_ipv6(xdb::policy_file);\n    test_ipv6(xdb::policy_vector);\n    test_ipv6(xdb::policy_content);\n    return 0;\n}\n"
  },
  {
    "path": "binding/cpp/test/edit_v4.cc",
    "content": "\n#include \"../src/edit.h\"\n\nint main() {\n    std::string file_name_old = \"../../data/ipv4_source.txt\";\n    std::string file_name_new = \"./1.txt\";\n    xdb::edit_t xdb(file_name_old, file_name_new, xdb::ipv4);\n    return 0;\n}\n"
  },
  {
    "path": "binding/cpp/test/edit_v6.cc",
    "content": "\n#include \"../src/edit.h\"\n\nint main() {\n    std::string file_name_old = \"../../data/ipv6_source.txt\";\n    std::string file_name_new = \"./1.txt\";\n    xdb::edit_t xdb(file_name_old, file_name_new, xdb::ipv6);\n    return 0;\n}\n"
  },
  {
    "path": "binding/cpp/test/header.cc",
    "content": "\n#include \"../src/header.h\"\n\nvoid test(const std::string& prompt, const std::string& file_name) {\n    std::cout << prompt << std::endl;\n\n    xdb::header_t head(fopen(file_name.data(), \"r\"));\n\n    std::cout << \"版本号: \" << head.version() << std::endl;\n    std::cout << \"缓存策略: \" << head.index_policy() << std::endl;\n\n    time_t     rawtime = head.create_at();\n    struct tm* info    = localtime(&rawtime);\n    char       buf[80];\n    strftime(buf, 80, \"%Y-%m-%d %H:%M:%S\", info);\n\n    std::cout << \"文件生成时间: \" << buf << std::endl;\n    std::cout << \"索引起始地址: \" << head.index_start() << std::endl;\n    std::cout << \"索引结束地址: \" << head.index_end() << std::endl;\n    std::cout << \"IP版本: \" << head.ip_version() << std::endl;\n    std::cout << \"指针字节数: \" << head.ptr() << std::endl;\n\n    std::cout << std::endl;\n}\n\nint main() {\n    test(\"测试 IPv4\", \"../../data/ip2region_v4.xdb\");\n    test(\"测试 IPv6\", \"../../data/ip2region_v6.xdb\");\n    return 0;\n}\n"
  },
  {
    "path": "binding/cpp/test/make.cc",
    "content": "\n#include \"../src/make.h\"\n\nvoid test(const std::string& prompt,\n          const std::string& filename_xdb,\n          const std::string& filename_src,\n          int                version\n\n) {\n    std::cout << prompt;\n    xdb::make_t(filename_xdb, filename_src, version);\n}\n\nint main() {\n    test(\"生成 ipv4 的 xdb 文件, \",\n         \"../../data/ipv4_source.txt\",\n         \"./ip2region_v4.xdb\",\n         xdb::ipv4);\n\n    test(\"生成 ipv6 的 xdb 文件, \",\n         \"../../data/ipv6_source.txt\",\n         \"./ip2region_v6.xdb\",\n         xdb::ipv6);\n\n    return 0;\n}\n"
  },
  {
    "path": "binding/cpp/test/search.cc",
    "content": "\n#include \"../src/search.h\"\n\nstd::map<int, std::string> prompt;\n\nvoid test(xdb::search_t& s, const std::string& ip, const std::string& region) {\n    if (s.search(ip) != region)\n        xdb::log_exit(\"测试失败, ip \" + ip + \", region \" + region);\n}\n\nvoid test_ipv4(int policy) {\n    std::cout << \"测试 IPv4 \" << prompt[policy];\n\n    xdb::search_t s(\"../../data/ip2region_v4.xdb\", xdb::ipv4, policy);\n    test(s, \"0.0.0.0\", \"Reserved|Reserved|Reserved|0|0\");\n    test(s, \"1.2.3.4\", \"Australia|Queensland|Brisbane|0|AU\");\n\n    std::cout << \" 成功\" << std::endl;\n}\n\nvoid test_ipv6(int policy) {\n    std::cout << \"测试 IPv6 \" << prompt[policy];\n\n    xdb::search_t s(\"../../data/ip2region_v6.xdb\", xdb::ipv6, policy);\n    test(s, \"::1\", \"\");\n    test(s, \"2001:200:124::\", \"Japan|Tokyo|Asagaya-minami|WIDE Project|JP\");\n    test(s, \"2001:200:124::\", \"Japan|Tokyo|Asagaya-minami|WIDE Project|JP\");\n    test(s, \"240e:3b7:3273:51d0:cd38:8ae1:e3c0:b708\", \"中国|广东省|深圳市|电信|CN\");\n\n    std::cout << \" 成功\" << std::endl;\n}\n\nint main() {\n    prompt[xdb::policy_file]    = \"  不缓存:\";\n    prompt[xdb::policy_vector]  = \"部分缓存:\";\n    prompt[xdb::policy_content] = \"全部缓存:\";\n\n    test_ipv4(xdb::policy_file);\n    test_ipv4(xdb::policy_vector);\n    test_ipv4(xdb::policy_content);\n\n    test_ipv6(xdb::policy_file);\n    test_ipv6(xdb::policy_vector);\n    test_ipv6(xdb::policy_content);\n    return 0;\n}\n"
  },
  {
    "path": "binding/csharp/.editorconfig",
    "content": "root = true\n\n# To learn more about .editorconfig see https://aka.ms/editorconfigdocs\n[*]\ncharset = utf-8\nindent_style = space\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nspelling_exclusion_path = .\\exclusion.dic\n\n[*.csproj]\ncharset = utf-8\ninsert_final_newline = true\n\n[*.{xml,config,csproj,nuspec,props,resx,targets,yml,tasks,json}]\nindent_size = 2\n\n[*.sh]\nend_of_line = lf\n\n[*.cs]\ncsharp_style_namespace_declarations = file_scoped:silent\n\n[*.cs]\nfile_header_template = Copyright 2025 The Ip2Region Authors. All rights reserved.\\nUse of this source code is governed by a Apache2.0-style\\nlicense that can be found in the LICENSE file.\\n@Author Alan <lzh.shap@gmail.com>\\n@Date   2023/07/25\\nUpdated by Argo Zhang <argo@live.ca> at 2025/11/21\n"
  },
  {
    "path": "binding/csharp/.gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- Backup*.rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/"
  },
  {
    "path": "binding/csharp/CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [3.0.0] - 2025-11-22\n- 支持 .NET 10.0\n- 增加 IPv6 支持\n- 修复若干 bug\n\n## [2.0.1] - 2023-07-30\n\n### Added\n- Support netstandard2.0\n\n## [2.0.0] - 2023-07-26\n\n### Removed\n- Remove nuget include xdb file\n- Searcher cache policy default parameters\n- Searcher xdb file path default parameters\n\n### Added\n- Dependent file query policies CachePolicy.VectorIndex, CachePolicy.File support thread-safe concurrent queries\n- Dramatically optimizes overall performance\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Abstractions/ISearcher.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing System.Net;\n\nnamespace IP2Region.Net.Abstractions;\n\n/// <summary>\n/// IP 转化为地理位置搜索器接口\n/// </summary>\npublic interface ISearcher : IDisposable\n{\n    /// <summary>\n    /// 搜索方法\n    /// </summary>\n    /// <param name=\"ipStr\">IP 地址字符串 如 192.168.0.1</param>\n    /// <returns></returns>\n    string? Search(string ipStr);\n\n    /// <summary>\n    /// 搜索方法\n    /// </summary>\n    string? Search(IPAddress ipAddress);\n\n    /// <summary>\n    /// 搜索方法 仅限 IPv4 使用\n    /// </summary>\n    /// <param name=\"ipAddress\">IPv4 地址字节数组小端读取 uint 数值</param>\n    [Obsolete(\"已弃用，请改用其他方法；Deprecated; please use Search(string) or Search(IPAddress) method.\")]\n    string? Search(uint ipAddress);\n\n    /// <summary>\n    /// 获得 内部 IO 访问次数\n    /// </summary>\n    int IoCount { get; }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Extensions/ServiceCollectionExtensions.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing IP2Region.Net.Abstractions;\nusing IP2Region.Net.XDB;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// IP2Region 服务扩展类\n/// </summary>\npublic static class IP2RegionExtensions\n{\n    /// <summary>\n    /// 添加 IP2RegionService 服务。\n    /// </summary>\n    /// <param name=\"services\"><see cref=\"IServiceCollection\"/> 集合</param>\n    /// <param name=\"path\">IP2Region 数据库文件的路径。</param>\n    /// <param name=\"cachePolicy\">缓存策略，默认为 <see cref=\"CachePolicy.Content\"/>。</param>\n    public static IServiceCollection AddIP2RegionService(this IServiceCollection services, string path, CachePolicy cachePolicy = CachePolicy.Content)\n    {\n        services.TryAddSingleton<ISearcher>(provider =>\n        {\n            return new Searcher(cachePolicy, path);\n        });\n#if NET8_0_OR_GREATER\n        services.TryAddKeyedSingleton(\"IP2Region.Net\", (provider, _) =>\n        {\n            return provider.GetRequiredService<ISearcher>();\n        });\n#endif\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/IP2Region.Net.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <id>IP2Region.Net</id>\n        <version>3.0.2</version>\n        <title>IP2Region.Net</title>\n        <authors>Alan Lee;Argo Zhang(argo@live.ca)</authors>\n        <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>\n        <PackageReadmeFile>README.md</PackageReadmeFile>\n        <PackageProjectUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</PackageProjectUrl>\n        <RepositoryUrl>https://github.com/lionsoul2014/ip2region/tree/master/binding/csharp</RepositoryUrl>\n        <PackageReleaseNotes>Please refer to CHANGELOG.md for details</PackageReleaseNotes>\n        <Description>.NET client library for ip2region</Description>        \n        <PackageTags>IP2Region GeoIP IPSearch</PackageTags>\n        <RepositoryType>git</RepositoryType>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>\n        <LangVersion>latest</LangVersion>\n        <UserSecretsId>c2f07fe1-a300-4de3-8200-1278ed8cb5b7</UserSecretsId>\n    </PropertyGroup>\n\t\n    <ItemGroup>\n        <None Include=\"..\\README.md\" Pack=\"true\" PackagePath=\"\\\" />\n    </ItemGroup>\n\t\n    <ItemGroup Condition=\"'$(TargetFramework)' == 'netstandard2.0'\">\n      <PackageReference Include=\"System.Buffers\" Version=\"4.5.1\" />\n      <PackageReference Include=\"System.Memory\" Version=\"4.5.5\" />\n    </ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'netstandard2.0'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.*\" />\n\t</ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'netstandard2.1'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.0\" />\n\t</ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'net6.0'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"8.0.*\" />\n\t</ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'net7.0'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"8.0.*\" />\n\t</ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'net8.0'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.*\" />\n\t</ItemGroup>\n\n\t<ItemGroup Condition=\"'$(TargetFramework)' == 'net9.0'\">\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.*\" />\n\t</ItemGroup>\n\n  <ItemGroup Condition=\"'$(TargetFramework)' == 'net10.0'\">\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection.Abstractions\" Version=\"10.0.*\" />\n  </ItemGroup>\n\n  <ItemGroup>\n      <None Include=\"..\\CHANGELOG.md\">\n        <Link>CHANGELOG.md</Link>\n      </None>\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Internal/CacheStrategyFactory.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing IP2Region.Net.XDB;\n\nnamespace IP2Region.Net.Internal;\n\nstatic class CacheStrategyFactory\n{\n    public static ICacheStrategy CreateCacheStrategy(CachePolicy cachePolicy, string xdbPath) => cachePolicy switch\n    {\n        CachePolicy.Content => new ContentCacheStrategy(xdbPath),\n        CachePolicy.VectorIndex => new VectorIndexCacheStrategy(xdbPath),\n        _ => new FileCacheStrategy(xdbPath),\n    };\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Internal/ContentCacheStrategy.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Wong <vcd.hai@outlook.com> at 2025/12/31\n\nnamespace IP2Region.Net.Internal;\n\nclass ContentCacheStrategy(string xdbPath) : ICacheStrategy\n{\n    // TODO: these constants can be moved to the interface as defaults when using .NET 10\n    private const int HeaderInfoLength = 256;\n    private const int VectorIndexSize = 8;\n\n    private readonly ReadOnlyMemory<byte> _cacheData = File.ReadAllBytes(xdbPath);\n\n    public int IoCount => 0;\n\n    public void ResetIoCount()\n    {\n        // Do nothing\n    }\n\n    public ReadOnlyMemory<byte> GetVectorIndex(int offset)\n        => _cacheData.Slice(HeaderInfoLength + offset, VectorIndexSize);\n\n    public ReadOnlyMemory<byte> GetData(long offset, int length) => _cacheData.Slice((int)offset, length);\n\n    public void Dispose()\n    {\n        // Do nothing\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Internal/FileCacheStrategy.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing System.Buffers;\n\nnamespace IP2Region.Net.Internal;\n\nclass FileCacheStrategy(string xdbPath) : ICacheStrategy\n{\n    protected const int HeaderInfoLength = 256;\n    protected const int VectorIndexSize = 8;\n\n    protected const int BufferSize = 64 * 1024;\n\n    protected FileStream XdbFileStream = new(xdbPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.RandomAccess);\n\n    public int IoCount { get; set; }\n\n    public void ResetIoCount()\n    {\n        IoCount = 0;\n    }\n\n    public virtual ReadOnlyMemory<byte> GetVectorIndex(int offset) => GetData(HeaderInfoLength + offset, VectorIndexSize);\n\n    public virtual ReadOnlyMemory<byte> GetData(long offset, int length)\n    {\n        var buffer = ArrayPool<byte>.Shared.Rent(length);\n        try\n        {\n            int totalBytesRead = 0;\n            XdbFileStream.Seek(offset, SeekOrigin.Begin);\n\n            int bytesRead;\n            while (totalBytesRead < length)\n            {\n                bytesRead = XdbFileStream.Read(buffer, totalBytesRead, length - totalBytesRead);\n                if (bytesRead == 0)\n                {\n                    break;\n                }\n\n                totalBytesRead += bytesRead;\n                IoCount++;\n            }\n\n            var ret = new byte[totalBytesRead];\n            if (totalBytesRead > 0)\n            {\n                Array.Copy(buffer, 0, ret, 0, totalBytesRead);\n            }\n            return ret;\n        }\n        finally\n        {\n            ArrayPool<byte>.Shared.Return(buffer);\n        }\n    }\n\n    /// <summary>\n    /// 释放文件句柄\n    /// </summary>\n    /// <param name=\"disposing\"></param>\n    protected virtual void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            XdbFileStream.Close();\n            XdbFileStream.Dispose();\n        }\n    }\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Internal/ICacheStrategy.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nnamespace IP2Region.Net.Internal;\n\ninternal interface ICacheStrategy : IDisposable\n{\n    int IoCount { get; }\n\n    void ResetIoCount();\n\n    ReadOnlyMemory<byte> GetVectorIndex(int offset);\n\n    ReadOnlyMemory<byte> GetData(long offset, int length);\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/Internal/VectorIndexCacheStrategy.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nnamespace IP2Region.Net.Internal;\n\nclass VectorIndexCacheStrategy : FileCacheStrategy\n{\n    private const int VectorIndexRows = 256;\n    private const int VectorIndexCols = 256;\n\n    private readonly ReadOnlyMemory<byte> _vectorCache;\n\n    public VectorIndexCacheStrategy(string xdbPath) : base(xdbPath)\n    {\n        _vectorCache = GetData(HeaderInfoLength, VectorIndexRows * VectorIndexCols * VectorIndexSize);\n    }\n\n    public override ReadOnlyMemory<byte> GetVectorIndex(int offset) => _vectorCache.Slice(offset, VectorIndexSize);\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/XDB/CachePolicy.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nnamespace IP2Region.Net.XDB;\n\n/// <summary>\n/// 缓存策略枚举\n/// </summary>\npublic enum CachePolicy\n{\n    /// <summary>\n    /// no cache \n    /// </summary>\n    File,\n\n    /// <summary>\n    /// cache vector index , reduce the number of IO operations\n    /// </summary>\n    VectorIndex,\n\n    /// <summary>\n    /// default cache policy , cache whole xdb file\n    /// </summary>\n    Content\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/XDB/Searcher.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing IP2Region.Net.Abstractions;\nusing IP2Region.Net.Internal;\nusing System.Buffers.Binary;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Net;\nusing System.Text;\n\nnamespace IP2Region.Net.XDB;\n\n/// <summary>\n/// <see cref=\"ISearcher\"/> 实现类\n/// </summary>\n/// <remarks>\n/// <inheritdoc/>\n/// </remarks>\npublic class Searcher(CachePolicy cachePolicy, string xdbPath) : ISearcher\n{\n    private readonly ICacheStrategy _cacheStrategy = CacheStrategyFactory.CreateCacheStrategy(cachePolicy, xdbPath);\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    public int IoCount => _cacheStrategy.IoCount;\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    public string? Search(string ipStr)\n    {\n        var ipAddress = IPAddress.Parse(ipStr);\n        return SearchCore(ipAddress.GetAddressBytes());\n    }\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    public string? Search(IPAddress ipAddress) => SearchCore(ipAddress.GetAddressBytes());\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    [Obsolete(\"已弃用，请改用其他方法；Deprecated; please use Search(string) or Search(IPAddress) method.\")]\n    [ExcludeFromCodeCoverage]\n    public string? Search(uint ipAddress)\n    {\n        var bytes = BitConverter.GetBytes(ipAddress);\n        Array.Reverse(bytes);\n        return SearchCore(bytes);\n    }\n\n    string? SearchCore(byte[] ipBytes)\n    {\n        // 重置 IO 计数器\n        _cacheStrategy.ResetIoCount();\n\n        // 每个 vector 索引项的字节数\n        var vectorIndexSize = 8;\n\n        // vector 索引的列数\n        var vectorIndexCols = 256;\n\n        // 计算得到 vector 索引项的开始地址。\n        var il0 = ipBytes[0];\n        var il1 = ipBytes[1];\n        var idx = il0 * vectorIndexCols * vectorIndexSize + il1 * vectorIndexSize;\n\n        var vector = _cacheStrategy.GetVectorIndex(idx);\n        var sPtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span);\n        var ePtr = BinaryPrimitives.ReadUInt32LittleEndian(vector.Span.Slice(4));\n\n        // @Note: ptr validate, zero ptr means source data missing\n        // so we could just stop here and return an empty string.\n        if (sPtr == 0 || ePtr == 0)\n        {\n            return \"\";\n        }\n\n\n        var length = ipBytes.Length;\n        var indexSize = length * 2 + 6;\n        var l = 0;\n        var h = (ePtr - sPtr) / indexSize;\n        var dataLen = 0;\n        long dataPtr = 0;\n\n        while (l <= h)\n        {\n            int m = (int)(l + h) >> 1;\n\n            var p = sPtr + m * indexSize;\n            var buff = _cacheStrategy.GetData(p, indexSize);\n\n            var s = buff.Span.Slice(0, length);\n            var e = buff.Span.Slice(length, length);\n            if (ByteCompare(ipBytes, s) < 0)\n            {\n                h = m - 1;\n            }\n            else if (ByteCompare(ipBytes, e) > 0)\n            {\n                l = m + 1;\n            }\n            else\n            {\n                dataLen = BinaryPrimitives.ReadUInt16LittleEndian(buff.Span.Slice(length * 2, 2));\n                dataPtr = BinaryPrimitives.ReadUInt32LittleEndian(buff.Span.Slice(length * 2 + 2, 4));\n                break;\n            }\n        }\n\n        var regionBuff = _cacheStrategy.GetData(dataPtr, dataLen);\n        return Encoding.UTF8.GetString(regionBuff.Span.ToArray());\n    }\n\n    static int ByteCompare(byte[] ip1, ReadOnlySpan<byte> ip2) => ip1.Length == 4 ? IPv4Compare(ip1, ip2) : IPv6Compare(ip1, ip2);\n\n    static int IPv4Compare(byte[] ip1, ReadOnlySpan<byte> ip2)\n    {\n        var ret = 0;\n        for (int i = 0; i < ip1.Length; i++)\n        {\n            var ip2Index = ip1.Length - 1 - i;\n            if (ip1[i] < ip2[ip2Index])\n            {\n                return -1;\n            }\n            else if (ip1[i] > ip2[ip2Index])\n            {\n                return 1;\n            }\n        }\n        return ret;\n    }\n\n    static int IPv6Compare(byte[] ip1, ReadOnlySpan<byte> ip2)\n    {\n        var ret = 0;\n        for (int i = 0; i < ip1.Length; i++)\n        {\n            if (ip1[i] < ip2[i])\n            {\n                return -1;\n            }\n            else if (ip1[i] > ip2[i])\n            {\n                return 1;\n            }\n        }\n        return ret;\n    }\n\n    /// <summary>\n    /// <inheritdoc/>\n    /// </summary>\n    public void Dispose()\n    {\n        _cacheStrategy.Dispose();\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/XDB/Util.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing System.Buffers;\nusing System.Buffers.Binary;\nusing System.Net;\nusing System.Runtime.InteropServices;\n\nnamespace IP2Region.Net.XDB;\n\n/// <summary>\n/// 工具类\n/// </summary>\npublic static class Util\n{\n    public static uint IpAddressToUInt32(string ipAddress)\n    {\n        var address = IPAddress.Parse(ipAddress);\n        return IpAddressToUInt32(address);\n    }\n\n    public static uint IpAddressToUInt32(IPAddress ipAddress)\n    {\n        byte[] bytes = ipAddress.GetAddressBytes();\n        Array.Reverse(bytes);\n        return MemoryMarshal.Read<uint>(bytes);\n    }\n\n    public static uint GetMidIp(uint x, uint y)\n        => (x & y) + ((x ^ y) >> 1);\n\n    public static async Task<XdbVersion> GetVersionAsync(string dbPath, CancellationToken token = default)\n    {\n        if (string.IsNullOrEmpty(dbPath))\n        {\n            throw new ArgumentNullException(nameof(dbPath));\n        }\n\n        if (!File.Exists(dbPath))\n        {\n            throw new FileNotFoundException(\"xdb file not found.\", dbPath);\n        }\n\n        using var reader = File.OpenRead(dbPath);\n        return await GetVersionAsync(reader, token);\n    }\n\n    internal static async Task<XdbVersion> GetVersionAsync(FileStream reader, CancellationToken token = default)\n    {\n        XdbVersion ret = default;\n        var buffer = ArrayPool<byte>.Shared.Rent(256);\n\n        try\n        {\n            var length = await reader.ReadAsync(buffer, 0, 256, token);\n            if (length == 256)\n            {\n                ret = Parse(buffer);\n            }\n        }\n        finally\n        {\n            ArrayPool<byte>.Shared.Return(buffer);\n        }\n\n        return ret;\n    }\n\n    private static XdbVersion Parse(ReadOnlySpan<byte> buffer)\n    {\n        var ret = new XdbVersion\n        {\n            Ver = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2)),\n            CachePolice = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2, 2)),\n            StartIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8, 4)),\n            EndIndex = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12, 4)),\n            IPVer = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(16, 2)),\n            BytesCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(18, 2))\n        };\n\n        var createdAt = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4, 4));\n        var dtm = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.FromHours(0));\n        ret.CreatedTime = dtm.AddSeconds(createdAt);\n\n        return ret;\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net/XDB/XdbVersion.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nnamespace IP2Region.Net.XDB;\n\n/// <summary>\n/// XdbVersion 结构体\n/// </summary>\npublic struct XdbVersion\n{\n    /// <summary>\n    /// 获得/设置 版本号\n    /// </summary>\n    public ushort Ver { get; set; }\n\n    /// <summary>\n    /// 获得/设置 缓存策略\n    /// </summary>\n    public ushort CachePolice { get; set; }\n\n    /// <summary>\n    /// 获得/设置 文件生成时间\n    /// </summary>\n    public DateTimeOffset CreatedTime { get; set; }\n\n    /// <summary>\n    /// 获得/设置 索引起始地址\n    /// </summary>\n    public uint StartIndex { get; set; }\n\n    /// <summary>\n    /// 获得/设置 索引结束地址\n    /// </summary>\n    public uint EndIndex { get; set; }\n\n    /// <summary>\n    /// 获得/设置 IP版本\n    /// </summary>\n    public ushort IPVer { get; set; }\n\n    /// <summary>\n    /// 获得/设置 指针字节数\n    /// </summary>\n    public ushort BytesCount { get; set; }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.BenchMark/Benmarks.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing BenchmarkDotNet.Attributes;\nusing IP2Region.Net.XDB;\n\nnamespace IP2Region.Net.BenchMark;\n\n[MemoryDiagnoser]\npublic class Benchmarks\n{\n    private static readonly string XdbPathV4 = Path.Combine(AppContext.BaseDirectory, \"IP2Region\", \"ip2region_v4.xdb\");\n    private static readonly string XdbPathV6 = Path.Combine(AppContext.BaseDirectory, \"IP2Region\", \"ip2region_v6.xdb\");\n    private static readonly Searcher _contentV4Searcher = new(CachePolicy.Content, XdbPathV4);\n    private static readonly Searcher _vectorV4Searcher = new(CachePolicy.VectorIndex, XdbPathV4);\n    private static readonly Searcher _fileV4Searcher = new(CachePolicy.File, XdbPathV4);\n    private static readonly Searcher _contentV6Searcher = new(CachePolicy.Content, XdbPathV6);\n    private static readonly Searcher _vectorV6Searcher = new(CachePolicy.VectorIndex, XdbPathV6);\n    private static readonly Searcher _fileV6Searcher = new(CachePolicy.File, XdbPathV6);\n\n    private readonly string _testIPv4Address = \"114.114.114.114\";\n    private readonly string _testIPv6Address = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";\n\n    public Benchmarks()\n    {\n        _contentV4Searcher.Search(_testIPv4Address);\n        _vectorV4Searcher.Search(_testIPv4Address);\n        _fileV4Searcher.Search(_testIPv4Address);\n\n        _contentV6Searcher.Search(_testIPv6Address);\n        _vectorV6Searcher.Search(_testIPv6Address);\n        _fileV6Searcher.Search(_testIPv6Address);\n    }\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv4\")]\n    public void ContentIPv4() => _contentV4Searcher.Search(_testIPv4Address);\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv4\")]\n    public void VectorIPv4() => _vectorV4Searcher.Search(_testIPv4Address);\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv4\")]\n    public void FileIPv4() => _fileV4Searcher.Search(_testIPv4Address);\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv6\")]\n    public void ContentIPv6() => _contentV6Searcher.Search(_testIPv6Address);\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv6\")]\n    public void VectorIPv6() => _vectorV6Searcher.Search(_testIPv6Address);\n\n    [Benchmark]\n    [BenchmarkCategory(\"IPv6\")]\n    public void FileIPv6() => _fileV6Searcher.Search(_testIPv6Address);\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <OutputType>Exe</OutputType>\n        <TargetFramework>net10.0</TargetFramework>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n    </PropertyGroup>\n\n    <ItemGroup>\n      <PackageReference Include=\"BenchmarkDotNet\" Version=\"0.15.6\" />\n      <PackageReference Include=\"BenchmarkDotNet.Annotations\" Version=\"0.15.6\" />\n    </ItemGroup>\n\n    <ItemGroup>\n      <ProjectReference Include=\"..\\IP2Region.Net\\IP2Region.Net.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n      <Content Include=\"..\\..\\..\\data\\ip2region_v4.xdb\">\n        <Link>IP2Region/ip2region_v4.xdb</Link>\n        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      </Content>\n\t  <Content Include=\"..\\..\\..\\data\\ip2region_v6.xdb\">\n\t\t<Link>IP2Region/ip2region_v6.xdb</Link>\n\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t  </Content>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.BenchMark/Program.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing BenchmarkDotNet.Running;\nusing IP2Region.Net.BenchMark;\n\nBenchmarkRunner.Run<Benchmarks>();\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.Test/IP2Region.Net.Test.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<ImplicitUsings>enable</ImplicitUsings>\n\t\t<Nullable>enable</Nullable>\n\n\t\t<IsPackable>false</IsPackable>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n\t\t<PackageReference Include=\"xunit\" Version=\"2.*\" />\n\t\t<PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.*\">\n\t\t\t<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t\t<PrivateAssets>all</PrivateAssets>\n\t\t</PackageReference>\n\t\t<PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\">\n\t\t\t<PrivateAssets>all</PrivateAssets>\n\t\t\t<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"10.0.0\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\IP2Region.Net\\IP2Region.Net.csproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<Content Include=\"..\\..\\..\\data\\ipv4_source.txt\">\n\t\t\t<Link>TestData/ipv4_source.txt</Link>\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"..\\..\\..\\data\\ip2region_v4.xdb\">\n\t\t\t<Link>TestData/ip2region_v4.xdb</Link>\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"..\\..\\..\\data\\ipv6_source.txt\">\n\t\t\t<Link>TestData/ipv6_source.txt</Link>\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t\t<Content Include=\"..\\..\\..\\data\\ip2region_v6.xdb\">\n\t\t\t<Link>TestData/ip2region_v6.xdb</Link>\n\t\t\t<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</Content>\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.Test/SearcherTest.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing IP2Region.Net.Abstractions;\nusing IP2Region.Net.XDB;\nusing Microsoft.Extensions.DependencyInjection;\nusing System.Net;\nusing Xunit;\n\nnamespace IP2Region.Net.Test;\n\npublic class SearcherTest\n{\n    private readonly string _xdbPathV4 = Path.Combine(AppContext.BaseDirectory, \"TestData\", \"ip2region_v4.xdb\");\n    private readonly string _xdbPathV6 = Path.Combine(AppContext.BaseDirectory, \"TestData\", \"ip2region_v6.xdb\");\n\n    [Theory]\n    [InlineData(\"58.251.27.201\", \"中国|广东省|深圳市|联通|CN\", \"v4\")]\n    [InlineData(\"114.114.114.114\", \"中国|江苏省|南京市|0|CN\", \"v4\")]\n    [InlineData(\"119.29.29.29\", \"中国|北京|北京市|腾讯|CN\", \"v4\")]\n    [InlineData(\"223.5.5.5\", \"中国|浙江省|杭州市|阿里|CN\", \"v4\")]\n    [InlineData(\"180.76.76.76\", \"中国|北京|北京市|百度|CN\", \"v4\")]\n    [InlineData(\"8.8.8.8\", \"United States|California|0|Google LLC|US\", \"v4\")]\n    [InlineData(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\", \"中国|广东省|深圳市|电信|CN\", \"v6\")]\n    public void TestSearchCacheContent(string ip, string expected, string version)\n    {\n        var _xdbPath = version == \"v4\" ? _xdbPathV4 : _xdbPathV6;\n        var contentSearcher = new Searcher(CachePolicy.Content, _xdbPath);\n        var region = contentSearcher.Search(ip);\n        Assert.Equal(expected, region);\n    }\n\n    [Theory]\n    [InlineData(\"58.251.27.201\", \"中国|广东省|深圳市|联通|CN\", \"v4\")]\n    [InlineData(\"114.114.114.114\", \"中国|江苏省|南京市|0|CN\", \"v4\")]\n    [InlineData(\"119.29.29.29\", \"中国|北京|北京市|腾讯|CN\", \"v4\")]\n    [InlineData(\"223.5.5.5\", \"中国|浙江省|杭州市|阿里|CN\", \"v4\")]\n    [InlineData(\"180.76.76.76\", \"中国|北京|北京市|百度|CN\", \"v4\")]\n    [InlineData(\"8.8.8.8\", \"United States|California|0|Google LLC|US\", \"v4\")]\n    [InlineData(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\", \"中国|广东省|深圳市|电信|CN\", \"v6\")]\n    public void TestSearchCacheVector(string ip, string expected, string version)\n    {\n        var _xdbPath = version == \"v4\" ? _xdbPathV4 : _xdbPathV6;\n        var vectorSearcher = new Searcher(CachePolicy.VectorIndex, _xdbPath);\n        var region = vectorSearcher.Search(ip);\n        Assert.Equal(expected, region);\n    }\n\n    [Theory]\n    [InlineData(\"58.251.0.0\", \"中国|广东省|深圳市|联通|CN\", \"v4\")]\n    [InlineData(\"58.251.255.255\", \"中国|广东省|深圳市|联通|CN\", \"v4\")]\n    [InlineData(\"58.251.27.201\", \"中国|广东省|深圳市|联通|CN\", \"v4\")]\n    [InlineData(\"114.114.114.114\", \"中国|江苏省|南京市|0|CN\", \"v4\")]\n    [InlineData(\"119.29.29.29\", \"中国|北京|北京市|腾讯|CN\", \"v4\")]\n    [InlineData(\"223.5.5.5\", \"中国|浙江省|杭州市|阿里|CN\", \"v4\")]\n    [InlineData(\"180.76.76.76\", \"中国|北京|北京市|百度|CN\", \"v4\")]\n    [InlineData(\"8.8.8.8\", \"United States|California|0|Google LLC|US\", \"v4\")]\n    [InlineData(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\", \"中国|广东省|深圳市|电信|CN\", \"v6\")]\n    [InlineData(\"240e:044d:2d00:0000:0000:0000:0000:0000\", \"中国|云南|楚雄|电信|CN\", \"v6\")]\n    public void TestSearchCacheFile(string ip, string expected, string version)\n    {\n        var _xdbPath = version == \"v4\" ? _xdbPathV4 : _xdbPathV6;\n        var fileSearcher = new Searcher(CachePolicy.File, _xdbPath);\n        var region = fileSearcher.Search(ip);\n        Assert.Equal(expected, region);\n    }\n\n    [Fact]\n    public void IoCount_File_Ok()\n    {\n        var searcher = new Searcher(CachePolicy.File, _xdbPathV4);\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(3, searcher.IoCount);\n\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(3, searcher.IoCount);\n\n        searcher.Dispose();\n    }\n\n    [Fact]\n    public void IoCount_Vector_Ok()\n    {\n        var searcher = new Searcher(CachePolicy.VectorIndex, _xdbPathV4);\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(2, searcher.IoCount);\n\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(2, searcher.IoCount);\n\n        searcher.Dispose();\n    }\n\n    [Fact]\n    public void IoCount_Content_Ok()\n    {\n        var searcher = new Searcher(CachePolicy.Content, _xdbPathV4);\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(0, searcher.IoCount);\n\n        searcher.Search(\"58.251.27.201\");\n        Assert.Equal(0, searcher.IoCount);\n\n        searcher.Dispose();\n    }\n\n    [Theory]\n    [InlineData(\"58.251.255.255\", \"中国|广东省|深圳市|联通|CN\")]\n    public void Search_Ip_Ok(string ipStr, string expected)\n    {\n        var fileSearcher = new Searcher(CachePolicy.File, _xdbPathV4);\n        var ipAddress = IPAddress.Parse(ipStr);\n        var region = fileSearcher.Search(ipAddress);\n        Assert.Equal(expected, region);\n    }\n\n    [Theory]\n    [InlineData(\"58.251.255.255\", \"中国|广东省|深圳市|联通|CN\")]\n    public void AddIP2RegionService_Ok(string ipStr, string expected)\n    {\n        var services = new ServiceCollection();\n        services.AddIP2RegionService(_xdbPathV4, CachePolicy.File);\n\n        var provider = services.BuildServiceProvider();\n        var searcher = provider.GetRequiredService<ISearcher>();\n        var region = searcher.Search(ipStr);\n        Assert.Equal(expected, region);\n\n        searcher = provider.GetRequiredKeyedService<ISearcher>(\"IP2Region.Net\");\n        region = searcher.Search(ipStr);\n        Assert.Equal(expected, region);\n    }\n\n    [Theory]\n    [InlineData(CachePolicy.Content, \"v4\")]\n    [InlineData(CachePolicy.VectorIndex, \"v4\")]\n    [InlineData(CachePolicy.File, \"v4\")]\n    [InlineData(CachePolicy.Content, \"v6\")]\n    [InlineData(CachePolicy.VectorIndex, \"v6\")]\n    [InlineData(CachePolicy.File, \"v6\")]\n    public void TestBenchSearch(CachePolicy cachePolicy, string version)\n    {\n        var _xdbPath = version == \"v4\" ? _xdbPathV4 : _xdbPathV6;\n        var searcher = new Searcher(cachePolicy, _xdbPath);\n        var srcPath = Path.Combine(AppContext.BaseDirectory, \"TestData\", $\"ip{version}_source.txt\");\n\n        foreach (var line in File.ReadLines(srcPath))\n        {\n            var ps = line.Trim().Split(\"|\", 3);\n            var sip = ps[0];\n            var eip = ps[1];\n\n            var s1 = searcher.Search(sip);\n            var s2 = searcher.Search(eip);\n            Assert.Equal(s1, ps[2]);\n            Assert.Equal(s2, ps[2]);\n        }\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.Test/UtilTest.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing Xunit;\n\nnamespace IP2Region.Net.Test;\n\npublic class UtilTest\n{\n    [Fact]\n    public void IpAddressToUInt32_Ok()\n    {\n        var uintIp = XDB.Util.IpAddressToUInt32(\"114.114.114.114\");\n        Assert.Equal((uint)1920103026, uintIp);\n    }\n\n    [Fact]\n    public void GetMidIp_Ok()\n    {\n        var uintIp = XDB.Util.GetMidIp(1, 10);\n        Assert.Equal((uint)5, uintIp);\n    }\n}"
  },
  {
    "path": "binding/csharp/IP2Region.Net.Test/XdbTest.cs",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Alan <lzh.shap@gmail.com>\n// @Date   2023/07/25\n// Updated by Argo Zhang <argo@live.ca> at 2025/11/21\n\nusing Xunit;\n\nnamespace IP2Region.Net.Test;\n\npublic class XdbTest\n{\n    [Fact]\n    public async Task VersionIPV4_Ok()\n    {\n        var db = Path.Combine(AppContext.BaseDirectory, \"TestData\", $\"ip2region_v4.xdb\");\n\n        var version = await XDB.Util.GetVersionAsync(db);\n        Assert.Equal(3, version.Ver);\n        Assert.Equal(1, version.CachePolice);\n        //Assert.Equal(\"2025-09-06 02:24:16\", version.CreatedTime.ToString(\"yyyy-MM-dd HH:mm:ss\"));\n        //Assert.Equal((uint)955933, version.StartIndex);\n        //Assert.Equal((uint)11042415, version.EndIndex);\n        Assert.Equal(4, version.IPVer);\n        Assert.Equal(4, version.BytesCount);\n    }\n\n    [Fact]\n    public async Task VersionIPV6_Ok()\n    {\n        var db = Path.Combine(AppContext.BaseDirectory, \"TestData\", $\"ip2region_v6.xdb\");\n        var version = await XDB.Util.GetVersionAsync(db);\n        Assert.Equal(3, version.Ver);\n        Assert.Equal(1, version.CachePolice);\n        //Assert.Equal(\"2025-10-17 04:41:04\", version.CreatedTime.ToString(\"yyyy-MM-dd HH:mm:ss\"));\n        //Assert.Equal((uint)3094259, version.StartIndex);\n        //Assert.Equal((uint)36258303, version.EndIndex);\n        Assert.Equal(6, version.IPVer);\n        Assert.Equal(4, version.BytesCount);\n    }\n\n    [Fact]\n    public async Task GetVersionAsync_Error()\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(async () => await XDB.Util.GetVersionAsync(null!));\n        await Assert.ThrowsAsync<FileNotFoundException>(async () => await XDB.Util.GetVersionAsync(Path.Combine(AppContext.BaseDirectory, \"test.xdb\")));\n    }\n}\n"
  },
  {
    "path": "binding/csharp/IP2Region.Net.slnx",
    "content": "<Solution>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\"README.md\" />\n  </Folder>\n  <Project Path=\"IP2Region.Net.BenchMark/IP2Region.Net.BenchMark.csproj\" />\n  <Project Path=\"IP2Region.Net.Test/IP2Region.Net.Test.csproj\" />\n  <Project Path=\"IP2Region.Net/IP2Region.Net.csproj\" />\n</Solution>\n"
  },
  {
    "path": "binding/csharp/README.md",
    "content": "# IP2Region.Net\n\n.NET client library for IP2Region\n\n## Installation\n\nInstall the package with [NuGet](https://www.nuget.org/packages/IP2Region.Net)\n\n```bash\nInstall-Package IP2Region.Net\n```\n\n## Usage\n\n```csharp\nusing IP2Region.Net.Abstractions;\nusing IP2Region.Net.XDB;\n\nISearcher searcher = new Searcher(CachePolicy , \"your xdb file path\");\n```\n### Cache Policy Description\n| Cache Policy            | Description                                                                                                | Thread Safe |\n|-------------------------|------------------------------------------------------------------------------------------------------------|-------------|\n| CachePolicy.Content     | Cache the entire `xdb` data.                                                                               | Yes         |\n| CachePolicy.VectorIndex | Cache `vecotorIndex` to speed up queries and reduce system io pressure by reducing one fixed IO operation. | Yes         |\n| CachePolicy.File        | Completely file-based queries                                                                              | Yes         |\n\n### XDB File Description\nGenerate using [maker](https://github.com/lionsoul2014/ip2region/tree/master/maker/csharp), or [download](https://github.com/lionsoul2014/ip2region/blob/master/data/ip2region.xdb) pre-generated xdb files\n\n## ASP.NET Core Usage\n\n```csharp\nservices.AddIP2RegionService(\"your xdb file path\", cachePolicy: CachePolicy.Content);\n```\n\nNET6/7\n```csharp\nprovider.GetRequiredService<ISearcher>()\n```\n\nNET8+ support keyed service\n```csharp\nprovider.GetRequiredKeyedService<ISearcher>(\"IP2Region.Net\");\n```\n\n## TargetFrameworks\nnetstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0\n\n## Performance\n// * Summary *\n\nBenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171)\n13th Gen Intel Core i7-13700 2.10GHz, 1 CPU, 24 logical and 16 physical cores\n.NET SDK 10.0.100\n  [Host]     : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3\n  DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3\n\n\n| Method      | Mean         | Error      | StdDev     | Gen0   | Allocated |\n|------------ |-------------:|-----------:|-----------:|-------:|----------:|\n| ContentIPv4 |     53.70 ns |   0.296 ns |   0.277 ns | 0.0086 |     136 B |\n| VectorIPv4  |  4,446.04 ns |  18.673 ns |  15.593 ns | 0.0076 |     232 B |\n| FileIPv4    |  6,712.40 ns |  15.718 ns |  13.934 ns | 0.0153 |     264 B |\n| ContentIPv6 |    145.53 ns |   0.331 ns |   0.277 ns | 0.0126 |     200 B |\n| VectorIPv6  |  7,058.39 ns | 125.505 ns | 117.398 ns | 0.0381 |     712 B |\n| FileIPv6    | 10,657.97 ns |  53.907 ns |  50.425 ns | 0.0458 |     744 B |\n\n// * Hints *\nOutliers\n  Benchmarks.VectorIPv4: Default  -> 2 outliers were removed (4.55 us, 4.58 us)\n  Benchmarks.FileIPv4: Default    -> 1 outlier  was  removed (6.79 us)\n  Benchmarks.ContentIPv6: Default -> 2 outliers were removed (148.08 ns, 152.27 ns)\n\n// * Legends *\n  Mean      : Arithmetic mean of all measurements\n  Error     : Half of 99.9% confidence interval\n  StdDev    : Standard deviation of all measurements\n  Gen0      : GC Generation 0 collects per 1000 operations\n  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)\n  1 ns      : 1 Nanosecond (0.000000001 sec)\n\n// * Diagnostic Output - MemoryDiagnoser *\n\n\n// ***** BenchmarkRunner: End *****\nRun time: 00:02:06 (126.09 sec), executed benchmarks: 6\n\nGlobal total time: 00:02:13 (133.47 sec), executed benchmarks: 6\n// * Artifacts cleanup *\nArtifacts cleanup is finished\n\n## Contributing\nPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\nPlease make sure to update tests as appropriate.\n\n## License\n[Apache License 2.0](https://github.com/lionsoul2014/ip2region/blob/master/LICENSE.md)\n"
  },
  {
    "path": "binding/erlang/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region erlang query client\n\n### Introduction\n\nThis binding implements the xdb query client in Erlang, based on the Erlang OTP Application. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing.\n\n### Application Configuration\n\nThe configurable parameters for this application are in `ip2region.app.src`, as follows:\n\n```erlang\n  {env,[\n    {poolargs, [\n        {size, 1},  %% Default number of worker processes\n        {max_overflow, 5}  %% Maximum number of worker processes\n    ]}\n  ]}\n```\n\n### Compile\n\n```\n$ rebar3 compile\n```\n\n### Run\n\nPlace the xdb file in the `priv` directory, then start the Erlang node:\n\n```\n$ rebar3 shell\n```\n\nCall the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers, as follows:\n\n```\n1> xdb:search(\"1.0.8.0\").\n[20013,22269,124,48,124,24191,19996,30465,124,24191,24030,\n 24066,124,30005,20449]\n2>\n3> io:format(\"~ts~n\", [xdb:search(\"1.0.8.0\")]).\n中国|0|广东省|广州市|电信\nio:format(\"~ts~n\", [xdb:search(<<\"1.0.8.0\">>)]).\n中国|0|广东省|广州市|电信\n4> io:format(\"~ts~n\", [xdb:search({1,0,8,0})]).\n中国|0|广东省|广州市|电信\n6> io:format(\"~ts~n\", [xdb:search(16779264)]).\n中国|0|广东省|广州市|电信\n```\n\n### Usage\n\n* Add the dependency in `rebar.config`\n\n```\n{deps, [\n  ip2region\n]}.\n```\n\n* Start the ip2region Application\n\n```\n......\n\napplication:ensure_started(ip2region),\n\n......\n```\n\n* Call the `xdb:search/1` interface to query IP information\n\n```\n......\n\nip2region:search(\"1.0.8.0\"),\n\n......\n```\n\n### Unit Test\n\n```\n$ rebar3 eunit\n===> Verifying dependencies...\n===> Analyzing applications...\n===> Compiling ip2region\n===> Performing EUnit tests...\n=INFO REPORT==== 17-Jan-2023::11:52:59.920155 ===\nXdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb\n\n....\nFinished in 0.074 seconds\n4 tests, 0 failures\n```\n\n### Benchmark\n\n```\n$ cd benchmarks/\n$ sh xdb-benchmark.sh\n===> Verifying dependencies...\n===> Analyzing applications...\n===> Compiling ip2region\nErlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]\n\nEshell V12.3.2.2  (abort with ^G)\n1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 ===\nXdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb\n\n===> Booted ip2region\n===> Evaluating: \"xdb_benchmark:main(\\\"../../data/ip.merge.txt\\\"), init:stop().\"\nCPU info:\nmodel name      : AMD EPYC 7K62 48-Core Processor\ncache size      : 512 KB\ncpu MHz         : 2595.124\nbogomips        : 5190.24\ncores/threads   : 2\n\nErlang info:\nsystem_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]\nload test data use 4.835593s\n\nstart run benchmark tests\n\nsearch from file:\nip count:683844,\ntotal time: 28.201699s,\nsearch 24248.326315375536 times per second,\nuse 41.23995969841075 micro second per search\n\nsearch from cache:\nip count:683844,\ntotal time: 0.671801s,\nsearch 1017926.4395259906 times per second,\nuse 0.9823892583688677 micro second per search\n\nbenchmark test finish\n```\n"
  },
  {
    "path": "binding/erlang/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region erlang 查询客户端\n\n### 简介\n该bingding以erlang语言实现xdb查询客户端，基于Erlang OTP Application，查询逻辑由ip2region_worker工作进程实现，支持配多个工作进程来进行负载均衡。\n\n### 应用配置\n该应用可配置的参数在ip2region.app.src中,如下：\n``` erlang\n  {env,[\n    {poolargs, [\n        {size, 1},  %% 工作进程默认数量\n        {max_overflow, 5}  %% 工作进程最大数量\n    ]}\n  ]}\n```\n\n### 编译\n\n```\n$ rebar3 compile\n```\n\n### 运行\n将xdb文件放到priv目录下，然后启动erlang节点：\n```\n$ rebar3 shell\n```\n在erlang shell中调用xdb:search/1接口查询Ip地址信息, 该接口支持以list格式字符串、binary格式字符串、tuple和整数表示的IP地址，如下：\n```\n1> xdb:search(\"1.0.8.0\").\n[20013,22269,124,48,124,24191,19996,30465,124,24191,24030,\n 24066,124,30005,20449]\n2>\n3> io:format(\"~ts~n\", [xdb:search(\"1.0.8.0\")]).\n中国|0|广东省|广州市|电信\nio:format(\"~ts~n\", [xdb:search(<<\"1.0.8.0\">>)]).\n中国|0|广东省|广州市|电信\n4> io:format(\"~ts~n\", [xdb:search({1,0,8,0})]).\n中国|0|广东省|广州市|电信\n6> io:format(\"~ts~n\", [xdb:search(16779264)]).\n中国|0|广东省|广州市|电信\n```\n\n### 使用方法\n* 在rebar.config中引入依赖\n```\n{deps, [\n  ip2region\n]}.\n```\n* 启动ip2region Application\n```\n......\n\napplication:ensure_started(ip2region),\n\n......\n```\n\n* 调用xdb:search/1接口查询IP信息\n```\n......\n\nip2region:search(\"1.0.8.0\"),\n\n......\n```\n\n### 单元测试\n\n```\n$ rebar3 eunit\n===> Verifying dependencies...\n===> Analyzing applications...\n===> Compiling ip2region\n===> Performing EUnit tests...\n=INFO REPORT==== 17-Jan-2023::11:52:59.920155 ===\nXdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb\n\n....\nFinished in 0.074 seconds\n4 tests, 0 failures\n```\n\n### 基准测试\n```\n$ cd benchmarks/\n$ sh xdb-benchmark.sh\n===> Verifying dependencies...\n===> Analyzing applications...\n===> Compiling ip2region\nErlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]\n\nEshell V12.3.2.2  (abort with ^G)\n1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 ===\nXdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb\n\n===> Booted ip2region\n===> Evaluating: \"xdb_benchmark:main(\\\"../../data/ip.merge.txt\\\"), init:stop().\"\nCPU info:\nmodel name      : AMD EPYC 7K62 48-Core Processor\ncache size      : 512 KB\ncpu MHz         : 2595.124\nbogomips        : 5190.24\ncores/threads   : 2\n\nErlang info:\nsystem_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]\nload test data use 4.835593s\n\nstart run benchmark tests\n\nsearch from file:\nip count:683844,\ntotal time: 28.201699s,\nsearch 24248.326315375536 times per second,\nuse 41.23995969841075 micro second per search\n\nsearch from cache:\nip count:683844,\ntotal time: 0.671801s,\nsearch 1017926.4395259906 times per second,\nuse 0.9823892583688677 micro second per search\n\nbenchmark test finish\n```\n"
  },
  {
    "path": "binding/erlang/benchmarks/xdb-benchmark.sh",
    "content": "#!/bin/bash\n\ncd ..\n\nrebar3 shell --eval=\"xdb_benchmark:main(\\\"../../data/ip.merge.txt\\\"), init:stop().\"\n"
  },
  {
    "path": "binding/erlang/include/ip2region.hrl",
    "content": "-ifndef(IP2REGION_HRL).\n-define(IP2REGION_HRL, true).\n\n-define(NONE, none).\n-define(APP_NAME, ip2region).\n\n-define(XDB_VECTOR_INDEX, ets_xdb_vector_index).\n-define(XDB_SEGMENT_INDEX, ets_xdb_segement_index).\n-define(IP2REGION_CACHE, ets_ip2region_cache).\n\n\n-define(XDB_HEADER_SIZE, 256).\n-define(XDB_VECTOR_COLS, 256).\n-define(XDB_VECTOR_INDEX_SIZE, 8). \n-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256\n\n-define(XDB_SEGMENT_INDEX_SIZE, 14).\n\n-define(IP2REGION_POOL, ip2region_pool).\n\n-ifndef(IF).\n-define(IF(C, T, F), case (C) of true -> (T); false -> (F) end).\n-define(IF(C, T), ?IF(C, T, skip)).\n-endif.\n\n-endif."
  },
  {
    "path": "binding/erlang/priv/dummy",
    "content": ""
  },
  {
    "path": "binding/erlang/rebar.config",
    "content": "{erl_opts, [\n    debug_info, \n    export_all, \n    nowarn_export_all\n]}.\n\n{plugins, [rebar3_hex, rebar3_ex_doc]}.\n\n{deps, [\n    poolboy\n]}.\n\n{shell, [\n  % {config, \"config/sys.config\"},\n    {apps, [ip2region]}\n]}.\n\n{ex_doc, [\n    {extras, [\"README.md\"]},\n    {main, \"README.md\"},\n    {source_url, \"https://github.com/leihua996/ip2region/tree/master/binding/erlang\"}\n]}.\n\n{hex, [{doc, ex_doc}]}.\n"
  },
  {
    "path": "binding/erlang/src/ip2region.app.src",
    "content": "{application, ip2region,\n [{description, \"ip2region xdb client application\"},\n  {vsn, \"0.1.0\"},\n  {registered, []},\n  {mod, {ip2region_app, []}},\n  {applications,\n   [kernel,\n    stdlib\n   ]},\n  {env,[\n    {poolargs, [\n        {size, 1},\n        {max_overflow, 5}\n    ]}\n  ]},\n  {modules, []},\n\n  {licenses, [\"Apache-2.0\"]},\n  {links, [{\"Github\", \"https://github.com/leihua996/ip2region/tree/master/binding/erlang\"}]}\n ]}.\n"
  },
  {
    "path": "binding/erlang/src/ip2region_app.erl",
    "content": "%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc \n%% @end\n%%%-------------------------------------------------------------------\n\n-module(ip2region_app).\n\n-behaviour(application).\n\n-export([start/2, stop/1]).\n\nstart(_StartType, _StartArgs) ->\n    ip2region_sup:start_link().\n\nstop(_State) ->\n    ok.\n\n%% internal functions\n"
  },
  {
    "path": "binding/erlang/src/ip2region_sup.erl",
    "content": "%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc ip2region top level supervisor.\n%% @end\n%%%-------------------------------------------------------------------\n-module(ip2region_sup).\n-behaviour(supervisor).\n-include(\"ip2region.hrl\").\n\n-export([start_link/0]).\n\n-export([init/1, create_table/0]).\n\n-define(SERVER, ?MODULE).\n\nstart_link() ->\n    {ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []),\n    {ok, _PoolPid} = start_ip2region_pool(SupPid),\n    {ok, SupPid}.\n\n%% sup_flags() = #{strategy => strategy(),         % optional\n%%                 intensity => non_neg_integer(), % optional\n%%                 period => pos_integer()}        % optional\n%% child_spec() = #{id => child_id(),       % mandatory\n%%                  start => mfargs(),      % mandatory\n%%                  restart => restart(),   % optional\n%%                  shutdown => shutdown(), % optional\n%%                  type => worker(),       % optional\n%%                  modules => modules()}   % optional\ninit([]) ->\n    create_table(),\n    SupFlags = #{strategy => one_for_one,\n                 intensity => 10,\n                 period => 5},\n    ChildSpecs = [],\n    {ok, {SupFlags, ChildSpecs}}.\n\n%% internal functions\n%% \ncreate_table() ->\n    Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],\n    ets:new(?XDB_VECTOR_INDEX, Opts),\n    ets:new(?XDB_SEGMENT_INDEX, Opts),\n    ets:new(?IP2REGION_CACHE, Opts).\n\nstart_ip2region_pool(Sup) ->\n    {ok, PoolArgsCfg} = application:get_env(poolargs),\n    PoolName = ?IP2REGION_POOL,\n    PoolArgs = [{strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg],\n    WorkerArgs = [],\n    ChildSpecs = poolboy:child_spec(PoolName, PoolArgs, WorkerArgs),\n    supervisor:start_child(Sup, ChildSpecs)."
  },
  {
    "path": "binding/erlang/src/ip2region_util.erl",
    "content": "%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc \n%% ip2region utils\n%% @end\n%%%-------------------------------------------------------------------\n-module(ip2region_util).\n-export([ipv4_to_n/1]).\n\n\nipv4_to_n(IntIp) when is_integer(IntIp) -> IntIp;\nipv4_to_n({A, B, C, D}) ->\n\t<<N:32>> = <<A, B, C, D>>,\n\tN;\nipv4_to_n(Ip) when is_binary(Ip) ->\n\tipv4_to_n(binary_to_list(Ip));\nipv4_to_n(Ip) when is_list(Ip) ->\n    case inet_parse:address(Ip) of\n        {ok, Addr} ->\n            ipv4_to_n(Addr);\n        _ ->\n            {error, bad_ip_format}\n    end."
  },
  {
    "path": "binding/erlang/src/ip2region_worker.erl",
    "content": "%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc \n%% ip2region xdb client worker\n%% @end\n%%%-------------------------------------------------------------------\n-module(ip2region_worker).\n-behaviour(gen_server).\n-include(\"ip2region.hrl\").\n\n%% API\n-export([start/1, stop/1, start_link/1]).\n-export([search/2]).\n-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).\n-record(state, {xdb_fd}).\n\n%%==========================================\n%% API\n%% =========================================\nstart(Args) ->\n    Opts = [{spawn_opt, [{min_heap_size, 6000}]}],\n    gen_server:start(?MODULE, Args, Opts).\n\nstart_link(Args) ->\n    Opts = [{spawn_opt, [{min_heap_size, 6000}]}],\n    gen_server:start_link(?MODULE, Args, Opts).\n\n\nstop(Pid) ->\n    gen_server:call(Pid, stop).\n\nsearch(Pid, Ip) ->\n    gen_server:call(Pid, {search, Ip}).\n\n%%==========================================\n%% gen_server callbacks\n%% =========================================\ninit(_Args) ->\n    process_flag(trap_exit, true),\n    AppName = \n        case application:get_application() of\n            {ok, AName} -> AName;\n            _ -> ?APP_NAME\n        end,\n    PrivDir = code:priv_dir(AppName),\n    XdbFileName = filename:join([PrivDir, \"ip2region.xdb\"]),\n    error_logger:info_report(io_lib:format(\"XdbFile:~s~n\", [XdbFileName])),\n    {ok, IoDevice} = file:open(XdbFileName, [read, binary]),\n    load_vector_index(IoDevice),\n    {ok, #state{xdb_fd = IoDevice}}.\n\nhandle_call(Request, From, State) ->\n    try\n        do_call(Request, From, State)\n    catch\n        Class:Error:Stacktrace ->\n            error_logger:error_report(io_lib:format(\"~p handle call error, Req:~p ~p, stacktrace:~p~n\", \n                [?MODULE, Request, {Class, Error}, Stacktrace])),\n            {reply, {error, {Class, Error}}, State}\n    end.\n\nhandle_cast(Msg, State) ->\n    try\n        do_cast(Msg, State)\n    catch\n        Class:Error:Stacktrace ->\n            error_logger:error_report(io_lib:format(\"~p handle cast error, Msg:~p, ~p, stacktrace:~w~n\", \n                [?MODULE, Msg, {Class, Error}, Stacktrace])),\n            {noreply, State}\n    end.\n\nhandle_info(Info, State) ->\n    try\n        do_info(Info, State)\n    catch\n        Class:Error:Stacktrace ->\n            error_logger:error_report(io_lib:format(\"~p handle info error, Info:~p, ~p, stacktrace:~p~n\", \n                [?MODULE, Info, {Class, Error}, Stacktrace])),\n            {noreply, State}\n    end.\n\nterminate(_Reason, State) ->\n    #state{xdb_fd = XdbFd} = State,\n    case is_pid(XdbFd) of\n        true ->\n            file:close(XdbFd);\n        _ ->\n            skip\n    end,\n    ok.\n\ncode_change(_OldVsn, State, _Extra) ->\n    {ok, State}.\n\n\n%%==========================================\n%% Internal function\n%% =========================================\ndo_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) ->\n    Reply = search_ip(IoDevice, Ip),\n    {reply, Reply, State};\n\ndo_call(stop, _From, State) ->\n    {stop, normal, stopped, State};\n\ndo_call(Request, From, State) ->\n    error_logger:error_report(io_lib:format(\"unknown request: ~p, from:~p\", [Request, From])),\n    {noreply, State}.\n\ndo_cast(Msg, State) ->\n    error_logger:error_report(io_lib:format(\"unknown msg: ~p\", [Msg])),\n    {noreply, State}.\n\ndo_info(Info, State) ->\n    error_logger:error_report(io:format(\"unknown info: ~p\", [Info])),\n    {noreply, State}.\n    \n\nload_vector_index(IoDevice) ->\n    Key = ip2region_header_loaded,\n    case persistent_term:get(Key, false) of\n        true -> ok;\n        _ -> \n            {ok, <<_Header:?XDB_HEADER_SIZE/binary, VectorIndexBin/binary>> } = \n                file:read(IoDevice, ?XDB_HEADER_SIZE + ?XDB_VECTOR_INDEX_COUNT*8),\n            load_vector_index_aux(VectorIndexBin, 0),\n            persistent_term:put(Key, true)\n    end.\n\nload_vector_index_aux(<<>>, _Index) -> ok;\nload_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index) ->\n    Term = {Index, SPtr, EPtr},\n    ets:insert(?XDB_VECTOR_INDEX, Term),\n    load_vector_index_aux(VectorIndexBin, Index + 1).\n\n\nsearch_ip(IoDevice, Ip) ->\n    IntIp = ip2region_util:ipv4_to_n(Ip),\n    case ets:lookup(?IP2REGION_CACHE, IntIp) of\n        [{_IntIp, RegionInfo}] ->\n            RegionInfo;\n        _ ->\n            <<A:8, B:8, _Rest/binary>> = <<IntIp:32>>,\n            VectorIdx = A * ?XDB_VECTOR_COLS + B,\n            [{_, SPtr, EPtr}] = ets:lookup(?XDB_VECTOR_INDEX, VectorIdx),\n            RegionInfo = search_ip(IoDevice, IntIp, SPtr, EPtr, 0, (EPtr - SPtr) div ?XDB_SEGMENT_INDEX_SIZE),\n            ets:insert_new(?IP2REGION_CACHE, {IntIp, RegionInfo}),\n            RegionInfo\n    end.\n\nsearch_ip(IoDevice, IntIp, SPtr, EPtr, Low, High) when Low =< High ->\n    Middle = (Low + High) bsr 1,\n    SPtr2 = SPtr + Middle * ?XDB_SEGMENT_INDEX_SIZE,\n    {SIp, EIp, DataLen, DataPtr} = read_segement_index(IoDevice, SPtr2),\n    if \n        IntIp < SIp ->\n            search_ip(IoDevice, IntIp, SPtr, EPtr, Low, Middle - 1);\n        IntIp > EIp ->\n            search_ip(IoDevice, IntIp, SPtr, EPtr, Middle + 1, High);\n        true ->\n            {ok, DataBin} = read_file(IoDevice, DataPtr, DataLen),\n            unicode:characters_to_nfc_list(DataBin)\n    end;\nsearch_ip(_IoDevice, _IntIp, _SPtr, _EPtr, _Low, _High)  ->\n    {error, unknown}.\n\nread_file(IoDevice, Position, DataLength) ->\n    file:position(IoDevice, {bof, Position}),\n    file:read(IoDevice, DataLength).\n\nread_segement_index(IoDevice, SPtr) ->\n    case ets:lookup(?XDB_SEGMENT_INDEX, SPtr) of\n        [{_SPtr, SIp, EIp, DataLen, DataPtr}] ->\n            {SIp, EIp, DataLen, DataPtr};\n        _ ->\n            {ok, <<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>>} = \n\t            read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE),\n            ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}),\n            {SIp, EIp, DataLen, DataPtr}\n    end."
  },
  {
    "path": "binding/erlang/src/xdb.erl",
    "content": "\n%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc \n%% ip2region xdb client search api\n%% @end\n%%%-------------------------------------------------------------------\n-module(xdb).\n-include(\"ip2region.hrl\").\n\n-export([search/1]).\n\n-spec search(Ip :: tuple() | list() | binary()) -> Result :: binary | {error, Reason::atom()}.\nsearch(Ip) when is_integer(Ip); is_list(Ip); is_tuple(Ip); is_binary(Ip) ->\n    case ip2region_util:ipv4_to_n(Ip) of\n    IntIp when is_integer(IntIp) ->\n        case ets:lookup(?IP2REGION_CACHE, IntIp) of\n        [{_IntIp, Region}] -> Region;\n        _ ->\n            Worker = poolboy:checkout(?IP2REGION_POOL, true, infinity),\n            try\n                ip2region_worker:search(Worker, IntIp)\n            after\n                poolboy:checkin(?IP2REGION_POOL, Worker)\n            end\n        end;\n    Ret ->\n        Ret\n    end.\n"
  },
  {
    "path": "binding/erlang/src/xdb_benchmark.erl",
    "content": "%%%-------------------------------------------------------------------\n%% Copyright 2022 The Ip2Region Authors. All rights reserved.\n%% Use of this source code is governed by a Apache2.0-style\n%% license that can be found in the LICENSE file.\n%% \n%% @doc \n%% ip2region xdb client benchmark test\n%% @end\n%%%-------------------------------------------------------------------\n-module(xdb_benchmark).\n-export([main/1]).\n\nmain(DataFile) ->\n\tapplication:ensure_started(ip2region),\n    show_hw_sw_info(),\n    IpList = load_test_data(DataFile),\n    run(IpList).\n\nshow_hw_sw_info() ->\n    io:format(\"CPU info:~n\", []),\n    io:format(\"~s\", [os:cmd(\"egrep '^model name' /proc/cpuinfo | head -1\")]),\n    io:format(\"~s\", [os:cmd(\"egrep '^cache' /proc/cpuinfo | head -1\")]),\n    io:format(\"~s\", [os:cmd(\"egrep '^cpu MHz' /proc/cpuinfo | head -1\")]),\n    io:format(\"~s\", [os:cmd(\"egrep '^bogomips' /proc/cpuinfo | head -1\")]),\n    io:format(\"cores/threads   : ~s~n\", [os:cmd(\"egrep -c '^processor' /proc/cpuinfo\")]),\n    io:format(\"Erlang info:~n\", []),\n    io:format(\"system_version:~s\", [erlang:system_info(system_version)]),\n    ok.\n\nload_test_data(DataFile) ->\n    {ok, Fd} = file:open(DataFile, [read]),\n    T0 = os:timestamp(),\n    IpList = load_test_data(Fd, []),\n    T1 = os:timestamp(),\n    Sec = timer:now_diff(T1, T0) / 1000000,\n    io:format(\"load test data use ~ps~n\", [Sec]),\n    IpList.\n\nload_test_data(Fd, IpList) ->\n    case file:read_line(Fd) of\n        {ok, Ip} -> \n            case string:tokens(unicode:characters_to_list(Ip), \"|\") of\n            [SIp | _Tail] ->\n                load_test_data(Fd, [string:trim(SIp)| IpList]);\n            _ ->\n                load_test_data(Fd, IpList)\n            end;\n        _ ->\n\t\t\tfile:close(Fd),\n            IpList\n    end.\n\nrun(IpList) ->\n    garbage_collect(),\n    io:format(\"~nstart run benchmark tests~n\", []),\n    io:format(\"~nsearch from file:~n\", []),\n    run_test(IpList),\n    io:format(\"~nsearch from cache:~n\", []),\n    run_test(IpList),\n    io:format(\"~nbenchmark test finish~n\", []).\n\nrun_test(IpList) ->\n    T0 = os:timestamp(),\n    run_test_aux(IpList),\n    T1 = os:timestamp(),\n    Sec = timer:now_diff(T1, T0) / 1000000,\n    IpCount = length(IpList),\n    io:format(\"ip count:~p,~ntotal time: ~ps,~nsearch ~p times per second,~nuse ~p micro second per search~n\", \n        [IpCount, Sec, IpCount / Sec, Sec * 1000000/IpCount]).\n\nrun_test_aux([]) -> ok;\nrun_test_aux([Ip | Tail]) ->\n    xdb:search(Ip),\n    run_test_aux(Tail).\n\n"
  },
  {
    "path": "binding/erlang/test/xdb_test.erl",
    "content": "-module(xdb_test).\n\n-include_lib(\"eunit/include/eunit.hrl\").\n\nsearch_test_() ->\n    application:ensure_started(ip2region),\n    A = \"中国|0|广东省|广州市|电信\",\n    Region0 = xdb:search(\"1.0.8.0\"),\n    Region1 = xdb:search(<<\"1.0.8.0\">>),\n    Region2 = xdb:search({1,0,8,0}),\n    Region3 = xdb:search(\"xxx.0.8.0\"),\n    [\n        ?_assert(A =:= Region0),\n        ?_assert(A =:= Region1),\n        ?_assert(A =:= Region2),\n        ?_assert({error, bad_ip_format} =:= Region3)\n\n    ]."
  },
  {
    "path": "binding/golang/Makefile",
    "content": "# ip2region golang binding makefile\nall: build\n.PHONY: all\n\nbuild:\n\tgo build -o xdb_searcher\ntest:\n\tgo test -v ./...\nclean:\n\tfind ./ -name xdb_searcher | xargs rm -f"
  },
  {
    "path": "binding/golang/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region golang query client\n\n# Usage\n\n### package get\n\n```bash\ngo get github.com/lionsoul2014/ip2region/binding/golang\n```\n\n### About the query service\n\nStarting from version `3.11.0`, a dual-protocol compatible and concurrency-safe `Ip2Region` query service is provided. **It is recommended to prioritize this method for query calls.** The specific usage is as follows:\n\n```go\nimport \"github.com/lionsoul2014/ip2region/binding/golang/service\"\n\n// 1. Create v4 configuration: specify the cache policy and the v4 xdb file path\n// Parameter 1: Cache policy, options: service.NoCache / service.VIndexCache / service.BufferCache\n// Parameter 2: xdb file path\n// Parameter 3: Number of initialized searchers\nv4Config, err := service.NewV4Config(service.VIndexCache, \"ip2region v4 xdb path\", 20)\nif err != nil {\n    return fmt.Errorf(\"failed to create v4 config: %s\", err)\n}\n\n// 2. Create v6 configuration: specify the cache policy and the v6 xdb file path\nv6Config, err := service.NewV6Config(service.VIndexCache, \"ip2region v6 xdb path\", 20)\nif err != nil {\n    return fmt.Errorf(\"failed to create v6 config: %s\", err)\n}\n\n// 3. Create the Ip2Region query service using the above configurations\nip2region, err := service.NewIp2Region(v4Config, v6Config)\nif err != nil {\n    return fmt.Errorf(\"failed to create ip2region service: %s\", err)\n}\n\n// 4. Export the ip2region service for concurrent dual-version IP address queries, for example:\nv4Region, err := ip2region.SearchByStr(\"113.92.157.29\")                          // Perform IPv4 query\nv6Region, err := ip2region.SearchByStr(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\") // Perform IPv6 query\n\n\n// 5. When the parent service needs to be closed, close the ip2region query service as well\nip2region.Close()\n```\n\n##### `Ip2Region` Query Notes:\n\n1. The API of this query service is concurrency-safe and supports both IPv4 and IPv6 addresses; the internal implementation handles identification automatically.\n2. v4 and v6 configurations need to be created separately. You can set different cache policies for v4 and v6, or specify one as `nil`, which will cause queries for that IP version to return `\"\"`.\n3. Please set an appropriate number of searchers based on your project's concurrency. This value is fixed during runtime; each query borrows a searcher from the pool to complete the operation and returns it afterward. If the pool is empty during borrowing, it will wait until a searcher becomes available.\n4. If the cache policy is set to `service.BufferCache` (Full Memory Cache), a single-instance memory searcher is used by default. This implementation is natively concurrency-safe, and the specified number of searchers will be ignored.\n5. If `Close` is called while the `Ip2Region` service is running, it will wait up to 10 seconds by default for searchers to be returned. You can also call `CloseTimeout` to define a custom maximum wait time.\n\n### About the Query API\n\nThe location information query API prototypes are:\n\n```go\nSearchByStr(string) (string, error)\nSearch([]byte) (string, error)\n```\n\nIf a query fails, the `error` will contain specific error details. If successful, it returns the `region` string. If the specified IP cannot be found, it returns an empty string `\"\"`.\n\n### About IPv4 / IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. Usage is as follows:\n\n```go\n// For IPv4: Set the xdb path to the v4 xdb file and specify the IP version as xdb.IPv4\ndbPath := \"../../data/ip2region_v4.xdb\"  // Or your ipv4 xdb path\nversion := xdb.IPv4\n\n// For IPv6: Set the xdb path to the v6 xdb file and specify the IP version as xdb.IPv6\ndbPath = \"../../data/ip2region_v6.xdb\"  // Or your ipv6 xdb path\nversion = xdb.IPv6\n\n// The IP version of the xdb specified by dbPath must match the version specified, otherwise the query will throw an error.\n// Note: The following demonstrations directly use the dbPath and version variables.\n```\n\n### File Verification\n\nIt is recommended to actively verify the applicability of the xdb file. New features in later versions may make the current Searcher version incompatible with your xdb file. Verification helps avoid unpredictable errors during runtime.\nYou do not need to verify every time; for example, run it during service startup or manually via command line to confirm version matching. Do not run verification every time a Searcher is created, as this will impact query response speed, especially in high-concurrency scenarios.\n\n```go\nerr := xdb.VerifyFromFile(dbPath)\nif err != nil {\n\t// err contains the verification error\n\treturn fmt.Errorf(\"xdb file verify: %w\", err)\n}\n\n// The current Searcher can safely be used for query operations on the xdb specified by dbPath.\n```\n\n### Pure File-Based Query\n\n```go\nimport (\n\t\"fmt\"\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n    \"time\"\n)\n\nfunc main() {\n\t// Create a pure file-based query object using version and dbPath\n    searcher, err := xdb.NewWithFileOnly(version, dbPath)\n    if err != nil {\n        fmt.Printf(\"failed to create searcher: %s\\n\", err.Error())\n        return\n    }\n\n    defer searcher.Close()\n\n    // Location info query: both IPv4 and IPv6 addresses are supported\n    var ip = \"1.2.3.4\"  // IPv4\n\t// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\n    var tStart = time.Now()\n    region, err := searcher.SearchByStr(ip)\n    if err != nil {\n        fmt.Printf(\"failed to SearchIP(%s): %s\\n\", ip, err)\n        return\n    }\n\n\t// IPv4 or IPv6 location information \n    fmt.Printf(\"{region: %s, took: %s}\\n\", region, time.Since(tStart))\n\n    // Note: For concurrent use, each goroutine needs to create an independent searcher object.\n}\n```\n\n### Caching `VectorIndex`\n\nYou can pre-load the `vectorIndex` cache and store it as a global variable. Using the global `vectorIndex` whenever creating a searcher reduces a fixed IO operation, accelerating queries and reducing system IO pressure.\n\n```go\n// 1. Load VectorIndex cache from dbPath and store the vIndex variable globally in memory.\nvIndex, err := xdb.LoadVectorIndexFromFile(dbPath)\nif err != nil {\n    fmt.Printf(\"failed to load vector index from `%s`: %s\\n\", dbPath, err)\n    return\n}\n\n// 2. Create a query object with VectorIndex cache using the global vIndex.\nsearcher, err := xdb.NewWithVectorIndex(version, dbPath, vIndex)\nif err != nil {\n    fmt.Printf(\"failed to create searcher with vector index: %s\\n\", err)\n    return\n}\n\n// Note: For concurrent use, all goroutines share the global read-only vIndex cache, while each goroutine creates an independent searcher object.\n```\n\n### Caching the entire `xdb` file\n\nYou can pre-load the entire xdb file into memory for full memory-based queries, similar to the previous memory search.\n\n```go\n// 1. Load the entire xdb from dbPath into memory\ncBuff, err := xdb.LoadContentFromFile(dbPath)\nif err != nil {\n    fmt.Printf(\"failed to load content from `%s`: %s\\n\", dbPath, err)\n    return\n}\n\n// 2. Create a fully memory-based query object using the global cBuff.\nsearcher, err := xdb.NewWithBuffer(version, cBuff)\nif err != nil {\n    fmt.Printf(\"failed to create searcher with content: %s\\n\", err)\n    return\n}\n\n// Note: For concurrent use, searcher objects created with the entire xdb cache can be safely used for concurrency.\n```\n\n# Compile the test program\n\nCompile to get the xdb_searcher executable through the following method:\n\n```bash\n# cd to the golang binding root directory first\nmake\n```\n\n# Query test\n\n### Query command\n\nTest xdb queries using the `./xdb_searcher search` command:\n\n```bash\n➜  golang git:(master) ✗ ./xdb_searcher search --help\n./xdb_searcher search [command options]\noptions:\n --v4-db string            ip2region v4 binary xdb file path\n --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\n --v6-db string            ip2region v6 binary xdb file path\n --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\n --help                    print this help menu\n```\n\n### Parameter parsing\n\n1. `v4-xdb`: IPv4 xdb file path, defaults to data/ip2region_v4.xdb in the repository\n2. `v6-xdb`: IPv6 xdb file path, defaults to data/ip2region_v6.xdb in the repository\n3. `v4-cache-policy`: Cache policy used for v4 queries, defaults to `vectorIndex`, options: file/vectorIndex/content\n4. `v6-cache-policy`: Cache policy used for v6 queries, defaults to `vectorIndex`, options: file/vectorIndex/content\n\n### Test Demo\n\nExample: Perform query tests using the default data/ip2region_v4.xdb and data/ip2region_v6.xdb:\n\n```bash\n➜  golang git:(master) ✗ ./xdb_searcher search       \nip2region search service test program\n+-v4 db: /data01/code/c/ip2region/data/ip2region_v4.xdb (vectorIndex)\n+-v6 db: /data01/code/c/ip2region/data/ip2region_v6.xdb (vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, took: 50.216µs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, took: 100.606µs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, took: 99.078µs}\n```\n\nEnter an IPv4 or IPv6 address to perform query tests. You can also set `cache-policy` to file/vectorIndex/content respectively to test the performance of the three different cache implementations.\n\n# bench test\n\n### Test command\n\nPerform automatic bench testing using the `xdb_searcher bench` command. This ensures that both the program and the `xdb` file are free of errors, and provides average query performance through a large number of queries:\n\n```bash\n➜  golang git:(fr_xdb_ipv6) ./xdb_searcher bench\n./xdb_searcher bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n### v4 bench\n\nExample: Perform ipv4 bench testing using data/ip2region_v4.xdb and data/ipv4_source.txt:\n\n```bash\n./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt \n```\n\n### v6 bench\n\nExample: Perform ipv6 bench testing using data/ip2region_v6.xdb and data/ipv6_source.txt:\n\n```bash\n./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt \n```\n\nYou can set the `cache-policy` parameter to test the efficiency of file/vectorIndex/content cache mechanisms respectively.\n\n*Please note that the src file used for bench needs to be the same source file used to generate the corresponding xdb file*.\n\nThe bench program will read the source IP file specified by `src` line by line, then select the start and end IPs from each IP segment for testing to ensure that the queried region information matches the original region information. There is no debug information output during the test; if an error occurs, the error message will be printed and execution will terminate. Seeing `Bench finished` indicates the bench was successful. Cost represents the average time for each query operation (ns).\n"
  },
  {
    "path": "binding/golang/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region golang 查询客户端\n\n# 使用方式\n\n### package 获取\n```bash\ngo get github.com/lionsoul2014/ip2region/binding/golang\n```\n\n### 关于查询服务\n从 `3.11.0` 版本开始提供了一个双协议兼容且并发安全的 `Ip2Region` 查询服务，**建议优先使用该方式来进行查询调用**，具体使用方式如下：\n```go\nimport \"github.com/lionsoul2014/ip2region/binding/golang/service\"\n\n// 1, 创建 v4 的配置：指定缓存策略和 v4 的 xdb 文件路径\n// 参数1： 缓存策略, options: service.NoCache / service.VIndexCache / service.BufferCache\n// 参数2: xdb 文件路径\n// 参数3: 初始化的查询器数量\nv4Config, err := service.NewV4Config(service.VIndexCache, \"ip2region v4 xdb path\", 20)\nif err != nil {\n    return fmt.Errorf(\"failed to create v4 config: %s\", err)\n}\n\n// 2, 创建 v6 的配置：指定缓存策略和 v6 的 xdb 文件路径\nv6Config, err := service.NewV6Config(service.VIndexCache, \"ip2region v6 xdb path\", 20)\nif err != nil {\n    return fmt.Errorf(\"failed to create v6 config: %s\", err)\n}\n\n// 3，通过上述配置创建 Ip2Region 查询服务\nip2region, err := service.NewIp2Region(v4Config, v6Config)\nif err != nil {\n    return fmt.Errorf(\"failed to create ip2region service: %s\", err)\n}\n\n// 4，导出 ip2region 服务进行双版本的IP地址的并发查询，例如：\nv4Region, err := ip2region.SearchByStr(\"113.92.157.29\")                          // 进行 IPv4 查询\nv6Region, err := ip2region.SearchByStr(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\") // 进行 IPv6 查询\n\n\n// 5，在服务需要关闭的时候，同时关闭 ip2region 查询服务\nip2region.Close()\n```\n\n##### `Ip2Region` 查询备注:\n1. 该查询服务的 API 并发安全且同时支持 IPv4 和 Ipv6 的地址，内部实现会自动判断。\n2. v4 和 v6 的配置需要单独创建，可以给 v4 和 v6 设置使用不同的缓存策略，也可以指定其中一个为 `nil` 则该版本的 IP 地址查询都会返回 `\"\"`。\n3. 请结合您的项目的并发数设置一个合适的查询器数量，这个值在运行过程中是固定的，每次查询会从池子里租借一个查询器来完成查询操作，查询完成后再归还回去，如果租借的时候池子已经空了则等待直到有可用的查询器来完成查询服务。\n4. 如果配置设置的缓存策略为 `service.BufferCache` 即 `全内存缓存` 则默认会使用单实例的内存查询器，该实现天生并发安全，此时指定的查询器数量无效。\n5. 如果 `Ip2Region` 查询器在提供服务期间，调用 Close 默认会最大等待 10 秒钟来等待尽量多的查询器归还，也可以调用 `CloseTimeout` 来自定义最长等待时间。\n\n\n### 关于查询 API\n定位信息查询 API 原型为：\n```go\nSearchByStr(string) (string, error)\nSearch([]byte) (string, error)\n```\n查询出错则 error 会包含具体的错误信息，查询成功会返回字符串的 `region` 信息，如果指定的 IP 查询不到则会返回空字符串 `\"\"`。\n\n### 关于 IPv4 / IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```go\n// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 xdb.IPv4\ndbPath := \"../../data/ip2region_v4.xdb\"  // 或者你的 ipv4 xdb 的路径\nversion := xdb.IPv4\n\n// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 xdb.IPv6\ndbPath = \"../../data/ip2region_v6.xdb\"  // 或者你的 ipv6 xdb 路径\nversion = xdb.IPv6\n\n// dbPath 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n// 备注：以下演示直接使用 dbPath 和 version 变量\n```\n\n### 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。\n你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```go\nerr := xdb.VerifyFromFile(dbPath)\nif err != nil {\n\t// err 包含的验证的错误\n\treturn fmt.Errorf(\"xdb file verify: %w\", err)\n}\n\n// 当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n\n```go\nimport (\n\t\"fmt\"\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n    \"time\"\n)\n\nfunc main() {\n\t// 通过 version 和 dbPath 创建完全基于文件的查询对象\n    searcher, err := xdb.NewWithFileOnly(version, dbPath)\n    if err != nil {\n        fmt.Printf(\"failed to create searcher: %s\\n\", err.Error())\n        return\n    }\n\n    defer searcher.Close()\n\n    // 定位信息查询：IPv4 或者 IPv6 的地址都支持\n    var ip = \"1.2.3.4\"  // IPv4\n\t// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\n    var tStart = time.Now()\n    region, err := searcher.SearchByStr(ip)\n    if err != nil {\n        fmt.Printf(\"failed to SearchIP(%s): %s\\n\", ip, err)\n        return\n    }\n\n\t// IPv4 或者 IPv6 的定位信息 \n    fmt.Printf(\"{region: %s, took: %s}\\n\", region, time.Since(tStart))\n\n    // 备注：并发使用，每个 goroutine 需要创建一个独立的 searcher 对象。\n}\n```\n\n### 缓存 `VectorIndex` 索引\n\n可以预先加载 `vectorIndex` 缓存，然后做成全局变量，每次创建 searcher 的时候使用全局的 `vectorIndex`，可以减少一次固定的 IO 操作从而加速查询，减少系统 io 压力。\n```go\n// 1、从 dbPath 加载 VectorIndex 缓存，把下述 vIndex 变量全局到内存里面。\nvIndex, err := xdb.LoadVectorIndexFromFile(dbPath)\nif err != nil {\n    fmt.Printf(\"failed to load vector index from `%s`: %s\\n\", dbPath, err)\n    return\n}\n\n// 2、用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。\nsearcher, err := xdb.NewWithVectorIndex(version, dbPath, vIndex)\nif err != nil {\n    fmt.Printf(\"failed to create searcher with vector index: %s\\n\", err)\n    return\n}\n\n// 备注：并发使用，全部 goroutine 共享全局的只读 vIndex 缓存，每个 goroutine 创建一个独立的 searcher 对象\n```\n\n### 缓存整个 `xdb` 文件\n\n可以预先加载整个 xdb 文件到内存，完全基于内存查询，类似于之前的 memory search 查询。\n```go\n// 1、从 dbPath 加载整个 xdb 到内存\ncBuff, err := xdb.LoadContentFromFile(dbPath)\nif err != nil {\n    fmt.Printf(\"failed to load content from `%s`: %s\\n\", dbPath, err)\n    return\n}\n\n// 2、用全局的 cBuff 创建完全基于内存的查询对象。\nsearcher, err := xdb.NewWithBuffer(version, cBuff)\nif err != nil {\n    fmt.Printf(\"failed to create searcher with content: %s\\n\", err)\n    return\n}\n\n// 备注：并发使用，用整个 xdb 缓存创建的 searcher 对象可以安全用于并发。\n```\n\n\n\n# 编译测试程序\n\n通过如下方式编译得到 xdb_searcher 可执行程序：\n```bash\n# 切换到 golang binding 根目录\nmake\n```\n\n\n# 查询测试\n\n### 查询命令\n通过 `./xdb_searcher search` 命令来测试 xdb 的查询：\n```bash\n➜  golang git:(master) ✗ ./xdb_searcher search --help\n./xdb_searcher search [command options]\noptions:\n --v4-db string            ip2region v4 binary xdb file path\n --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\n --v6-db string            ip2region v6 binary xdb file path\n --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\n --help                    print this help menu\n```\n\n### 参数解析\n1. `v4-xdb`: IPv4 的 xdb 文件路径，默认为仓库中的 data/ip2region_v4.xdb\n2. `v6-xdb`: IPv6 的 xdb 文件路径，默认为仓库中的 data/ip2region_v6.xdb\n3. `v4-cache-policy`: v4 查询使用的缓存策略，默认为 `vectorIndex`，可选：file/vectorIndex/content\n4. `v6-cache-policy`: v6 查询使用的缓存策略，默认为 `vectorIndex`，可选：file/vectorIndex/content\n\n### 测试 Demo\n例如：使用默认的 data/ip2region_v4.xdb 和 data/ip2region_v6.xdb 进行查询测试：\n```bash\n➜  golang git:(master) ✗ ./xdb_searcher search       \nip2region search service test program\n+-v4 db: /data01/code/c/ip2region/data/ip2region_v4.xdb (vectorIndex)\n+-v6 db: /data01/code/c/ip2region/data/ip2region_v6.xdb (vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, took: 50.216µs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, took: 100.606µs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, took: 99.078µs}\n```\n输入 v4 或者 v6 的 IP 地址即可进行查询测试，也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。\n\n\n# bench 测试\n\n### 测试命令\n通过 `xdb_searcher bench` 命令来进行自动 bench 测试，一方面确保程序和 `xdb` 文件都没有错误，另一方面通过大量的查询得到平均查询性能：\n```bash\n➜  golang git:(fr_xdb_ipv6) ./xdb_searcher bench\n./xdb_searcher bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n### v4 bench\n例如：通过 data/ip2region_v4.xdb 和 data/ipv4_source.txt 进行 ipv4 的 bench 测试：\n```bash\n./xdb_searcher bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt \n```\n\n### v6 bench\n例如：通过 data/ip2region_v6.xdb 和 data/ipv6_source.txt 进行 ipv6 的 bench 测试：\n```bash\n./xdb_searcher bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt \n```\n\n可以设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 不同缓存实现机制的效率。\n\n*请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件的相同的源文件*。\n\nbench 程序会逐行读取 `src` 指定的源IP文件，然后每个 IP 段选取开始和结束的 IP 进行测试，以确保查询的 region 信息和原始的 region 信息是相同。测试途中没有调试信息的输出，有错误会打印错误信息并且终止运行，所以看到 `Bench finished` 就表示 bench 成功了，cost 是表示每次查询操作的平均时间(ns)。\n"
  },
  {
    "path": "binding/golang/go.mod",
    "content": "module github.com/lionsoul2014/ip2region/binding/golang\n\ngo 1.17\n\nrequire github.com/mitchellh/go-homedir v1.1.0"
  },
  {
    "path": "binding/golang/go.sum",
    "content": "github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\n"
  },
  {
    "path": "binding/golang/main.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/16\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/binding/golang/service\"\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n\t\"github.com/mitchellh/go-homedir\"\n)\n\nfunc getXdbPath(fileName string) (string, error) {\n\tbinPath, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get executale: %w\", err)\n\t}\n\n\txdbPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(binPath))), \"/data/\", fileName)\n\t_, err = os.Stat(xdbPath)\n\tif err != nil {\n\t\treturn \"\", nil\n\t}\n\n\t// fmt.Printf(\"xdbPath=%s\\n\", xdbPath)\n\treturn xdbPath, nil\n}\n\nfunc createService(v4XdbPath string, v4CachePolicy string, v6XdbPath string, v6CachePolicy string) (*service.Ip2Region, error) {\n\t// try to create v4 config\n\tv4CPolicy, err := service.CachePolicyFromName(v4CachePolicy)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse v4 cache policy: %w\", err)\n\t}\n\n\tv4DbPath, err := homedir.Expand(v4XdbPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Expand(`%s`): %s\", v4XdbPath, err)\n\t}\n\tv4Config, err := service.NewV4Config(v4CPolicy, v4DbPath, 1)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"NewV4Config: %w\", err)\n\t}\n\n\t// try to create v6 config\n\tv6Policy, err := service.CachePolicyFromName(v6CachePolicy)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse v6 cache policy: %w\", err)\n\t}\n\n\tv6DbPath, err := homedir.Expand(v6XdbPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Expand(`%s`): %s\", v6XdbPath, err)\n\t}\n\tv6Config, err := service.NewV6Config(v6Policy, v6DbPath, 1)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"NewV6Config: %w\", err)\n\t}\n\n\treturn service.NewIp2Region(v4Config, v6Config)\n}\n\nfunc createSearcher(dbPath string, cachePolicy string) (*xdb.Searcher, error) {\n\thandle, err := os.OpenFile(dbPath, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open xdb file `%s`: %w\", dbPath, err)\n\t}\n\n\tdefer handle.Close()\n\n\t// verify the xdb file\n\t// @Note: do NOT call it every time you create a searcher since this will slow down the search response.\n\t// @see the util.Verify function for details.\n\terr = xdb.Verify(handle)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"xdb verify: %w\", err)\n\t}\n\n\t// auto-detect the ip version from the xdb header\n\theader, err := xdb.LoadHeader(handle)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to load header from `%s`: %s\", dbPath, err)\n\t}\n\n\tversion, err := xdb.VersionFromHeader(header)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to detect IP version from `%s`: %s\", dbPath, err)\n\t}\n\n\tswitch cachePolicy {\n\tcase \"nil\", \"file\":\n\t\treturn xdb.NewWithFileOnly(version, dbPath)\n\tcase \"vectorIndex\":\n\t\tvIndex, err := xdb.LoadVectorIndexFromFile(dbPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to load vector index from `%s`: %w\", dbPath, err)\n\t\t}\n\n\t\treturn xdb.NewWithVectorIndex(version, dbPath, vIndex)\n\tcase \"content\":\n\t\tcBuff, err := xdb.LoadContentFromFile(dbPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to load content from '%s': %w\", dbPath, err)\n\t\t}\n\n\t\treturn xdb.NewWithBuffer(version, cBuff)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid cache policy `%s`, options: file/vectorIndex/content\", cachePolicy)\n\t}\n}\n\nfunc printHelp() {\n\tfmt.Printf(\"ip2region xdb searcher\\n\")\n\tfmt.Printf(\"%s [command] [command options]\\n\", os.Args[0])\n\tfmt.Printf(\"Command: \\n\")\n\tfmt.Printf(\"  search    search input test\\n\")\n\tfmt.Printf(\"  bench     search bench test\\n\")\n}\n\nfunc testSearch() {\n\tvar err error\n\tvar help = \"\"\n\tvar v4DbPath, v4CachePolicy = \"\", \"vectorIndex\"\n\tvar v6DbPath, v6CachePolicy = \"\", \"vectorIndex\"\n\tfor i := 2; i < len(os.Args); i++ {\n\t\tr := os.Args[i]\n\t\tif len(r) < 5 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Index(r, \"--\") != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar key, val = \"\", \"\"\n\t\tvar sIdx = strings.Index(r, \"=\")\n\t\tif sIdx < 0 {\n\t\t\t// fmt.Printf(\"missing = for args pair '%s'\\n\", r)\n\t\t\t// return\n\t\t\tkey = r[2:]\n\t\t} else {\n\t\t\tkey = r[2:sIdx]\n\t\t\tval = r[sIdx+1:]\n\t\t}\n\n\t\tswitch key {\n\t\tcase \"help\":\n\t\t\tif val == \"\" {\n\t\t\t\thelp = \"true\"\n\t\t\t} else {\n\t\t\t\thelp = val\n\t\t\t}\n\t\tcase \"v4-db\":\n\t\t\tv4DbPath = val\n\t\tcase \"v4-cache-policy\":\n\t\t\tv4CachePolicy = val\n\t\tcase \"v6-db\":\n\t\t\tv6DbPath = val\n\t\tcase \"v6-cache-policy\":\n\t\t\tv6CachePolicy = val\n\t\tdefault:\n\t\t\tfmt.Printf(\"undefined option `%s`\\n\", r)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// check and get the get the default v4 xdb path\n\tif v4DbPath == \"\" {\n\t\tv4DbPath, err = getXdbPath(\"ip2region_v4.xdb\")\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"failed to get v4 xdb path: %s\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// check and get the get the default v6 xdb path\n\tif v6DbPath == \"\" {\n\t\tv6DbPath, err = getXdbPath(\"ip2region_v6.xdb\")\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"failed to get v6 xdb path: %s\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif v4DbPath == \"\" || v6DbPath == \"\" || help == \"true\" {\n\t\tfmt.Printf(\"%s search [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --v4-db string            ip2region v4 binary xdb file path\\n\")\n\t\tfmt.Printf(\" --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\\n\")\n\t\tfmt.Printf(\" --v6-db string            ip2region v6 binary xdb file path\\n\")\n\t\tfmt.Printf(\" --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\\n\")\n\t\tfmt.Printf(\" --help                    print this help menu\\n\")\n\t\treturn\n\t}\n\n\t// create the search service with the xdb paths and cache policies\n\tip2region, err := createService(v4DbPath, v4CachePolicy, v6DbPath, v6CachePolicy)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create ip2region service: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tdefer func() {\n\t\tip2region.Close()\n\t\tfmt.Printf(\"searcher test program exited, thanks for trying\\n\")\n\t}()\n\n\tfmt.Printf(`ip2region search service test program\n+-v4 db: %s (%s)\n+-v6 db: %s (%s)\ntype 'quit' to exit\n`, v4DbPath, v4CachePolicy, v6DbPath, v6CachePolicy)\n\treader := bufio.NewReader(os.Stdin)\n\tfor {\n\t\tfmt.Print(\"ip2region>> \")\n\t\tstr, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to read string: %s\", err)\n\t\t}\n\n\t\tline := strings.TrimSpace(strings.TrimSuffix(str, \"\\n\"))\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif line == \"quit\" {\n\t\t\tbreak\n\t\t}\n\n\t\ttStart := time.Now()\n\t\tregion, err := ip2region.SearchByStr(line)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\x1b[0;31m{err: %s}\\x1b[0m\\n\", err.Error())\n\t\t} else {\n\t\t\tfmt.Printf(\"\\x1b[0;32m{region: %s, took: %s}\\x1b[0m\\n\", region, time.Since(tStart))\n\t\t}\n\t}\n}\n\nfunc testBench() {\n\tvar err error\n\tvar dbFile, srcFile, cachePolicy = \"\", \"\", \"vectorIndex\"\n\tfor i := 2; i < len(os.Args); i++ {\n\t\tr := os.Args[i]\n\t\tif len(r) < 5 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Index(r, \"--\") != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar sIdx = strings.Index(r, \"=\")\n\t\tif sIdx < 0 {\n\t\t\tfmt.Printf(\"missing = for args pair '%s'\\n\", r)\n\t\t\treturn\n\t\t}\n\n\t\tswitch r[2:sIdx] {\n\t\tcase \"db\":\n\t\t\tdbFile = r[sIdx+1:]\n\t\tcase \"src\":\n\t\t\tsrcFile = r[sIdx+1:]\n\t\tcase \"cache-policy\":\n\t\t\tcachePolicy = r[sIdx+1:]\n\t\tdefault:\n\t\t\tfmt.Printf(\"undefined option `%s`\\n\", r)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif dbFile == \"\" || srcFile == \"\" {\n\t\tfmt.Printf(\"%s bench [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --db string              ip2region binary xdb file path\\n\")\n\t\tfmt.Printf(\" --src string             source ip text file path\\n\")\n\t\tfmt.Printf(\" --cache-policy string    cache policy: file/vectorIndex/content\\n\")\n\t\treturn\n\t}\n\n\tdbPath, err := homedir.Expand(dbFile)\n\tif err != nil {\n\t\tfmt.Printf(\"invalid xdb file path `%s`: %s\", dbFile, err)\n\t\treturn\n\t}\n\n\tsearcher, err := createSearcher(dbPath, cachePolicy)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create searcher: %s\\n\", err.Error())\n\t\treturn\n\t}\n\tdefer func() {\n\t\tsearcher.Close()\n\t}()\n\n\thandle, err := os.OpenFile(srcFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to open source text file: %s\\n\", err)\n\t\treturn\n\t}\n\tdefer handle.Close()\n\n\tvar count, tStart, costs = int64(0), time.Now(), int64(0)\n\tvar scanner = bufio.NewScanner(handle)\n\tscanner.Split(bufio.ScanLines)\n\tfor scanner.Scan() {\n\t\tvar l = strings.TrimSpace(strings.TrimSuffix(scanner.Text(), \"\\n\"))\n\t\tvar ps = strings.SplitN(l, \"|\", 3)\n\t\tif len(ps) != 3 {\n\t\t\tfmt.Printf(\"invalid ip segment line `%s`\\n\", l)\n\t\t\treturn\n\t\t}\n\n\t\tsip, err := xdb.ParseIP(ps[0])\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"check start ip `%s`: %s\\n\", ps[0], err)\n\t\t\treturn\n\t\t}\n\n\t\teip, err := xdb.ParseIP(ps[1])\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"check end ip `%s`: %s\\n\", ps[1], err)\n\t\t\treturn\n\t\t}\n\n\t\tif xdb.IPCompare(sip, eip) > 0 {\n\t\t\tfmt.Printf(\"start ip(%s) should not be greater than end ip(%s)\\n\", ps[0], ps[1])\n\t\t\treturn\n\t\t}\n\n\t\tfor _, ip := range [][]byte{sip, eip} {\n\t\t\tsTime := time.Now()\n\t\t\tregion, err := searcher.Search(ip)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"failed to search ip '%s': %s\\n\", xdb.IP2String(ip), err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcosts += time.Since(sTime).Nanoseconds()\n\n\t\t\t// check the region info\n\t\t\tif region != ps[2] {\n\t\t\t\tfmt.Printf(\"failed Search(%s) with (%s != %s)\\n\", xdb.IP2String(ip), region, ps[2])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcount++\n\t\t}\n\t}\n\n\tcost := time.Since(tStart)\n\tfmt.Printf(\"Bench finished, {cachePolicy: %s, total: %d, took: %s, cost: %d μs/op}\\n\",\n\t\tcachePolicy, count, cost, costs/count/1000)\n}\n\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\tprintHelp()\n\t\treturn\n\t}\n\n\t// set the log flag\n\tlog.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)\n\tswitch strings.ToLower(os.Args[1]) {\n\tcase \"search\":\n\t\ttestSearch()\n\tcase \"bench\":\n\t\ttestBench()\n\tdefault:\n\t\tprintHelp()\n\t}\n}\n"
  },
  {
    "path": "binding/golang/service/config.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n)\n\n// ---\n// Ip2Region service config\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/12/03\n\nconst (\n\tNoCache     = 0\n\tVIndexCache = 1\n\tBufferCache = 2\n)\n\nfunc CachePolicyFromName(name string) (int, error) {\n\tswitch strings.ToLower(name) {\n\tcase \"file\", \"nocache\":\n\t\treturn NoCache, nil\n\tcase \"vectorindex\", \"vindex\", \"vindexcache\":\n\t\treturn VIndexCache, nil\n\tcase \"content\", \"buffercache\":\n\t\treturn BufferCache, nil\n\tdefault:\n\t\treturn NoCache, fmt.Errorf(\"invalid cache policy name `%s`\", name)\n\t}\n}\n\ntype Config struct {\n\tcachePolicy int\n\tipVersion   *xdb.Version\n\n\t// xdb file path\n\txdbPath string\n\theader  *xdb.Header\n\n\t// buffers\n\tvIndex  []byte\n\tcBuffer []byte\n\n\tsearchers int\n}\n\nfunc NewV4Config(cachePolicy int, xdbPath string, searchers int) (*Config, error) {\n\treturn newConfig(cachePolicy, xdb.IPv4, xdbPath, searchers)\n}\n\nfunc NewV6Config(cachePolicy int, xdbPath string, searchers int) (*Config, error) {\n\treturn newConfig(cachePolicy, xdb.IPv6, xdbPath, searchers)\n}\n\nfunc newConfig(cachePolicy int, ipVersion *xdb.Version, xdbPath string, searchers int) (*Config, error) {\n\tif searchers < 1 {\n\t\treturn nil, fmt.Errorf(\"searchers=%d, > 0 expected\", searchers)\n\t}\n\n\t// open the xdb binary file\n\thandle, err := os.OpenFile(xdbPath, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 1, verify the xdb\n\terr = xdb.Verify(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2, load the header\n\theader, err := xdb.LoadHeader(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// verify the ip version\n\txIpVersion, err := xdb.VersionFromHeader(header)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif xIpVersion.Id != ipVersion.Id {\n\t\treturn nil, fmt.Errorf(\"ip verison not match: xdb file %s with ip version=%s, as %s expected\", xdbPath, xIpVersion.Name, ipVersion.Name)\n\t}\n\n\t// 3, check and load the vector index buffer\n\tvar vIndex []byte = nil\n\tif cachePolicy == VIndexCache {\n\t\tvIndex, err = xdb.LoadVectorIndex(handle)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// 4, check and load the content buffer\n\tvar cBuffer []byte = nil\n\tif cachePolicy == BufferCache {\n\t\tcBuffer, err = xdb.LoadContent(handle)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &Config{\n\t\tcachePolicy: cachePolicy,\n\t\tipVersion:   ipVersion,\n\n\t\txdbPath: xdbPath,\n\t\theader:  header,\n\n\t\tvIndex:  vIndex,\n\t\tcBuffer: cBuffer,\n\n\t\tsearchers: searchers,\n\t}, nil\n}\n\nfunc (c *Config) String() string {\n\tvIndex := \"null\"\n\tif c.vIndex != nil {\n\t\tvIndex = fmt.Sprintf(\"{bytes:%d}\", len(c.vIndex))\n\t}\n\n\tcBuffer := \"null\"\n\tif c.cBuffer != nil {\n\t\tcBuffer = fmt.Sprintf(\"{bytes:%d}\", len(c.cBuffer))\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"{cache_policy:%d, version:%s, xdb_path:%s, header:%s, v_index:%s, c_buffer:%s}\",\n\t\tc.cachePolicy, c.ipVersion.String(), c.xdbPath, c.header.String(), vIndex, cBuffer,\n\t)\n}\n\nfunc (c *Config) CachePolicy() int {\n\treturn c.cachePolicy\n}\n\nfunc (c *Config) IPVersion() *xdb.Version {\n\treturn c.ipVersion\n}\n\nfunc (c *Config) Header() *xdb.Header {\n\treturn c.header\n}\n\nfunc (c *Config) VIndex() []byte {\n\treturn c.vIndex\n}\n\nfunc (c *Config) CBuffer() []byte {\n\treturn c.cBuffer\n}\n\nfunc (c *Config) Searchers() int {\n\treturn c.searchers\n}\n"
  },
  {
    "path": "binding/golang/service/config_test.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestV4Config(t *testing.T) {\n\tv4Config, err := NewV4Config(VIndexCache, \"../../../data/ip2region_v4.xdb\", 10)\n\tif err != nil {\n\t\tt.Errorf(\"failed to new v4 config: %s\", err)\n\t\treturn\n\t}\n\n\tv4BufferConfig, err := NewV4Config(BufferCache, \"../../../data/ip2region_v4.xdb\", 10)\n\tif err != nil {\n\t\tt.Errorf(\"failed to new v4 config: %s\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"v4Config: %s\\n\", v4Config)\n\tfmt.Printf(\"v4BufferConfig: %s\\n\", v4BufferConfig)\n}\n\nfunc TestV6Config(t *testing.T) {\n\tv6Config, err := NewV6Config(NoCache, \"../../../data/ip2region_v6.xdb\", 10)\n\tif err != nil {\n\t\tt.Errorf(\"failed to new v6 config: %s\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"v6Config: %s\\n\", v6Config)\n}\n"
  },
  {
    "path": "binding/golang/service/ip2region.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n)\n\n// ---\n// Ip2Region service\n// 1. Unified query interface to IPv4 and IPv6 address.\n// 2. Concurrency search support.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/12/05\n\ntype Ip2Region struct {\n\t// v4 pool for cache policy vIndex or NoCache\n\tv4Pool *SearcherPool\n\n\t// v4 xdb searcher for full in-memeory search\n\tv4InMemSearcher *xdb.Searcher\n\n\t// v6 pool for cache policy vIndex or NoCache:w\n\tv6Pool *SearcherPool\n\n\t// v6 xdb searcher for full in-memeory search\n\tv6InMemSearcher *xdb.Searcher\n}\n\n// create a new Ip2Region service with specified v4 and v6 config.\n// set it to nil to disabled the specified search for the specified version.\nfunc NewIp2Region(v4Config *Config, v6Config *Config) (*Ip2Region, error) {\n\tvar err error\n\n\t// check and init the v4 pool or in-memory searcher\n\tvar v4Pool *SearcherPool\n\tvar v4InMemSearcher *xdb.Searcher\n\tif v4Config == nil {\n\t\t// with IPv4 disabled ?\n\t\tv4Pool = nil\n\t\tv4InMemSearcher = nil\n\t} else if v4Config.cachePolicy == BufferCache {\n\t\tv4Pool = nil\n\t\tv4InMemSearcher, err = xdb.NewWithBuffer(v4Config.ipVersion, v4Config.cBuffer)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v4 in-memory searcher: %w\", err)\n\t\t}\n\t} else {\n\t\tv4InMemSearcher = nil\n\t\tv4Pool, err = NewSearcherPool(v4Config)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v4 searcher pool: %w\", err)\n\t\t}\n\t}\n\n\t// check and init the v6 pool or in-memory searcher\n\tvar v6Pool *SearcherPool\n\tvar v6InMemSearcher *xdb.Searcher\n\tif v6Config == nil {\n\t\tv6Pool = nil\n\t\tv6InMemSearcher = nil\n\t} else if v6Config.cachePolicy == BufferCache {\n\t\tv6Pool = nil\n\t\tv6InMemSearcher, err = xdb.NewWithBuffer(v6Config.ipVersion, v6Config.cBuffer)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v6 in-memory searcher: %w\", err)\n\t\t}\n\t} else {\n\t\tv6InMemSearcher = nil\n\t\tv6Pool, err = NewSearcherPool(v6Config)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v6 in-memeory searcher pool: %w\", err)\n\t\t}\n\t}\n\n\treturn &Ip2Region{\n\t\tv4Pool:          v4Pool,\n\t\tv4InMemSearcher: v4InMemSearcher,\n\n\t\tv6Pool:          v6Pool,\n\t\tv6InMemSearcher: v6InMemSearcher,\n\t}, nil\n}\n\n// create the ip2region search service with the specified v4 & v6 xdb path.\n// with default cache policy VIndexCache and default searchers = 20\nfunc NewIp2RegionWithPath(v4XdbPath string, v6XdbPath string) (*Ip2Region, error) {\n\tvar err error\n\n\t// create v4 config with default config items\n\tvar v4Config *Config\n\tif v4XdbPath == \"\" {\n\t\tv4Config = nil\n\t} else {\n\t\tv4Config, err = NewV4Config(VIndexCache, v4XdbPath, 20)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v4 config: %w\", err)\n\t\t}\n\t}\n\n\t// create v6 config with default config items\n\tvar v6Config *Config\n\tif v6XdbPath == \"\" {\n\t\tv6Config = nil\n\t} else {\n\t\tv6Config, err = NewV6Config(VIndexCache, v6XdbPath, 20)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create v6 config: %w\", err)\n\t\t}\n\t}\n\n\treturn NewIp2Region(v4Config, v6Config)\n}\n\nfunc (ip2r *Ip2Region) SearchByStr(ipStr string) (string, error) {\n\tipBytes, err := xdb.ParseIP(ipStr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn ip2r.Search(ipBytes)\n}\n\nfunc (ip2r *Ip2Region) Search(ipBytes []byte) (string, error) {\n\tif l := len(ipBytes); l == 4 {\n\t\treturn ip2r.v4Search(ipBytes)\n\t} else if l == 16 {\n\t\treturn ip2r.v6Search(ipBytes)\n\t} else {\n\t\treturn \"\", fmt.Errorf(\"invalid byte ip address with len=%d\", l)\n\t}\n}\n\nfunc (ip2r *Ip2Region) v4Search(ipBytes []byte) (string, error) {\n\tif ip2r.v4InMemSearcher != nil {\n\t\treturn ip2r.v4InMemSearcher.Search(ipBytes)\n\t}\n\n\t// v4 search is disabled\n\tif ip2r.v4Pool == nil {\n\t\treturn \"\", nil\n\t}\n\n\tv4Searcher := ip2r.v4Pool.BorrowSearcher()\n\tdefer ip2r.v4Pool.ReturnSearcher(v4Searcher)\n\treturn v4Searcher.Search(ipBytes)\n}\n\nfunc (ip2r *Ip2Region) v6Search(ipBytes []byte) (string, error) {\n\tif ip2r.v6InMemSearcher != nil {\n\t\treturn ip2r.v6InMemSearcher.Search(ipBytes)\n\t}\n\n\t// v6 search is disabled\n\tif ip2r.v6Pool == nil {\n\t\treturn \"\", nil\n\t}\n\n\tv6Searcher := ip2r.v6Pool.BorrowSearcher()\n\tdefer ip2r.v6Pool.ReturnSearcher(v6Searcher)\n\treturn v6Searcher.Search(ipBytes)\n}\n\nfunc (ip2r *Ip2Region) Close() {\n\tip2r.CloseTimeout(time.Second * 10)\n}\n\nfunc (ip2r *Ip2Region) CloseTimeout(d time.Duration) {\n\tif ip2r.v4Pool != nil {\n\t\tip2r.v4Pool.CloseTimeout(d)\n\t}\n\n\tif ip2r.v6Pool != nil {\n\t\tip2r.v6Pool.CloseTimeout(d)\n\t}\n}\n"
  },
  {
    "path": "binding/golang/service/ip2region_test.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n)\n\nfunc TestConfigCreate(t *testing.T) {\n\tv4Config, err := NewV4Config(VIndexCache, \"../../../data/ip2region_v4.xdb\", 10)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v4 config: %s\", err)\n\t}\n\n\tv6Config, err := NewV6Config(VIndexCache, \"../../../data/ip2region_v6.xdb\", 10)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v6 config: %s\", err)\n\t}\n\n\tip2region, err := NewIp2Region(v4Config, v6Config)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create ip2region service: %s\", err)\n\t}\n\n\tv4Bytes, err := xdb.ParseIP(\"219.133.110.197\")\n\tif err != nil {\n\t\tt.Fatal(\"invalid ipv4 address\")\n\t}\n\n\tv6Bytes, err := xdb.ParseIP(\"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\")\n\tif err != nil {\n\t\tt.Fatalf(\"invalid ipv6 address\")\n\t}\n\n\tfor i := 0; i < 20; i++ {\n\t\tv4Bytes = xdb.IPAddOne(v4Bytes)\n\t\tv6Bytes = xdb.IPAddOne(v6Bytes)\n\t\tv4Region, err := ip2region.Search(v4Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v4Bytes), err)\n\t\t}\n\n\t\tv6Region, err := ip2region.Search(v6Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v6Bytes), err)\n\t\t}\n\n\t\tfmt.Printf(\n\t\t\t\"%2d->search(%s)=%s, search(%s)=%s\\n\",\n\t\t\ti, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region,\n\t\t)\n\t}\n\n\tip2region.Close()\n\tfmt.Print(\"ip2region closed gracefully\")\n}\n\nfunc TestPathCreate(t *testing.T) {\n\tip2region, err := NewIp2RegionWithPath(\"../../../data/ip2region_v4.xdb\", \"../../../data/ip2region_v6.xdb\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create ip2region with path: %s\", err)\n\t}\n\n\tv4Bytes, err := xdb.ParseIP(\"219.133.110.197\")\n\tif err != nil {\n\t\tt.Fatal(\"invalid ipv4 address\")\n\t}\n\n\tv6Bytes, err := xdb.ParseIP(\"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\")\n\tif err != nil {\n\t\tt.Fatalf(\"invalid ipv6 address\")\n\t}\n\n\tfor i := 0; i < 20; i++ {\n\t\tv4Bytes = xdb.IPAddOne(v4Bytes)\n\t\tv6Bytes = xdb.IPAddOne(v6Bytes)\n\t\tv4Region, err := ip2region.Search(v4Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v4Bytes), err)\n\t\t}\n\n\t\tv6Region, err := ip2region.Search(v6Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v6Bytes), err)\n\t\t}\n\n\t\tfmt.Printf(\n\t\t\t\"%2d->search(%s)=%s, search(%s)=%s\\n\",\n\t\t\ti, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region,\n\t\t)\n\t}\n\n\tip2region.Close()\n\tfmt.Print(\"ip2region closed gracefully\")\n}\n\nfunc TestInMemSearch(t *testing.T) {\n\tv4Config, err := NewV4Config(BufferCache, \"../../../data/ip2region_v4.xdb\", 10)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v4 config: %s\", err)\n\t}\n\n\tv6Config, err := NewV6Config(BufferCache, \"../../../data/ip2region_v6.xdb\", 10)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v6 config: %s\", err)\n\t}\n\n\tip2region, err := NewIp2Region(v4Config, v6Config)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create ip2region service: %s\", err)\n\t}\n\n\tv4Bytes, err := xdb.ParseIP(\"219.133.110.197\")\n\tif err != nil {\n\t\tt.Fatal(\"invalid ipv4 address\")\n\t}\n\n\tv6Bytes, err := xdb.ParseIP(\"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\")\n\tif err != nil {\n\t\tt.Fatalf(\"invalid ipv6 address\")\n\t}\n\n\tfor i := 0; i < 20; i++ {\n\t\tv4Bytes = xdb.IPAddOne(v4Bytes)\n\t\tv6Bytes = xdb.IPAddOne(v6Bytes)\n\t\tv4Region, err := ip2region.Search(v4Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v4Bytes), err)\n\t\t}\n\n\t\tv6Region, err := ip2region.Search(v6Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v6Bytes), err)\n\t\t}\n\n\t\tfmt.Printf(\n\t\t\t\"%2d->search(%s)=%s, search(%s)=%s\\n\",\n\t\t\ti, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region,\n\t\t)\n\t}\n\n\tip2region.Close()\n\tfmt.Print(\"ip2region closed gracefully\")\n}\n\nfunc TestV4Only(t *testing.T) {\n\tv4Config, err := NewV4Config(NoCache, \"../../../data/ip2region_v4.xdb\", 10)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v4 config: %s\", err)\n\t}\n\n\tip2region, err := NewIp2Region(v4Config, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create ip2region service: %s\", err)\n\t}\n\n\tv4Bytes, err := xdb.ParseIP(\"219.133.110.197\")\n\tif err != nil {\n\t\tt.Fatal(\"invalid ipv4 address\")\n\t}\n\n\tv6Bytes, err := xdb.ParseIP(\"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\")\n\tif err != nil {\n\t\tt.Fatalf(\"invalid ipv6 address\")\n\t}\n\n\tfor i := 0; i < 10; i++ {\n\t\tv4Bytes = xdb.IPAddOne(v4Bytes)\n\t\tv6Bytes = xdb.IPAddOne(v6Bytes)\n\t\tv4Region, err := ip2region.Search(v4Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v4Bytes), err)\n\t\t}\n\n\t\tv6Region, err := ip2region.Search(v6Bytes)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", xdb.IP2String(v6Bytes), err)\n\t\t}\n\n\t\tfmt.Printf(\n\t\t\t\"%2d->search(%s)=%s, search(%s)=%s\\n\",\n\t\t\ti, xdb.IP2String(v4Bytes), v4Region, xdb.IP2String(v6Bytes), v6Region,\n\t\t)\n\t}\n\n\tip2region.Close()\n\tfmt.Print(\"ip2region closed gracefully\")\n}\n\nfunc TestConcurrentCall(t *testing.T) {\n\tv4Config, err := NewV4Config(VIndexCache, \"../../../data/ip2region_v4.xdb\", 15)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v4 config: %s\", err)\n\t}\n\n\tv6Config, err := NewV6Config(VIndexCache, \"../../../data/ip2region_v6.xdb\", 15)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create v6 config: %s\", err)\n\t}\n\n\tv4Bytes, err := xdb.ParseIP(\"219.133.110.197\")\n\tif err != nil {\n\t\tt.Fatal(\"invalid ipv4 address\")\n\t}\n\n\tv6Bytes, err := xdb.ParseIP(\"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\")\n\tif err != nil {\n\t\tt.Fatalf(\"invalid ipv6 address\")\n\t}\n\n\tfmt.Printf(\"v4Config: %s\\n\", v4Config)\n\tfmt.Printf(\"v6Config: %s\\n\", v6Config)\n\tip2region, err := NewIp2Region(v4Config, v6Config)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create ip2region service: %s\", err)\n\t}\n\n\tcoroutines := 100\n\tvar wg sync.WaitGroup\n\tvar count int64 = 0\n\ttStart := time.Now()\n\tfor i := 0; i < coroutines; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tvar ipBytes []byte\n\t\t\tfor i := 0; i < 5000; i++ {\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\tipBytes = v4Bytes\n\t\t\t\t} else {\n\t\t\t\t\tipBytes = v6Bytes\n\t\t\t\t}\n\n\t\t\t\tregion, err := ip2region.Search(ipBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"Error: failed to search(%s): %s\", xdb.IP2String(ipBytes), err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif l := len(ipBytes); l == 4 {\n\t\t\t\t\tif region != \"中国|广东省|深圳市|电信\" {\n\t\t\t\t\t\tfmt.Print(\"Error: region not equals\")\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif region != \"中国|广东省|深圳市|家庭宽带\" {\n\t\t\t\t\t\tfmt.Print(\"Error: region not equals\")\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tatomic.AddInt64(&count, 1)\n\t\t\t}\n\n\t\t\t// mark all searches finished for this coroutine\n\t\t\twg.Done()\n\t\t}()\n\t}\n\n\t// wait for all the searches to finished\n\twg.Wait()\n\tcosts := time.Since(tStart)\n\tfmt.Printf(\"%d searches finished in %s, avg took: %s\\n\", count, costs, costs/time.Duration(count))\n\tip2region.Close()\n\tfmt.Print(\"ip2region closed gracefully\")\n}\n"
  },
  {
    "path": "binding/golang/service/searcher_pool.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/binding/golang/xdb\"\n)\n\n// ---\n// ip2region searcher pool\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/12/03\n\ntype SearcherPool struct {\n\t// config\n\tconfig *Config\n\n\t// searcher pool\n\tpool chan *xdb.Searcher\n\n\t// for pool close\n\tclosing chan struct{}\n\n\t// searcher number that was loaned out\n\tloanCount int32\n}\n\nfunc NewSearcherPool(config *Config) (*SearcherPool, error) {\n\tif config.searchers < 1 {\n\t\treturn nil, fmt.Errorf(\"config.searchers must > 0\")\n\t}\n\n\tpool := make(chan *xdb.Searcher, config.searchers+1)\n\t// check and create all the searchers\n\tfor i := 0; i < config.searchers; i++ {\n\t\tsearcher, err := xdb.NewSearcher(config.ipVersion, config.xdbPath, config.vIndex, config.cBuffer)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create the %dth searcher: %w\", i+1, err)\n\t\t}\n\n\t\t// push the search to the pool\n\t\tpool <- searcher\n\t}\n\n\treturn &SearcherPool{\n\t\tconfig: config,\n\t\tpool:   pool,\n\n\t\tclosing:   make(chan struct{}, 1),\n\t\tloanCount: 0,\n\t}, nil\n}\n\n// get the loaned count\nfunc (sp *SearcherPool) LoanCount() int {\n\treturn int(atomic.LoadInt32(&sp.loanCount))\n}\n\nfunc (sp *SearcherPool) BorrowSearcher() *xdb.Searcher {\n\t// @Note: still accept searcher borrow while closing\n\ts := <-sp.pool\n\tatomic.AddInt32(&sp.loanCount, 1)\n\treturn s\n}\n\nfunc (sp *SearcherPool) ReturnSearcher(searcher *xdb.Searcher) {\n\tselect {\n\tcase <-sp.closing:\n\t\t// manually close the searcher\n\t\tsearcher.Close()\n\n\t\t// decrease the loan count\n\t\tatomic.AddInt32(&sp.loanCount, -1)\n\tdefault:\n\t\t// return the searcher\n\t\tsp.pool <- searcher\n\t\tatomic.AddInt32(&sp.loanCount, -1)\n\t}\n}\n\nfunc (sp *SearcherPool) Close() {\n\tsp.CloseTimeout(time.Second * 10)\n}\n\nfunc (sp *SearcherPool) CloseTimeout(d time.Duration) {\n\tclose(sp.closing)\n\tfor {\n\t\ttimeout := false\n\t\tselect {\n\t\tcase s := <-sp.pool:\n\t\t\ts.Close()\n\t\tcase <-time.After(d):\n\t\t\t// check if all the loaned searchers was closed\n\t\t\ttimeout = true\n\t\t}\n\n\t\tlc, left := sp.LoanCount(), len(sp.pool)\n\t\tif left == 0 && lc == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif timeout {\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "binding/golang/service/searcher_pool_test.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage service\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestV4SearcherPool(t *testing.T) {\n\tv4Config, err := NewV4Config(VIndexCache, \"../../../data/ip2region_v4.xdb\", 5)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to new v4 config: %s\", err)\n\t}\n\n\tsearcherPool, err := NewSearcherPool(v4Config)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create searcher pool: %s\", err)\n\t}\n\n\tipString := \"219.133.110.197\"\n\tfor i := 0; i < 20; i++ {\n\t\tsearcher := searcherPool.BorrowSearcher()\n\t\tregion, err := searcher.SearchByStr(ipString)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", ipString, err)\n\t\t}\n\n\t\tfmt.Printf(\"%2d->search(%s)=%s\\n\", i, ipString, region)\n\t\tsearcherPool.ReturnSearcher(searcher)\n\t}\n\n\t// borrow one at last for Close timeout wait testing ONLY\n\t// searcherPool.BorrowSearcher()\n\n\t// close the searcher pool\n\tsearcherPool.Close()\n}\n\nfunc TestV6SearcherPool(t *testing.T) {\n\tv6Config, err := NewV6Config(VIndexCache, \"../../../data/ip2region_v6.xdb\", 5)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to new v6 config: %s\", err)\n\t}\n\n\tsearcherPool, err := NewSearcherPool(v6Config)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create searcher pool: %s\", err)\n\t}\n\n\tipString := \"240e:3b7:3275:f090:d2a3:7d1a:dd90:c3b6\"\n\tfor i := 0; i < 20; i++ {\n\t\tsearcher := searcherPool.BorrowSearcher()\n\t\tregion, err := searcher.SearchByStr(ipString)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to search(%s): %s\", ipString, err)\n\t\t}\n\n\t\tfmt.Printf(\"%2d->search(%s)=%s\\n\", i, ipString, region)\n\t\tsearcherPool.ReturnSearcher(searcher)\n\t}\n\n\t// borrow one at last for Close timeout wait testing ONLY\n\t// searcherPool.BorrowSearcher()\n\n\t// close the searcher pool\n\tsearcherPool.Close()\n}\n"
  },
  {
    "path": "binding/golang/xdb/header.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// Ip2Region database v2.0 searcher.\n// @Note this is a Not thread safe implementation.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/12/03\n\npackage xdb\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\nconst (\n\tStructure20      = 2\n\tStructure30      = 3\n\tHeaderInfoLength = 256\n\tVectorIndexRows  = 256\n\tVectorIndexCols  = 256\n\tVectorIndexSize  = 8\n)\n\n// --- Index policy define\n\ntype IndexPolicy int\n\nconst (\n\tVectorIndexPolicy IndexPolicy = 1\n\tBTreeIndexPolicy  IndexPolicy = 2\n)\n\nfunc (i IndexPolicy) String() string {\n\tswitch i {\n\tcase VectorIndexPolicy:\n\t\treturn \"VectorIndex\"\n\tcase BTreeIndexPolicy:\n\t\treturn \"BtreeIndex\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// --- Header define\n\ntype Header struct {\n\t// data []byte\n\tVersion       uint16\n\tIndexPolicy   IndexPolicy\n\tCreatedAt     uint32\n\tStartIndexPtr uint32\n\tEndIndexPtr   uint32\n\n\t// since IPv6 supporting\n\tIPVersion       int\n\tRuntimePtrBytes int\n}\n\nfunc NewHeader(input []byte) (*Header, error) {\n\tif len(input) < 16 {\n\t\treturn nil, fmt.Errorf(\"invalid input buffer\")\n\t}\n\n\treturn &Header{\n\t\tVersion:       binary.LittleEndian.Uint16(input[0:]),\n\t\tIndexPolicy:   IndexPolicy(binary.LittleEndian.Uint16(input[2:])),\n\t\tCreatedAt:     binary.LittleEndian.Uint32(input[4:]),\n\t\tStartIndexPtr: binary.LittleEndian.Uint32(input[8:]),\n\t\tEndIndexPtr:   binary.LittleEndian.Uint32(input[12:]),\n\n\t\tIPVersion:       int(binary.LittleEndian.Uint16(input[16:])),\n\t\tRuntimePtrBytes: int(binary.LittleEndian.Uint16(input[18:])),\n\t}, nil\n}\n\nfunc (h *Header) String() string {\n\treturn fmt.Sprintf(\n\t\t\"{version:%d, index_policy:%d, created_at:%d, start_index_ptr:%d, end_index_ptr:%d, ip_version:%d, runtime_ptr_bytes:%d}\",\n\t\th.Version, h.IndexPolicy, h.CreatedAt, h.StartIndexPtr, h.EndIndexPtr, h.IPVersion, h.RuntimePtrBytes,\n\t)\n}\n"
  },
  {
    "path": "binding/golang/xdb/searcher.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// Ip2Region database v2.0 searcher.\n// @Note this is a Not thread safe implementation.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/16\n\npackage xdb\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n)\n\ntype Searcher struct {\n\tversion *Version\n\thandle  *os.File\n\n\tioCount int\n\n\t// use it only when this feature enabled.\n\t// Preload the vector index will reduce the number of IO operations\n\t// thus speedup the search process\n\tvectorIndex []byte\n\n\t// content buffer.\n\t// running with the whole xdb file cached\n\tcontentBuff []byte\n}\n\nfunc NewWithFileOnly(version *Version, dbFile string) (*Searcher, error) {\n\treturn NewSearcher(version, dbFile, nil, nil)\n}\n\nfunc NewWithVectorIndex(version *Version, dbFile string, vIndex []byte) (*Searcher, error) {\n\treturn NewSearcher(version, dbFile, vIndex, nil)\n}\n\nfunc NewWithBuffer(version *Version, cBuff []byte) (*Searcher, error) {\n\treturn NewSearcher(version, \"\", nil, cBuff)\n}\n\nfunc NewSearcher(version *Version, dbFile string, vIndex []byte, cBuff []byte) (*Searcher, error) {\n\tvar err error\n\n\t// content buff first\n\tif cBuff != nil {\n\t\treturn &Searcher{\n\t\t\tversion:     version,\n\t\t\tvectorIndex: nil,\n\t\t\tcontentBuff: cBuff,\n\t\t}, nil\n\t}\n\n\t// open the xdb binary file\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Searcher{\n\t\tversion:     version,\n\t\thandle:      handle,\n\t\tvectorIndex: vIndex,\n\t}, nil\n}\n\nfunc (s *Searcher) Close() {\n\tif s.handle != nil {\n\t\terr := s.handle.Close()\n\t\tif err != nil {\n\t\t\t// do error log here ?\n\t\t}\n\t}\n}\n\n// IPVersion return the ip version\nfunc (s *Searcher) IPVersion() *Version {\n\treturn s.version\n}\n\n// GetIOCount return the global io count for the last search\nfunc (s *Searcher) GetIOCount() int {\n\treturn s.ioCount\n}\n\n// SearchByStr find the region for the specified ip string\nfunc (s *Searcher) SearchByStr(str string) (string, error) {\n\tip, err := ParseIP(str)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn s.Search(ip)\n}\n\n// Search find the region for the specified long ip\nfunc (s *Searcher) Search(ip []byte) (string, error) {\n\t// ip version check\n\tif len(ip) != s.version.Bytes {\n\t\treturn \"\", fmt.Errorf(\"invalid ip address(%s expected)\", s.version.Name)\n\t}\n\n\t// reset the global ioCount\n\ts.ioCount = 0\n\n\t// locate the segment index block based on the vector index\n\tvar il0, il1 = int(ip[0]), int(ip[1])\n\tvar idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize\n\tvar sPtr, ePtr = uint32(0), uint32(0)\n\tif s.vectorIndex != nil {\n\t\tsPtr = binary.LittleEndian.Uint32(s.vectorIndex[idx:])\n\t\tePtr = binary.LittleEndian.Uint32(s.vectorIndex[idx+4:])\n\t} else if s.contentBuff != nil {\n\t\tsPtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx:])\n\t\tePtr = binary.LittleEndian.Uint32(s.contentBuff[HeaderInfoLength+idx+4:])\n\t} else {\n\t\t// read the vector index block\n\t\tvar buff = make([]byte, VectorIndexSize)\n\t\terr := s.read(int64(HeaderInfoLength+idx), buff)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"read vector index block at %d: %w\", HeaderInfoLength+idx, err)\n\t\t}\n\n\t\tsPtr = binary.LittleEndian.Uint32(buff)\n\t\tePtr = binary.LittleEndian.Uint32(buff[4:])\n\t}\n\n\t// fmt.Printf(\"sPtr=%d, ePtr=%d\\n\", sPtr, ePtr)\n\t// @Note: ptr validate, zero ptr means source data missing\n\t// so we could just stop here and return an empty string.\n\tif sPtr == 0 || ePtr == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// binary search the segment index to get the region\n\tvar bytes, dBytes = len(ip), len(ip) << 1\n\tvar segIndexSize = uint32(s.version.SegmentIndexSize)\n\tvar dataLen, dataPtr = 0, uint32(0)\n\tvar buff = make([]byte, segIndexSize)\n\tvar l, h = 0, int((ePtr - sPtr) / segIndexSize)\n\tfor l <= h {\n\t\tm := (l + h) >> 1\n\t\tp := sPtr + uint32(m)*segIndexSize\n\t\terr := s.read(int64(p), buff)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"read segment index at %d: %w\", p, err)\n\t\t}\n\n\t\t// decode the data step by step to reduce the unnecessary operations\n\t\tif s.version.IPCompare(ip, buff[0:bytes]) < 0 {\n\t\t\th = m - 1\n\t\t} else if s.version.IPCompare(ip, buff[bytes:dBytes]) > 0 {\n\t\t\tl = m + 1\n\t\t} else {\n\t\t\tdataLen = int(binary.LittleEndian.Uint16(buff[dBytes:]))\n\t\t\tdataPtr = binary.LittleEndian.Uint32(buff[dBytes+2:])\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// fmt.Printf(\"dataLen: %d, dataPtr: %d\\n\", dataLen, dataPtr)\n\tif dataLen == 0 {\n\t\treturn \"\", nil\n\t}\n\n\t// load and return the region data\n\tvar regionBuff = make([]byte, dataLen)\n\terr := s.read(int64(dataPtr), regionBuff)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"read region at %d: %w\", dataPtr, err)\n\t}\n\n\treturn string(regionBuff), nil\n}\n\n// do the data read operation based on the setting.\n// content buffer first or will read from the file.\n// this operation will invoke the Seek for file based read.\nfunc (s *Searcher) read(offset int64, buff []byte) error {\n\tif s.contentBuff != nil {\n\t\tcLen := copy(buff, s.contentBuff[offset:])\n\t\tif cLen != len(buff) {\n\t\t\treturn fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t\t}\n\t} else {\n\t\t_, err := s.handle.Seek(offset, 0)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"seek to %d: %w\", offset, err)\n\t\t}\n\n\t\ts.ioCount++\n\t\trLen, err := s.handle.Read(buff)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"handle read: %w\", err)\n\t\t}\n\n\t\tif rLen != len(buff) {\n\t\t\treturn fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "binding/golang/xdb/util.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/16\n\npackage xdb\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n)\n\nfunc ParseIP(ip string) ([]byte, error) {\n\tparsedIP := net.ParseIP(ip)\n\tif parsedIP == nil {\n\t\treturn nil, fmt.Errorf(\"invalid ip address: %s\", ip)\n\t}\n\n\tv4 := parsedIP.To4()\n\tif v4 != nil {\n\t\treturn v4, nil\n\t}\n\n\tv6 := parsedIP.To16()\n\tif v6 != nil {\n\t\treturn v6, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"invalid ip address: %s\", ip)\n}\n\nfunc IP2String(ip []byte) string {\n\treturn net.IP(ip[:]).String()\n}\n\n// IPCompare compares two IP addresses\n// Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\nfunc IPCompare(ip1, ip2 []byte) int {\n\t// for i := 0; i < len(ip1); i++ {\n\t// \tif ip1[i] < ip2[i] {\n\t// \t\treturn -1\n\t// \t}\n\t// \tif ip1[i] > ip2[i] {\n\t// \t\treturn 1\n\t// \t}\n\t// }\n\t// return 0\n\treturn bytes.Compare(ip1, ip2)\n}\n\nfunc IPAddOne(ip []byte) []byte {\n\tvar r = make([]byte, len(ip))\n\tcopy(r, ip)\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\tr[i]++\n\t\tif r[i] != 0 { // No overflow\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc IPSubOne(ip []byte) []byte {\n\tvar r = make([]byte, len(ip))\n\tcopy(r, ip)\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\tif r[i] != 0 { // No borrow needed\n\t\t\tr[i]--\n\t\t\tbreak\n\t\t}\n\t\tr[i] = 0xFF // borrow from the next byte\n\t}\n\n\treturn r\n}\n\n// Verify if the current Searcher could be used to search the specified xdb file.\n// Why do we need this check ?\n// The future features of the xdb impl may cause the current searcher not able to work properly.\n//\n// @Note: You Just need to check this ONCE when the service starts\n// Or use another process (eg, A command) to check once Just to confirm the suitability.\nfunc Verify(handle *os.File) error {\n\theader, err := LoadHeader(handle)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"loading header: %w\", err)\n\t}\n\n\t// get the runtime ptr bytes\n\truntimePtrBytes := 0\n\tswitch header.Version {\n\tcase Structure20:\n\t\truntimePtrBytes = 4\n\tcase Structure30:\n\t\truntimePtrBytes = header.RuntimePtrBytes\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid version: %d\", header.Version)\n\t}\n\n\t// 1, confirm the xdb file size.\n\t// to sure that the MaxFilePointer does no overflow\n\tstat, err := handle.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"file stat: %w\", err)\n\t}\n\n\tmaxFilePtr := int64(1<<(runtimePtrBytes*8) - 1)\n\tif stat.Size() > maxFilePtr {\n\t\treturn fmt.Errorf(\"xdb file exceeds the maximum supported bytes: %d\", maxFilePtr)\n\t}\n\n\treturn nil\n}\n\n// VerifyFromFile check Verify for details\nfunc VerifyFromFile(dbFile string) error {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open xdb file `%s`: %w\", dbFile, err)\n\t}\n\tdefer handle.Close()\n\n\treturn Verify(handle)\n}\n\n// LoadHeader load the header info from the specified handle\nfunc LoadHeader(handle *os.File) (*Header, error) {\n\t_, err := handle.Seek(0, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"seek to the header: %w\", err)\n\t}\n\n\tvar buff = make([]byte, HeaderInfoLength)\n\trLen, err := handle.Read(buff)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif rLen != len(buff) {\n\t\treturn nil, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t}\n\n\treturn NewHeader(buff)\n}\n\n// LoadHeaderFromFile load header info from the specified db file path\nfunc LoadHeaderFromFile(dbFile string) (*Header, error) {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open xdb file `%s`: %w\", dbFile, err)\n\t}\n\tdefer handle.Close()\n\n\theader, err := LoadHeader(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn header, nil\n}\n\n// LoadHeaderFromBuff wrap the header info from the content buffer\nfunc LoadHeaderFromBuff(cBuff []byte) (*Header, error) {\n\treturn NewHeader(cBuff[0:HeaderInfoLength])\n}\n\n// LoadVectorIndex util function to load the vector index from the specified file handle\nfunc LoadVectorIndex(handle *os.File) ([]byte, error) {\n\t// load all the vector index block\n\t_, err := handle.Seek(HeaderInfoLength, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"seek to vector index: %w\", err)\n\t}\n\n\tvar buff = make([]byte, VectorIndexRows*VectorIndexCols*VectorIndexSize)\n\trLen, err := handle.Read(buff)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif rLen != len(buff) {\n\t\treturn nil, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t}\n\n\treturn buff, nil\n}\n\n// LoadVectorIndexFromFile load vector index from a specified file path\nfunc LoadVectorIndexFromFile(dbFile string) ([]byte, error) {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open xdb file `%s`: %w\", dbFile, err)\n\t}\n\tdefer handle.Close()\n\n\tvIndex, err := LoadVectorIndex(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn vIndex, nil\n}\n\n// LoadContent load the whole xdb content from the specified file handle\nfunc LoadContent(handle *os.File) ([]byte, error) {\n\t// get file size\n\tfi, err := handle.Stat()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"stat: %w\", err)\n\t}\n\n\tsize := fi.Size()\n\n\t// seek to the head of the file\n\t_, err = handle.Seek(0, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"seek to get xdb file length: %w\", err)\n\t}\n\n\tvar buff = make([]byte, size)\n\trLen, err := handle.Read(buff)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif rLen != len(buff) {\n\t\treturn nil, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t}\n\n\treturn buff, nil\n}\n\n// LoadContentFromFile load the whole xdb content from the specified db file path\nfunc LoadContentFromFile(dbFile string) ([]byte, error) {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open xdb file `%s`: %w\", dbFile, err)\n\t}\n\tdefer handle.Close()\n\n\tcBuff, err := LoadContent(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cBuff, nil\n}\n\n// LoadContentFromFS load the whole xdb binary from embed.FS\nfunc LoadContentFromFS(fs embed.FS, filePath string) ([]byte, error) {\n\tfile, err := fs.Open(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open embedded file `%s`: %w\", filePath, err)\n\t}\n\tdefer file.Close()\n\n\tvar cBuff []byte\n\tcBuff, err = io.ReadAll(file)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read embedded file `%s`: %w\", filePath, err)\n\t}\n\n\treturn cBuff, nil\n}\n"
  },
  {
    "path": "binding/golang/xdb/util_test.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/16\n\npackage xdb\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestParseIP(t *testing.T) {\n\tvar ips = []string{\"29.34.191.255\", \"2c0f:fff0::\", \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"}\n\tfor _, ip := range ips {\n\t\tbytes, err := ParseIP(ip)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"check ip `%s`: %s\\n\", IP2String(bytes), err)\n\t\t}\n\n\t\tnip := IP2String(bytes)\n\t\tfmt.Printf(\"checkip: (%s / %s), isEqual: %v\\n\", ip, nip, ip == nip)\n\t}\n}\n\nfunc TestIPCompare(t *testing.T) {\n\tvar ipPairs = [][]string{\n\t\t{\"1.2.3.4\", \"1.2.3.5\"},\n\t\t{\"58.250.36.41\", \"58.250.30.41\"},\n\t\t{\"2c10::\", \"2e00::\"},\n\t\t{\"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"},\n\t\t{\"fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"fe00::\"},\n\t}\n\n\tfor _, pairs := range ipPairs {\n\t\tfmt.Printf(\"IPCompare(%s, %s): %d\\n\", pairs[0], pairs[1], IPCompare([]byte(pairs[0]), []byte(pairs[1])))\n\t}\n}\n\nfunc TestLoadVectorIndex(t *testing.T) {\n\tvIndex, err := LoadVectorIndexFromFile(\"../../../data/ip2region_v4.xdb\")\n\tif err != nil {\n\t\tfmt.Printf(\"failed to load vector index: %s\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"vIndex length: %d\\n\", len(vIndex))\n}\n\nfunc TestLoadContent(t *testing.T) {\n\tbuff, err := LoadContentFromFile(\"../../../data/ip2region_v4.xdb\")\n\tif err != nil {\n\t\tfmt.Printf(\"failed to load xdb content: %s\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"buff length: %d\\n\", len(buff))\n}\n\nfunc TestLoadHeader(t *testing.T) {\n\theader, err := LoadHeaderFromFile(\"../../../data/ip2region_v4.xdb\")\n\tif err != nil {\n\t\tfmt.Printf(\"failed to load xdb header info: %s\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"Version         : %d\\n\", header.Version)\n\tfmt.Printf(\"IndexPolicy     : %s\\n\", header.IndexPolicy.String())\n\tfmt.Printf(\"CreatedAt       : %d(%s)\\n\", header.CreatedAt, time.Unix(int64(header.CreatedAt), 0).Format(time.RFC3339))\n\tfmt.Printf(\"StartIndexPtr   : %d\\n\", header.StartIndexPtr)\n\tfmt.Printf(\"EndIndexPtr     : %d\\n\", header.EndIndexPtr)\n\tfmt.Printf(\"IPVersion       : %d\\n\", header.IPVersion)\n\tfmt.Printf(\"RuntimePtrBytes : %d\\n\", header.RuntimePtrBytes)\n}\n"
  },
  {
    "path": "binding/golang/xdb/version.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Version struct {\n\tId               int\n\tName             string\n\tBytes            int\n\tSegmentIndexSize int\n\n\t// function to compare two ips\n\tIPCompare func([]byte, []byte) int\n}\n\nfunc (v *Version) String() string {\n\treturn fmt.Sprintf(\n\t\t\"{id:%d, name:%s, bytes:%d, segment_index_size:%d}\",\n\t\tv.Id, v.Name, v.Bytes, v.SegmentIndexSize,\n\t)\n}\n\nconst (\n\tIPv4VersionNo = 4\n\tIPv6VersionNo = 6\n)\n\nvar (\n\tIPvx = &Version{}\n\tIPv4 = &Version{\n\t\tId:               IPv4VersionNo,\n\t\tName:             \"IPv4\",\n\t\tBytes:            4,\n\t\tSegmentIndexSize: 14, // 4 + 4 + 2 + 4,\n\t\tIPCompare: func(ip1, ip2 []byte) int {\n\t\t\t// ip1 - with Big endian byte order parsed from an input\n\t\t\t// ip2 - with Little endian byte order read from the xdb index\n\t\t\tip2[0], ip2[3] = ip2[3], ip2[0]\n\t\t\tip2[1], ip2[2] = ip2[2], ip2[1]\n\t\t\treturn bytes.Compare(ip1, ip2)\n\t\t},\n\t}\n\tIPv6 = &Version{\n\t\tId:               IPv6VersionNo,\n\t\tName:             \"IPv6\",\n\t\tBytes:            16,\n\t\tSegmentIndexSize: 38, // 16 + 16 + 2 + 4,\n\t\tIPCompare: func(ip1, ip2 []byte) int {\n\t\t\treturn bytes.Compare(ip1, ip2)\n\t\t},\n\t}\n)\n\nfunc VersionFromIP(ip string) (*Version, error) {\n\tr, err := ParseIP(ip)\n\tif err != nil {\n\t\treturn IPvx, fmt.Errorf(\"parse ip fail: %w\", err)\n\t}\n\n\tif len(r) == 4 {\n\t\treturn IPv4, nil\n\t}\n\n\treturn IPv6, nil\n}\n\nfunc VersionFromName(name string) (*Version, error) {\n\tswitch strings.ToUpper(name) {\n\tcase \"V4\", \"IPV4\":\n\t\treturn IPv4, nil\n\tcase \"V6\", \"IPV6\":\n\t\treturn IPv6, nil\n\tdefault:\n\t\treturn IPvx, fmt.Errorf(\"invalid version name `%s`\", name)\n\t}\n}\n\nfunc VersionFromHeader(header *Header) (*Version, error) {\n\t// old structure with IPv4 supports ONLY\n\tif header.Version == Structure20 {\n\t\treturn IPv4, nil\n\t}\n\n\t// structure 3.0 after IPv6 supporting\n\tif header.Version != Structure30 {\n\t\treturn IPvx, fmt.Errorf(\"invalid version `%d`\", header.IPVersion)\n\t}\n\n\tswitch header.IPVersion {\n\tcase IPv4VersionNo:\n\t\treturn IPv4, nil\n\tcase IPv6VersionNo:\n\t\treturn IPv6, nil\n\tdefault:\n\t\treturn IPvx, fmt.Errorf(\"invalid version `%d`\", header.Version)\n\t}\n}\n"
  },
  {
    "path": "binding/java/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region java Query Client\n\n# Usage\n\n### Maven Repository:\n\n```xml\n<dependency>\n    <groupId>org.lionsoul</groupId>\n    <artifactId>ip2region</artifactId>\n    <version>3.3.6</version>\n</dependency>\n```\n\n### About Query Service\n\nStarting from version `3.2.0`, a dual-protocol compatible and concurrency-safe `Ip2Region` query service is provided. **It is recommended to prioritize this method for query calls.** The specific usage is as follows:\n\n```java\nimport org.lionsoul.ip2region.service.Config;\nimport org.lionsoul.ip2region.service.Ip2Region;\n\n// 1. Create v4 configuration: specify cache policy and v4 xdb file path\nfinal Config v4Config = Config.custom()\n    .setCachePolicy(Config.VIndexCache)     // Specify cache policy: NoCache / VIndexCache / BufferCache\n    .setSearchers(15)                       // Set the number of initialized searchers\n    // .setCacheSliceBytes(int)             // Set cache slice bytes, default is 50MiB\n    // .setXdbInputStream(InputStream)      // Set v4 xdb file inputstream object\n    // .setXdbFile(File)                    // Set v4 xdb File object\n    .setXdbPath(\"ip2region v4 xdb path\")    // Set the path of v4 xdb file\n    .asV4();    // Specify as v4 configuration\n\n// 2. Create v6 configuration: specify cache policy and v6 xdb file path\nfinal Config v6Config = Config.custom()\n    .setCachePolicy(Config.VIndexCache)     // Specify cache policy: NoCache / VIndexCache / BufferCache\n    .setSearchers(15)                       // Set the number of initialized searchers\n    // .setCacheSliceBytes(int)             // Set cache slice bytes, default is 50MiB\n    // .setXdbInputStream(InputStream)      // Set v6 xdb file inputstream object\n    // .setXdbFile(File)                    // Set v6 xdb File object\n    .setXdbPath(\"ip2region v6 xdb path\")    // Set the path of v6 xdb file\n    .asV6();    // Specify as v6 configuration\n\n// Note: Priority for the three types of Xdb initialization inputs: XdbInputStream -> XdbFile -> XdbPath\n// setXdbInputStream is only for the convenience of users to load xdb file content from jar packages, in which case cachePolicy can only be set to Config.BufferCache\n\n// 3. Create Ip2Region query service through the above configurations\nfinal Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);\n\n// 4. Export the ip2region service as a global variable to perform concurrent queries for both versions of IP addresses, for example:\nfinal String v4Region = ip2Region.search(\"113.92.157.29\");                          // Perform IPv4 query\nfinal String v6Region = ip2Region.search(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"); // Perform IPv6 query\n\n// 5. When the service needs to be shut down, close the ip2region query service at the same time\n// Note: The close method only needs to be called when the entire service is shut down; no operation is needed during queries\nip2Region.close();\n```\n\n##### `Ip2Region` Query Notes:\n\n1. The API of this query service is concurrency-safe and supports both `IPv4` and `IPv6` addresses; the internal implementation will automatically distinguish them.\n2. v4 and v6 configurations need to be created separately. You can set different cache policies for v4 and v6, or specify one of them as `null`, in which case IP address queries for that version will return `null`.\n3. Please set a suitable number of searchers for `setSearchers` based on your project's concurrency. The default is 20. This value is fixed during runtime. Each query will borrow a searcher from the pool and return it after the query is completed. If the pool is empty when borrowing, it will wait until a searcher becomes available. The borrow lock is managed using `ReentrantLock`. You can also set the `Ip2Region` query service to use a fair lock as follows:\n\n```java\nfinal Ip2Region ip2region = Ip2Region.create(v4Config, v6Config, true);\n```\n\n4. If the cache policy in the configuration is set to `Config.BufferCache` (i.e., `Full Memory Cache`), a single-instance memory searcher will be used by default. This implementation is natively concurrency-safe, and the number of searchers specified via `setSearchers` will be ignored.\n5. If `close` is called while the `ip2region` searcher is providing service, it will wait for a maximum of 10 seconds by default to allow as many searchers as possible to be returned.\n\n### About Query API\n\nThe prototype of the location information query API is:\n\n```java\nString search(String ipStr) throw Exception;\nString search(byte[] ip) throw Exception;\n```\n\nAn exception will be thrown if the query fails. If the query is successful, the `region` information string will be returned. If the specified IP cannot be found, an empty string `\"\"` will be returned, which may occur for custom data or incomplete data.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. The usage is as follows:\n\n```java\nimport org.lionsoul.ip2region.xdb.Version;\n\n// For IPv4: Set xdb path to the v4 xdb file, specify IP version as Version.IPv4\nfinal String dbPath = \"../../data/ip2region_v4.xdb\";  // or your ipv4 xdb path\nfinal Version version = Version.IPv4;\n\n// For IPv6: Set xdb path to the v6 xdb file, specify IP version as Version.IPv6\nfinal String dbPath = \"../../data/ip2region_v6.xdb\";  // or your ipv6 xdb path\nfinal Version version = Version.IPv6;\n\n// The IP version of the xdb specified by dbPath must be consistent with version, otherwise an error will occur during query execution\n// Note: The following demonstration directly uses the dbPath and version variables\n```\n\n### File Verification\n\nIt is recommended that you proactively verify the applicability of the xdb file, as some future new features may cause the current Searcher version to be incompatible with the xdb file you are using. Verification can avoid unpredictable errors during runtime. You do not need to verify every time; for example, verify when the service starts or manually call a command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```java\ntry {\n    Searcher.verifyFromFile(dbPath);\n} catch (Exception e) {\n    // Applicability verification failed!!!\n    // The current query client implementation is not suitable for querying the xdb file specified by dbPath.\n    // You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with dbPath.\n    return;\n}\n\n// Verification passed, the current Searcher can be safely used for query operations on the xdb pointed to by dbPath\n```\n\n### File-Based Query\n\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // 1. Create a searcher object using the version and dbPath mentioned above\n        Searcher searcher = null;\n        try {\n            searcher = Searcher.newWithFileOnly(version, dbPath);\n        } catch (IOException e) {\n            System.out.printf(\"failed to create searcher with `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2. Query, both IPv4 and IPv6 addresses are supported\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n\n        // 3. Close resources\n        searcher.close();\n        \n        // Note: For concurrent use, each thread needs to create an independent searcher object for separate use.\n    }\n}\n```\n\n### Caching `VectorIndex`\n\nWe can pre-load `VectorIndex` data from the `xdb` file and cache it globally. Using the global VectorIndex cache every time a Searcher object is created can reduce a fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // Note: For version and dbPath sources, please see the version description above\n\n        // 1. Pre-load VectorIndex cache from dbPath and use the obtained data as a global variable for subsequent repeated use.\n        byte[] vIndex;\n        try {\n            vIndex = Searcher.loadVectorIndexFromFile(dbPath);\n        } catch (Exception e) {\n            System.out.printf(\"failed to load vector index from `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2. Use the global vIndex to create a query object with VectorIndex cache.\n        Searcher searcher;\n        try {\n            searcher = Searcher.newWithVectorIndex(version, dbPath, vIndex);\n        } catch (Exception e) {\n            System.out.printf(\"failed to create vectorIndex cached searcher with `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 3. Query, both IPv4 and IPv6 addresses are supported\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n        \n        // 4. Close resources\n        searcher.close();\n\n        // Note: Each thread needs to create an independent Searcher object, but they all share the same read-only global vIndex cache.\n    }\n}\n```\n\n### Caching the Entire `xdb` File\n\nWe can also pre-load the entire xdb file data into memory and then create a query object based on this data to achieve a fully memory-based query, similar to the previous memory search.\n\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // Note: For version and dbPath sources, please see the version description above\n\n        // 1. Load the entire xdb from dbPath into memory.\n        // Starting from this release version, the xdb buffer uses LongByteArray for storage to avoid int type overflow when the xdb file is too large\n        LongByteArray cBuff;\n        try {\n            cBuff = Searcher.loadContentFromFile(dbPath);\n        } catch (Exception e) {\n            System.out.printf(\"failed to load content from `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2. Use the above cBuff to create a fully memory-based query object.\n        Searcher searcher;\n        try {\n            searcher = Searcher.newWithBuffer(version, cBuff);\n        } catch (Exception e) {\n            System.out.printf(\"failed to create content cached searcher: %s\\n\", e);\n            return;\n        }\n\n        // 3. Query, both IPv4 and IPv6 are supported\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n        \n        // 4. Close resources - this searcher object can be safely used for concurrency; close it when the entire service is shut down\n        // searcher.close();\n\n        // Note: For concurrent use, the query object created with the entire xdb data cache can be safely used for concurrency, \n        // meaning you can make this searcher object a global object for cross-thread access.\n    }\n}\n```\n\nIf an OOM error occurs while calling the `loadContentXXX` method to load the xdb buffer, please refer to the [sliceBytes setting](#slicebytes) below and choose the `loadContentXXX` method with the sliceBytes parameter.\n\n### sliceBytes\n\nsliceBytes represents the size of the partitioned memory for the `List<byte[]> buffs` collection maintained inside the `LongByteArray` class during full memory caching. The default value is `Searcher.DEFAULT_SLICE_BYTES` = `50MiB`. The maximum allowed value is `Searcher.MAX_WRITE_BYTES` = `0x7ffff000`. For the source of this value, please refer to the author's blog post: [https://mp.weixin.qq.com/s/4xHRcnQbIcjtMGdXEGrxsA](https://mp.weixin.qq.com/s/4xHRcnQbIcjtMGdXEGrxsA).\n\n1. Starting from version `3.3.3`, `LongByteArray` implements fixed partition size support, which allows for fast `offset` positioning through simple calculation to perform `slice` or `copy` operations.\n2. In terms of calculation speed, the larger the sliceBytes, the smaller the length of buffs and the lower the calculation time. However, with the fixed sliceBytes implementation, this gap is completely negligible. Therefore, it is recommended to keep the default value of `50MiB`, which also avoids the OOM issues that could be caused by elastic partition sizes previously.\n\n# Compiling the Test Program\n\nCompile the test program via Maven.\n\n```bash\n# cd to the root directory of java binding\ncd binding/java/\nmvn compile package\n```\n\nThen a packaged file named ip2region-{version}.jar will be generated in the target directory under the current folder.\n\n# Query Testing\n\n### Test Command\n\nYou can test queries via the `java -jar target/ip2region-{version}.jar search` command:\n\n```bash\n➜  java git:(master) ✗ java -jar target/ip2region-3.3.4.jar search --help\njava -jar ip2region-{version}.jar search [command options]\noptions:\n --v4-db string            ip2region ipv4 binary xdb file path\n --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\n --v6-db string            ip2region ipv6 binary xdb file path\n --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\n --help                    print this help menu\n```\n\n### Parameter Parsing\n\n1. `v4-xdb`: IPv4 xdb file path, defaults to data/ip2region_v4.xdb in the repository.\n2. `v6-xdb`: IPv6 xdb file path, defaults to data/ip2region_v6.xdb in the repository.\n3. `v4-cache-policy`: Cache policy used for v4 queries, default is `vectorIndex`, options: file/vectorIndex/content.\n4. `v6-cache-policy`: Cache policy used for v6 queries, default is `vectorIndex`, options: file/vectorIndex/content.\n\n### Test Demo\n\nExample: performing query testing using default data/ip2region_v4.xdb and data/ip2region_v6.xdb:\n\n```bash\n➜  java git:(java_app_with_ip2region_service) ✗ java -jar target/ip2region-3.3.4.jar search\nip2region search service test program\n+-v4 xdb: /data01/code/c/ip2region/data/ip2region_v4.xdb (vectorIndex)\n+-v6 xdb: /data01/code/c/ip2region/data/ip2region_v6.xdb (vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, took: 140 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, took: 391 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, took: 503 μs}\n```\n\nEnter a v4 or v6 IP address to perform a query test. You can also set `cache-policy` to file/vectorIndex/content respectively to test the effects of the three different cache implementations.\n\n# bench Testing\n\n### Test Command\n\nYou can perform bench testing via the `java -jar ip2region-{version}.jar bench` command to ensure the `xdb` file is error-free and to evaluate query performance:\n\n```bash\n➜  java git:(fr_java_ipv6) ✗ java -jar target/ip2region-3.3.4.jar bench\njava -jar ip2region-{version}.jar bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n### v4 bench\n\nExample: IPv4 bench testing using default data/ip2region_v4.xdb and data/ipv4_source.txt files:\n\n```bash\njava -jar target/ip2region-3.3.4.jar bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n### v6 bench\n\nExample: IPv6 bench testing using default data/ip2region_v6.xdb and data/ipv6_source.txt files:\n\n```bash\njava -jar target/ip2region-3.3.4.jar bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\nYou can test the effects of the three different cache implementations by setting `cache-policy` to file/vectorIndex/content.\n@Note: Please ensure that the src file used for benching is the same source file used to generate the corresponding xdb file.\n"
  },
  {
    "path": "binding/java/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region java 查询客户端\n\n# 使用方式\n\n### maven 仓库：\n```xml\n<dependency>\n    <groupId>org.lionsoul</groupId>\n    <artifactId>ip2region</artifactId>\n    <version>3.3.6</version>\n</dependency>\n```\n\n### 关于查询服务\n从 `3.2.0` 版本开始提供了一个双协议兼容且并发安全的 `Ip2Region` 查询服务，**建议优先使用该方式来进行查询调用**，具体使用方式如下：\n```java\nimport org.lionsoul.ip2region.service.Config;\nimport org.lionsoul.ip2region.service.Ip2Region;\n\n// 1, 创建 v4 的配置：指定缓存策略和 v4 的 xdb 文件路径\nfinal Config v4Config = Config.custom()\n    .setCachePolicy(Config.VIndexCache)     // 指定缓存策略:  NoCache / VIndexCache / BufferCache\n    .setSearchers(15)                       // 设置初始化的查询器数量\n    // .setCacheSliceBytes(int)             // 设置缓存的分片字节数，默认为 50MiB\n    // .setXdbInputStream(InputStream)      // 设置 v4 xdb 文件的 inputstream 对象\n    // .setXdbFile(File)                    // 设置 v4 xdb File 对象\n    .setXdbPath(\"ip2region v4 xdb path\")    // 设置 v4 xdb 文件的路径\n    .asV4();    // 指定为 v4 配置\n\n// 2, 创建 v6 的配置：指定缓存策略和 v6 的 xdb 文件路径\nfinal Config v6Config = Config.custom()\n    .setCachePolicy(Config.VIndexCache)     // 指定缓存策略: NoCache / VIndexCache / BufferCache\n    .setSearchers(15)                       // 设置初始化的查询器数量\n    // .setCacheSliceBytes(int)             // 设置缓存的分片字节数，默认为 50MiB\n    // .setXdbInputStream(InputStream)      // 设置 v6 xdb 文件的 inputstream 对象\n    // .setXdbFile(File)                    // 设置 v6 xdb File 对象\n    .setXdbPath(\"ip2region v6 xdb path\")    // 设置 v6 xdb 文件的路径\n    .asV6();    // 指定为 v6 配置\n\n// 备注：Xdb 三种初始化输入的优先级：XdbInputStream -> XdbFile -> XdbPath\n// setXdbInputStream 仅方便使用者从 jar 包中加载 xdb 文件内容，这时 cachePolicy 只能设置为 Config.BufferCache\n\n// 3，通过上述配置创建 Ip2Region 查询服务\nfinal Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);\n\n// 4，导出 ip2region 服务作为全局变量，进行双版本的IP地址的并发查询，例如：\nfinal String v4Region = ip2Region.search(\"113.92.157.29\");                          // 进行 IPv4 查询\nfinal String v6Region = ip2Region.search(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"); // 进行 IPv6 查询\n\n// 5，在服务需要关闭的时候，同时关闭 ip2region 查询服务\n// 备注：close 方法只需要在整个服务关闭的时候关闭，查询途中不需要操作\nip2Region.close();\n```\n##### `Ip2Region` 查询备注：\n1. 该查询服务的 API 并发安全且同时支持 `IPv4` 和 `IPv6` 的地址，内部实现会自动判断。\n2. v4 和 v6 的配置需要单独创建，可以给 v4 和 v6 设置使用不同的缓存策略，也可以指定其中一个为 `null` 则该版本的 IP 地址查询都会返回 `null`。\n3. 请结合您项目的并发数给 `setSearchers` 一个合适的查询器数量，默认为 20 个，这个值在运行过程中是固定的，每次查询会从池子里租借一个查询器来完成查询操作，查询完成后再归还回去，如果租借的时候池子已经空了则等待直到有可用的查询器来完成查询服务，租借的锁是使用的 `ReentrantLock` 来管理，也可以通过如下方式来设置 `Ip2Region` 查询服务使用公平锁：\n```java\nfinal Ip2Region ip2region = Ip2Region.create(v4Config, v6Config, true);\n```\n4. 如果配置设置的缓存策略为 `Config.BufferCache` 即 `全内存缓存` 则默认会使用单实例的内存查询器，该实现天生并发安全，此时通过 `setSearchers` 指定的查询器数量无效。\n5. 如果 `ip2region` 查询器在提供服务期间，调用 close 默认会最大等待 10 秒钟来等待尽量多的查询器归还。\n\n\n### 关于查询 API\n定位信息查询 API 的原型为：\n```java\nString search(String ipStr) throw Exception;\nString search(byte[] ip) throw Exception;\n```\n查询出错会抛出异常，如果查询成功会返回字符串的 `region` 信息，如果指定的 ip 查询不到会返回空字符串 `\"\"`，这对于自定义数据或者数据不完整的情况会出现。\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```java\nimport org.lionsoul.ip2region.xdb.Version;\n\n// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 Version.IPv4\nfinal String dbPath = \"../../data/ip2region_v4.xdb\";  // 或者你的 ipv4 xdb 的路径\nfinal Version version = Version.IPv4;\n\n// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 Version.IPv6\nfinal String dbPath = \"../../data/ip2region_v6.xdb\";  // 或者你的 ipv6 xdb 路径\nfinal Version version = Version.IPv6;\n\n// dbPath 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n// 备注：以下演示直接使用 dbPath 和 version 变量\n```\n\n### 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```java\ntry {\n    Searcher.verifyFromFile(dbPath);\n} catch (Exception e) {\n    // 适用性验证失败！！！\n    // 当前查询客户端实现不适用于 dbPath 指定的 xdb 文件的查询.\n    // 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 dbPath 的 Searcher 实现。\n    return;\n}\n\n// 验证通过，当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // 1、使用上述的 version 和 dbPath 创建 searcher 对象\n        Searcher searcher = null;\n        try {\n            searcher = Searcher.newWithFileOnly(version, dbPath);\n        } catch (IOException e) {\n            System.out.printf(\"failed to create searcher with `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2、查询，IPv4 或者 IPv6 的地址都支持\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n\n        // 3、关闭资源\n        searcher.close();\n        \n        // 备注：并发使用，每个线程需要创建一个独立的 searcher 对象单独使用。\n    }\n}\n```\n\n### 缓存 `VectorIndex` 索引\n\n我们可以提前从 `xdb` 文件中加载出来 `VectorIndex` 数据，然后全局缓存，每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作，从而加速查询，减少 IO 压力。\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // 备注：version 和 dbPath 来源，请看上面的版本描述\n\n        // 1、从 dbPath 中预先加载 VectorIndex 缓存，并且把这个得到的数据作为全局变量，后续反复使用。\n        byte[] vIndex;\n        try {\n            vIndex = Searcher.loadVectorIndexFromFile(dbPath);\n        } catch (Exception e) {\n            System.out.printf(\"failed to load vector index from `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。\n        Searcher searcher;\n        try {\n            searcher = Searcher.newWithVectorIndex(version, dbPath, vIndex);\n        } catch (Exception e) {\n            System.out.printf(\"failed to create vectorIndex cached searcher with `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 3、查询，IPv4 或者 IPv6 地址都支持\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n        \n        // 4、关闭资源\n        searcher.close();\n\n        // 备注：每个线程需要单独创建一个独立的 Searcher 对象，但是都共享全局的只读 vIndex 缓存。\n    }\n}\n```\n\n### 缓存整个 `xdb` 文件\n\n我们也可以预先加载整个 xdb 文件的数据到内存，然后基于这个数据创建查询对象来实现完全基于文件的查询，类似之前的 memory search。\n```java\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport java.io.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n    public static void main(String[] args) {\n        // 备注：version 和 dbPath 来源，请看上面的版本描述\n\n        // 1、从 dbPath 加载整个 xdb 到内存。\n        // 从这个 release 版本开始，xdb 的 buffer 使用 LongByteArray 来存储，避免 xdb 文件过大的时候 int 类型的溢出\n        LongByteArray cBuff;\n        try {\n            cBuff = Searcher.loadContentFromFile(dbPath);\n        } catch (Exception e) {\n            System.out.printf(\"failed to load content from `%s`: %s\\n\", dbPath, e);\n            return;\n        }\n\n        // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。\n        Searcher searcher;\n        try {\n            searcher = Searcher.newWithBuffer(version, cBuff);\n        } catch (Exception e) {\n            System.out.printf(\"failed to create content cached searcher: %s\\n\", e);\n            return;\n        }\n\n        // 3、查询，IPv4 和 IPv6 都支持\n        try {\n            String ip = \"1.2.3.4\";\n            // ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\n            long sTime = System.nanoTime();\n            String region = searcher.search(ip);\n            long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n            System.out.printf(\"{region: %s, ioCount: %d, took: %d μs}\\n\", region, searcher.getIOCount(), cost);\n        } catch (Exception e) {\n            System.out.printf(\"failed to search(%s): %s\\n\", ip, e);\n        }\n        \n        // 4、关闭资源 - 该 searcher 对象可以安全用于并发，等整个服务关闭的时候再关闭 searcher\n        // searcher.close();\n\n        // 备注：并发使用，用整个 xdb 数据缓存创建的查询对象可以安全的用于并发，也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。\n    }\n}\n```\n\n如果调用 `loadContentXXX` 方法来加载 xdb buffer 的过程中出现了 OOM 错误，请参考以下的 [sliceBytes 设置](#slicebytes)，选择使用带 sliceBytes 参数的 `loadContentXXX` 方法来加载 。\n\n### sliceBytes\n\nsliceBytes 表示 xdb 全内存缓存时 `LongByteArray` 类内部维护的 `List<byte[]> buffs` 集合的分片内存的大小，默认值为 `Searcher.DEFAULT_SLICE_BYTES` = `50MiB`，这个值的最大允许值为 `Searcher.MAX_WRITE_BYTES` = `0x7ffff000`，关于该取值的来源可以参考作者博客文章：[https://mp.weixin.qq.com/s/4xHRcnQbIcjtMGdXEGrxsA](https://mp.weixin.qq.com/s/4xHRcnQbIcjtMGdXEGrxsA)。\n1. 从 `3.3.3` 版本开始 `LongByteArray` 实现了固定分片尺寸支持，可以通过简单的计算快速的完成 `offset` 定位的从而实现 `slice` 或者 `copy` 操作。\n2. 从计算速度来说 sliceBytes 越大 buffs 的长度越小，计算耗时越小，不过有了固定 sliceBytes 实现这个差距完全可以忽略，所以建议保持默认值为 `50MiB` 即可，也不会出现之前弹性分片尺寸可能导致的 OOM 问题。\n\n\n\n# 编译测试程序\n\n通过 maven 来编译测试程序。\n```bash\n# cd 到 java binding 的根目录\ncd binding/java/\nmvn compile package\n```\n\n然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。\n\n\n\n# 查询测试\n\n### 测试命令\n可以通过 `java -jar target/ip2region-{version}.jar search` 命令来测试查询：\n```bash\n➜  java git:(master) ✗ java -jar target/ip2region-3.3.0.jar search --help\njava -jar ip2region-{version}.jar search [command options]\noptions:\n --v4-db string            ip2region ipv4 binary xdb file path\n --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\n --v6-db string            ip2region ipv6 binary xdb file path\n --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\n --help                    print this help menu\n```\n\n### 参数解析\n1. `v4-xdb`: IPv4 的 xdb 文件路径，默认为仓库中的 data/ip2region_v4.xdb\n2. `v6-xdb`: IPv6 的 xdb 文件路径，默认为仓库中的 data/ip2region_v6.xdb\n3. `v4-cache-policy`: v4 查询使用的缓存策略，默认为 `vectorIndex`，可选：file/vectorIndex/content\n4. `v6-cache-policy`: v6 查询使用的缓存策略，默认为 `vectorIndex`，可选：file/vectorIndex/content\n\n### 测试 Demo\n例如：使用默认的 data/ip2region_v4.xdb 和 data/ip2region_v6.xdb 进行查询测试：\n```bash\n➜  java git:(java_app_with_ip2region_service) ✗ java -jar target/ip2region-3.3.0.jar search       \nip2region search service test program\n+-v4 xdb: /data01/code/c/ip2region/data/ip2region_v4.xdb (vectorIndex)\n+-v6 xdb: /data01/code/c/ip2region/data/ip2region_v6.xdb (vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, took: 140 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, took: 391 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, took: 503 μs}\n```\n输入 v4 或者 v6 的 IP 地址即可进行查询测试，也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。\n\n\n# bench 测试\n\n### 测试命令\n可以通过 `java -jar ip2region-{version}.jar bench` 命令来进行 bench 测试，一方面确保 `xdb` 文件没有错误，一方面可以评估查询性能：\n```bash\n➜  java git:(fr_java_ipv6) ✗ java -jar target/ip2region-3.1.0.jar bench                                  \njava -jar ip2region-{version}.jar bench [command options]\noptions:\n --db string              ip2region binary xdb file path\n --src string             source ip text file path\n --cache-policy string    cache policy: file/vectorIndex/content\n```\n\n### v4 bench\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 文件进行 IPv4 的 bench 测试：\n```bash\njava -jar target/ip2region-3.1.0.jar bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n### v6 bench\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 文件进行 IPv6 的 bench 测试：\n```bash\njava -jar target/ip2region-3.1.0.jar bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\n可以通过分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。\n@Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。\n"
  },
  {
    "path": "binding/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.lionsoul</groupId>\n    <artifactId>ip2region</artifactId>\n    <version>3.3.6</version>\n    <packaging>jar</packaging>\n\n    <name>ip2region</name>\n    <url>https://github.com/lionsoul2014/ip2region</url>\n    <description>An open source offline IP address data manager framework and locator with both IPv4 and IPv6 suppported</description>\n\n    <licenses>\n        <license>\n            <name>The Apache Software License, Version 2.0</name>\n            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <scm>\n        <url>git@github.com:lionsoul2014/ip2region.git</url>\n        <connection>scm:git:git@github.com:lionsoul2014/ip2region.git</connection>\n        <developerConnection>scm:git:git@github.com:lionsoul2014/ip2region.git</developerConnection>\n    </scm>\n\n    <developers>\n        <developer>\n            <id>lionsoul</id>\n            <name>chenxin</name>\n            <email>chenxin619315@gmail.com</email>\n        </developer>\n    </developers>\n\n    <distributionManagement>\n\t\t<snapshotRepository>\n\t\t\t<id>lionsoul</id>\n\t\t\t<url>https://oss.sonatype.org/content/repositories/snapshots/</url>\n\t\t</snapshotRepository>\n\t\t<repository>\n\t\t\t<id>lionsoul</id>\n\t\t\t<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n\t\t</repository>\n\t</distributionManagement>\n\n    <issueManagement>\n        <url>https://github.com/lionsoul2014/ip2region/issues</url>\n        <system>Github issues</system>\n    </issueManagement>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <maven.compiler.source>1.8</maven.compiler.source>\n        <maven.compiler.target>1.8</maven.compiler.target>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.13.1</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>2.1.2</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>2.9</version>\n                <executions>\n                    <execution>\n                        <id>attach-javadocs</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                        <configuration>\n                            <additionalparam>${javadoc.opts}</additionalparam>\n                        </configuration>\n                    </execution>\n                </executions>\n                <configuration>\n                    <failOnError>false</failOnError>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>1.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>org.lionsoul.ip2region.SearcherTest</mainClass>\n                                </transformer>\n                            </transformers>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>java8-doclint-disabled</id>\n            <activation>\n                <jdk>[1.8,)</jdk>\n            </activation>\n            <properties>\n                <javadoc.opts>-Xdoclint:none</javadoc.opts>\n            </properties>\n        </profile>\n        <profile>\n            <id>release</id>\n            <build>\n                <plugins>\n                    <!-- Source -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-source-plugin</artifactId>\n                        <version>2.2.1</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>jar-no-fork</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <!-- Javadoc -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>2.9.1</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                                <configuration>\n                                    <additionalparam>${javadoc.opts}</additionalparam>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <!-- GPG -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <version>1.5</version>\n                        <executions>\n                            <execution>\n                                <phase>verify</phase>\n                                <goals>\n                                    <goal>sign</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <!--use the Central Portal: https://central.sonatype.org/publish/publish-portal-maven/ instead-->\n                    <plugin>\n                        <groupId>org.sonatype.central</groupId>\n                        <artifactId>central-publishing-maven-plugin</artifactId>\n                        <version>0.7.0</version>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <publishingServerId>lionsoul</publishingServerId>\n                            <autoPublish>true</autoPublish>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n\n</project>\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/SearcherTest.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/23\n\npackage org.lionsoul.ip2region;\n\nimport org.lionsoul.ip2region.service.Config;\nimport org.lionsoul.ip2region.service.InvalidConfigException;\nimport org.lionsoul.ip2region.service.Ip2Region;\nimport org.lionsoul.ip2region.xdb.InetAddressException;\nimport org.lionsoul.ip2region.xdb.XdbException;\nimport org.lionsoul.ip2region.xdb.LongByteArray;\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport org.lionsoul.ip2region.xdb.Util;\nimport org.lionsoul.ip2region.xdb.Version;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.security.CodeSource;\nimport java.util.concurrent.TimeUnit;\n\npublic class SearcherTest {\n\n    public static void printHelp(String[] args) {\n        System.out.print(\"ip2region xdb searcher\\n\");\n        System.out.print(\"java -jar ip2region-{version}.jar [command] [command options]\\n\");\n        System.out.print(\"Command: \\n\");\n        System.out.print(\"  search    search input test\\n\");\n        System.out.print(\"  bench     search bench test\\n\");\n    }\n\n    public static final String getXdbPath(String fileName) throws IOException {\n        String xdbPath;\n        final CodeSource cs = SearcherTest.class.getProtectionDomain().getCodeSource();\n        if (cs != null) {\n            // log.debugf(\"code path: %s\", cs.getLocation().getPath().concat(\"../../../../data/\"));\n            final Path jarPath = Paths.get(cs.getLocation().getPath());\n            xdbPath = jarPath.getParent().toString().concat(\"/../../../data/\").concat(fileName);\n        } else {\n            xdbPath = \"../../../data/\".concat(fileName);\n        }\n\n        final File xdbFile = new File(xdbPath);\n        return xdbFile.exists() ? xdbFile.getCanonicalPath() : \"\";\n    }\n\n    public static final Ip2Region createService(\n        String v4XdbPath, String v4CachePolicy, String v6XdbPath, String v6CachePolicy) throws IOException, XdbException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.cachePolicyFromName(v4CachePolicy))\n            .setSearchers(1)\n            .setXdbPath(v4XdbPath).asV4();\n\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.cachePolicyFromName(v6CachePolicy))\n            .setSearchers(1)\n            .setXdbPath(v6XdbPath).asV6();\n\n        return Ip2Region.create(v4Config, v6Config);\n    }\n\n    public static Searcher createSearcher(String dbPath, String cachePolicy) throws IOException, XdbException {\n        final RandomAccessFile handle = new RandomAccessFile(dbPath, \"r\");\n\n        // verify the xdb file\n        // @Note: do NOT call it every time you create a searcher since this will slow\n        // down the search response.\n        // @see the util.Verify function for details.\n        Searcher.verify(handle);\n\n        // get the ip version from header\n        final Version version = Version.fromHeader(Searcher.loadHeader(handle));\n\n        // create the final searcher\n        if (\"file\".equals(cachePolicy)) {\n            return Searcher.newWithFileOnly(version, dbPath);\n        } else if (\"vectorIndex\".equals(cachePolicy)) {\n            final byte[] vIndex = Searcher.loadVectorIndexFromFile(dbPath);\n            return Searcher.newWithVectorIndex(version, dbPath, vIndex);\n        } else if (\"content\".equals(cachePolicy)) {\n            final LongByteArray cBuff = Searcher.loadContentFromFile(dbPath);\n            return Searcher.newWithBuffer(version, cBuff);\n        } else {\n            throw new IOException(\"invalid cache policy `\" + cachePolicy + \"`, options: file/vectorIndex/content\");\n        }\n    }\n\n    public static void searchTest(String[] args) throws IOException, XdbException, InterruptedException, InvalidConfigException {\n        String help = \"\";\n        String v4DbPath = \"\", v4CachePolicy = \"vectorIndex\";\n        String v6DbPath = \"\", v6CachePolicy = \"vectorIndex\";\n        for (final String r : args) {\n            if (r.length() < 5) {\n                continue;\n            }\n\n            if (r.indexOf(\"--\") != 0) {\n                continue;\n            }\n\n            String key = \"\", val = \"\";\n            int sIdx = r.indexOf('=');\n            if (sIdx < 0) {\n                key = r.substring(2);\n                // System.out.printf(\"missing = for args pair `%s`\\n\", r);\n                // return;\n            } else {\n                key = r.substring(2, sIdx);\n                val = r.substring(sIdx + 1);\n            }\n\n            // System.out.printf(\"key=%s, val=%s\\n\", key, val);\n            if (\"help\".equals(key)) {\n                help = val == \"\" ? \"true\" : val;\n            } else if (\"v4-db\".equals(key)) {\n                v4DbPath = val;\n            } else if (\"v4-cache-policy\".equals(key)) {\n                v4CachePolicy = val;\n            } else if (\"v6-db\".equals(key)) {\n                v6DbPath = val;\n            } else if (\"v6-cache-policy\".equals(key)) {\n                v6CachePolicy = val;\n            } else {\n                System.out.printf(\"undefined option `%s`\\n\", r);\n                return;\n            }\n        }\n\n        \n        // check and set the default path for v4\n        if (v4DbPath.isEmpty()) {\n            v4DbPath = getXdbPath(\"ip2region_v4.xdb\");\n        }\n\n        // check and set the default path for 6\n        if (v6DbPath.isEmpty()) {\n            v6DbPath = getXdbPath(\"ip2region_v6.xdb\");\n        }\n\n        if (v4DbPath.isEmpty() || v6DbPath.isEmpty() || help.equals(\"true\")) {\n            System.out.print(\"java -jar ip2region-{version}.jar search [command options]\\n\");\n            System.out.print(\"options:\\n\");\n            System.out.print(\" --v4-db string            ip2region ipv4 binary xdb file path\\n\");\n            System.out.print(\" --v4-cache-policy string  v4 cache policy, default vectorIndex, options: file/vectorIndex/content\\n\");\n            System.out.print(\" --v6-db string            ip2region ipv6 binary xdb file path\\n\");\n            System.out.print(\" --v6-cache-policy string  v6 cache policy, default vectorIndex, options: file/vectorIndex/content\\n\");\n            System.out.print(\" --help                    print this help menu\\n\");\n            return;\n        }\n\n        final Ip2Region ip2region = createService(v4DbPath, v4CachePolicy, v6DbPath, v6CachePolicy);\n        final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));\n        System.out.printf(\"ip2region search service test program\\n\" \n+ \"+-v4 xdb: %s (%s)\\n\"\n+ \"+-v6 xdb: %s (%s)\\n\"\n+ \"type 'quit' to exit\\n\", v4DbPath, v4CachePolicy, v6DbPath, v6CachePolicy);\n        while ( true ) {\n            System.out.print(\"ip2region>> \");\n            String line = reader.readLine().trim();\n            if ( line.length() < 2 ) {\n                continue;\n            }\n\n            if ( line.equalsIgnoreCase(\"quit\") ) {\n                break;\n            }\n\n            try {\n                double sTime = System.nanoTime();\n                String region = ip2region.search(line);\n                long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));\n                System.out.printf(\"{region: %s, took: %d μs}\\n\", region, cost);\n            } catch (Exception e) {\n                System.out.printf(\"{region: , err: %s}\\n\", e);\n            }\n        }\n\n        reader.close();\n        ip2region.close();\n        System.out.println(\"ip2region test program exited, thanks for trying\");\n    }\n\n    public static void benchTest(String[] args) throws IOException, XdbException, InetAddressException {\n        String dbPath = \"\", srcPath = \"\", cachePolicy = \"vectorIndex\";\n        for (final String r : args) {\n            if (r.length() < 5) {\n                continue;\n            }\n\n            if (r.indexOf(\"--\") != 0) {\n                continue;\n            }\n\n            int sIdx = r.indexOf('=');\n            if (sIdx < 0) {\n                System.out.printf(\"missing = for args pair `%s`\\n\", r);\n                return;\n            }\n\n            String key = r.substring(2, sIdx);\n            String val = r.substring(sIdx + 1);\n            if (\"db\".equals(key)) {\n                dbPath = val;\n            } else if (\"src\".equals(key)) {\n                srcPath = val;\n            } else if (\"cache-policy\".equals(key)) {\n                cachePolicy = val;\n            } else {\n                System.out.printf(\"undefined option `%s`\\n\", r);\n                return;\n            }\n        }\n\n        if (dbPath.length() < 1 || srcPath.length() < 1) {\n            System.out.print(\"java -jar ip2region-{version}.jar bench [command options]\\n\");\n            System.out.print(\"options:\\n\");\n            System.out.print(\" --db string              ip2region binary xdb file path\\n\");\n            System.out.print(\" --src string             source ip text file path\\n\");\n            System.out.print(\" --cache-policy string    cache policy: file/vectorIndex/content\\n\");\n            return;\n        }\n\n        Searcher searcher = createSearcher(dbPath, cachePolicy);\n        long count = 0, costs = 0, tStart = System.nanoTime();\n        String line;\n        final Charset charset = Charset.forName(\"utf-8\");\n        final FileInputStream fis = new FileInputStream(srcPath);\n        final BufferedReader reader = new BufferedReader(new InputStreamReader(fis, charset));\n        while ((line = reader.readLine()) != null) {\n            String l = line.trim();\n            String[] ps = l.split(\"\\\\|\", 3);\n            if (ps.length != 3) {\n                reader.close();\n                System.out.printf(\"invalid ip segment `%s`\\n\", l);\n                return;\n            }\n\n            // mark the start time\n            long sTime = System.nanoTime();\n\n            byte[] sip;\n            try {\n                sip = Util.parseIP(ps[0]);\n            } catch (Exception e) {\n                reader.close();\n                System.out.printf(\"check start ip `%s`: %s\\n\", ps[0], e);\n                return;\n            }\n\n            byte[] eip;\n            try {\n                eip = Util.parseIP(ps[1]);\n            } catch (Exception e) {\n                reader.close();\n                System.out.printf(\"check end ip `%s`: %s\\n\", ps[1], e);\n                return;\n            }\n\n            if (Util.ipCompare(sip, eip) > 0) {\n                reader.close();\n                System.out.printf(\"start ip(%s) should not be greater than end ip(%s)\\n\", ps[0], ps[1]);\n                return;\n            }\n\n            for (final byte[] ip : new byte[][]{sip, eip}) {\n                String region = searcher.search(ip);\n\n                // check the region info\n                if (!ps[2].equals(region)) {\n                    System.out.printf(\"failed search(%s) with (%s != %s)\\n\", Util.ipToString(ip), region, ps[2]);\n                    reader.close();\n                    return;\n                }\n\n                count++;\n            }\n\n            costs += System.nanoTime() - sTime;\n        }\n\n        reader.close();\n        searcher.close();\n        long took = System.nanoTime() - tStart;\n        System.out.printf(\"Bench finished, {cachePolicy: %s, total: %d, took: %ds, cost: %d μs/op}\\n\",\n                cachePolicy, count, TimeUnit.NANOSECONDS.toSeconds(took),\n                count == 0 ? 0 : TimeUnit.NANOSECONDS.toMicros(costs/count));\n    }\n\n    public static void main(String[] args) {\n        if (args.length < 1) {\n            printHelp(args);\n            return;\n        }\n\n        if (\"search\".equals(args[0])) {\n            try {\n                searchTest(args);\n            } catch (Exception e) {\n                System.out.printf(\"failed running search test: %s\\n\", e);\n            }\n        } else if (\"bench\".equals(args[0])) {\n            try {\n                benchTest(args);\n            } catch (Exception e) {\n                System.out.printf(\"fwailed running bench test: %s\\n\", e);\n            }\n        } else {\n            printHelp(args);\n        }\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/service/Config.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.service;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport org.lionsoul.ip2region.xdb.Header;\nimport org.lionsoul.ip2region.xdb.LongByteArray;\nimport org.lionsoul.ip2region.xdb.Version;\nimport org.lionsoul.ip2region.xdb.XdbException;\n\n/**\n * ip2region config class\n * @Author Lion <chenxin619315@gmail.com>\n * @Date   2025/11/20\n*/\npublic class Config {\n    // cache policy consts\n    public static final int NoCache = 0;\n    public static final int VIndexCache = 1;\n    public static final int BufferCache = 2;\n\n    // alias of BufferCache but easier to understand or Remember\n    public static final int FullCache = 2;\n\n    // search cache policy\n    public final int cachePolicy;\n    public final Version ipVersion;\n\n    // xdb file path\n    public final File xdbFile;\n    public final Header header;\n\n    public final byte[] vIndex;\n    public final LongByteArray cBuffer;\n\n    public final int searchers;\n\n    // config builder\n    public static ConfigBuilder custom() {\n        return new ConfigBuilder();\n    }\n\n    protected Config(int cachePolicy, Version ipVersion, File xdbFile, \n        Header header, byte[] vIndex, LongByteArray cBuffer, int searchers) throws IOException, XdbException {\n        this.cachePolicy = cachePolicy;\n        this.ipVersion = ipVersion;\n\n        this.xdbFile = xdbFile;\n        this.header  = header;\n        this.vIndex  = vIndex;\n        this.cBuffer = cBuffer;\n\n        final Version xVersion = Version.fromHeader(header);\n        // verify the ip version (ipVersion and the version of the xdb file should be the same)\n        if (header.ipVersion != ipVersion.id) {\n            throw new XdbException(\"ip verison not match: xdb file \" \n                + xdbFile.getAbsolutePath() + \" (\" + xVersion.name + \"), as \" + ipVersion.name + \" expected\");\n        }\n\n        this.searchers = searchers;\n    }\n\n    @Override public String toString() {\n        final StringBuffer sb = new StringBuffer();\n        sb.append('{');\n        sb.append(\"cache_policy:\").append(cachePolicy).append(',');\n        sb.append(\"version:\").append(ipVersion.toString()).append(',');\n        sb.append(\"xdb_path:\").append(xdbFile == null ? \"null\" : xdbFile.getAbsolutePath()).append(',');\n        sb.append(\"header:\").append(header.toString()).append(',');\n        if (vIndex == null) {\n            sb.append(\"v_index: null, \");\n        } else {\n            sb.append(\"v_index: {bytes: \").append(vIndex.length).append(\"},\");\n        }\n        if (cBuffer == null) {\n            sb.append(\"c_buffer: null, \");\n        } else {\n            sb.append(\"c_buffer: {bytes: \").append(cBuffer.length()).append(\"},\");\n        }\n        sb.append(\"searchers:\").append(searchers);\n        sb.append('}');\n        return sb.toString();\n    }\n\n    public static final int cachePolicyFromName(String name) throws InvalidConfigException {\n        final String lName = name.toLowerCase();\n        if (lName.equals(\"file\") || lName.equals(\"nocache\")) {\n            return NoCache;\n        } else if (lName.equals(\"vectorindex\") || lName.equals(\"vindex\") || lName.equals(\"vindexcache\")) {\n            return VIndexCache;\n        } else if (lName.equals(\"content\") || lName.equals(\"buffercache\")) {\n            return BufferCache;\n        } else {\n            throw new InvalidConfigException(\"invalid cache policy `\" + name + \"`\");\n        }\n    }\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/service/ConfigBuilder.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.service;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.RandomAccessFile;\n\nimport org.lionsoul.ip2region.xdb.Header;\nimport org.lionsoul.ip2region.xdb.LongByteArray;\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport org.lionsoul.ip2region.xdb.Version;\nimport org.lionsoul.ip2region.xdb.XdbException;\n\n/**\n * ip2region config builder\n * @Author Lion <chenxin619315@gmail.com>\n * @Date   2025/11/20\n*/\npublic class ConfigBuilder {\n\n    // cache policy\n    private int cachePolicy = Config.VIndexCache;\n\n    // xdb file path / Object / InputStream.\n    // Priority: InputStream -> File Object -> String path\n    private String xdbPath = null;\n    private File xdbFile = null;\n    private InputStream xdbInputStream = null;\n\n    // slice bytes for in-memory xdb content\n    private int cacheSliceBytes = Searcher.DEFAULT_SLICE_BYTES;\n\n    // searchers\n    private int searchers = 20;\n\n    public ConfigBuilder() {}\n\n    public ConfigBuilder(String xdbPath) {\n        this.xdbPath = xdbPath;\n    }\n\n    public ConfigBuilder setCachePolicy(int cachePolicy) {\n        this.cachePolicy = cachePolicy;\n        return this;\n    }\n\n    public ConfigBuilder setXdbPath(String xdbPath) {\n        assert xdbPath != null && xdbPath.length() > 0;\n        this.xdbPath = xdbPath;\n        return this;\n    }\n\n    public ConfigBuilder setXdbFile(File xdbFile) {\n        assert xdbFile != null;\n        this.xdbFile = xdbFile;\n        return this;\n    }\n\n    public ConfigBuilder setXdbInputStream(InputStream xdbInputStream) {\n        assert xdbInputStream != null;\n        this.xdbInputStream = xdbInputStream;\n        return this;\n    }\n\n    public ConfigBuilder setCacheSliceBytes(int cacheSliceBytes) {\n        this.cacheSliceBytes = cacheSliceBytes;\n        return this;\n    }\n\n    public ConfigBuilder setSearchers(int searchers) {\n        this.searchers = searchers;\n        return this;\n    }\n\n    private Config build(Version ipVersion) throws IOException, XdbException, InvalidConfigException {\n        if (xdbInputStream == null) {\n            // everyting is fine\n        } else if (cachePolicy != Config.BufferCache) {\n            // @Note: we can't directly rewrite the cachePolicy to Config.BufferCache.\n            // you must know what you are doing.\n            throw new InvalidConfigException(\"SetXdbInputStream could ONLY be used with cachePolicy = Config.BufferCache\");\n        } else {\n            // 1, load the content buffer\n            final LongByteArray cBuffer = Searcher.loadContentFromInputStream(xdbInputStream, cacheSliceBytes);\n\n            // 2, verify the xdb from the buffer\n            Searcher.verify(Searcher.loadHeaderFromBuffer(cBuffer), cBuffer.length());\n\n            // 3, load the header\n            final Header header = Searcher.loadHeaderFromBuffer(cBuffer);\n\n            // create the config without xdbFile and vIndex\n            return new Config(cachePolicy, ipVersion, null, header, null, cBuffer, searchers);\n        }\n\n        // load the header and the cache buffer\n        final File xdbFile;\n        if (this.xdbFile != null) {\n            xdbFile = this.xdbFile;\n        } else if (this.xdbPath != null) {\n            xdbFile = new File(this.xdbPath);\n        } else {\n            throw new InvalidConfigException(\"Both xdbFile and xdbPath is null\");\n        }\n\n        final RandomAccessFile raf = new RandomAccessFile(xdbFile, \"r\");\n\n        // 1, verify the xdb\n        Searcher.verify(raf);\n\n        // 2, load the header\n        final Header header = Searcher.loadHeader(raf);\n\n        // 3, check and load the vector index buffer\n        final byte[] vIndex = cachePolicy == Config.VIndexCache ? Searcher.loadVectorIndex(raf) : null;\n\n        // 4, check and load the content buffer\n        final LongByteArray cBuffer = cachePolicy == Config.BufferCache ? Searcher.loadContent(raf, cacheSliceBytes) : null;\n\n        raf.close();\n        return new Config(cachePolicy, ipVersion, xdbFile, header, vIndex, cBuffer, searchers);\n    }\n\n    // build the final #Config instance for IPv4\n    public Config asV4() throws IOException, XdbException, InvalidConfigException {\n        return build(Version.IPv4);\n    }\n\n    // build the final #Config instance for IPv6\n    public Config asV6() throws IOException, XdbException, InvalidConfigException {\n        return build(Version.IPv6);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/service/InvalidConfigException.java",
    "content": "package org.lionsoul.ip2region.service;\n\npublic class InvalidConfigException extends Exception {\n    public InvalidConfigException(String str) {\n        super(str);\n    }\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/service/Ip2Region.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.service;\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport org.lionsoul.ip2region.xdb.InetAddressException;\nimport org.lionsoul.ip2region.xdb.Searcher;\nimport org.lionsoul.ip2region.xdb.Util;\nimport org.lionsoul.ip2region.xdb.XdbException;\n\n/**\n * ip2region searcher manager service to provider:\n * 1. Unified query interface for IPv4 and IPv6 address.\n * 2. Concurrency search support.\n * \n * @Author Lion <chenxin619315@gmail.com>\n * Date    2025/11/21\n*/\npublic class Ip2Region {\n\n    /* v4 pool for cache policy vIndex or NoCache */\n    private final SearcherPool v4Pool;\n\n    /* v4 xdb searcher for cache policy cBuffer */\n    private final Searcher v4InMemSearcher;\n\n    /* v6 pool for cache policy vIndex or NoCache*/\n    private final SearcherPool v6Pool;\n\n    /* v6 xdb searcher for cache policy cBuffer */\n    private final Searcher v6InMemSearcher;\n\n    public static final Ip2Region create(final Config v4Config, final Config v6Config) throws IOException {\n        return new Ip2Region(v4Config, v6Config).init();\n    }\n\n    public static final Ip2Region create(final String v4XdbPath, final String v6XdbPath) throws IOException, XdbException, InvalidConfigException {\n        return new Ip2Region(new File(v4XdbPath), new File(v6XdbPath)).init();\n    }\n\n    public static final Ip2Region create(final File v4XdbFile, final File v6XdbFile) throws IOException, XdbException, InvalidConfigException {\n        return new Ip2Region(v4XdbFile, v6XdbFile).init();\n    }\n\n    /**\n     * init the ip2reigon with two xdb file path and default cachePolicy vIndex.\n     * set it to null to disabled the search for specified version\n     * \n     * @param v4XdbFile\n     * @param v6XdbFile\n     * @throws XdbException \n     * @throws IOException \n     * @throws InvalidConfigException \n    */\n    protected Ip2Region(File v4XdbFile, File v6XdbFile) throws IOException, XdbException, InvalidConfigException {\n        this(\n            v4XdbFile == null ? null : Config.custom().setXdbFile(v4XdbFile).asV4(), \n            v6XdbFile == null ? null : Config.custom().setXdbFile(v6XdbFile).asV6()\n        );\n    }\n\n    /**\n     * init the ip2region with specified config.\n     * set it to null for disabled the search for specified version\n     * \n     * @param v4Config\n     * @param v6Config\n     * @throws IOException\n    */ \n    protected Ip2Region(Config v4Config, Config v6Config) throws IOException {\n        if (v4Config == null) {\n            // @Note: with IPv4 disabled ?\n            this.v4InMemSearcher = null;\n            this.v4Pool = null;\n        } else if (v4Config.cachePolicy == Config.BufferCache) {\n            this.v4InMemSearcher = Searcher.newWithBuffer(v4Config.ipVersion, v4Config.cBuffer);\n            this.v4Pool = null;\n        } else {\n            this.v4InMemSearcher = null;\n            this.v4Pool = new SearcherPool(v4Config);\n        }\n\n        if (v6Config == null) {\n            // @Note: with IPv6 disabled ?\n            this.v6InMemSearcher = null;\n            this.v6Pool = null;\n        } else if (v6Config.cachePolicy == Config.BufferCache) {\n            this.v6InMemSearcher = Searcher.newWithBuffer(v6Config.ipVersion, v6Config.cBuffer);\n            this.v6Pool = null;\n        } else {\n            this.v6InMemSearcher = null;\n            this.v6Pool = new SearcherPool(v6Config);\n        }\n    }\n\n    // init the current ip2region service\n    protected Ip2Region init() throws IOException {\n        if (v4Pool != null) {\n            v4Pool.init();\n        }\n\n        if (v6Pool != null) {\n            v6Pool.init();\n        }\n\n        return this;\n    }\n\n    public String search(String ipString) throws InetAddressException, IOException, InterruptedException {\n        return search(Util.parseIP(ipString));\n    }\n\n    public String search(byte[] ipBytes) throws InetAddressException, IOException, InterruptedException {\n        if (ipBytes.length == 4) {\n            return v4Search(ipBytes);\n        } else if (ipBytes.length == 16) {\n            return v6Search(ipBytes);\n        } else {\n            throw new InetAddressException(\"invalid byte ip address with length=\" + ipBytes.length);\n        }\n    }\n\n    protected String v4Search(final byte[] ipBytes) throws IOException, InetAddressException, InterruptedException {\n        if (v4InMemSearcher != null) {\n            return v4InMemSearcher.search(ipBytes);\n        }\n\n        // IPv4 search is disabled\n        if (v4Pool == null) {\n            return null;\n        }\n\n        final Searcher searcher = v4Pool.borrowSearcher();\n        try {\n            return searcher.search(ipBytes);\n        } finally {\n            v4Pool.returnSearcher(searcher);\n        }\n    }\n\n    protected String v6Search(final byte[] ipBytes) throws IOException, InetAddressException, InterruptedException {\n        if (v6InMemSearcher != null) {\n            return v6InMemSearcher.search(ipBytes);\n        }\n\n        // IPv6 search is disabled\n        if (v6Pool == null) {\n            return null;\n        }\n\n        final Searcher searcher = v6Pool.borrowSearcher();\n        try {\n            return searcher.search(ipBytes);\n        } finally {\n            v6Pool.returnSearcher(searcher);\n        }\n    }\n\n    public void close() throws InterruptedException {\n        close(10000);\n    }\n\n    public void close(long timeoutMillis) throws InterruptedException {\n        if (v4Pool != null) {\n            v4Pool.close(timeoutMillis);\n        }\n\n        if (v6Pool != null) {\n            v6Pool.close(timeoutMillis);\n        }\n    }\n\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/service/SearcherPool.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.service;\n\nimport java.io.IOException;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.Queue;\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.ReentrantLock;\n\nimport org.lionsoul.ip2region.xdb.Searcher;\n\n/**\n * ip2region searcher pool manager to provider Concurrency search support.\n * \n * @author Leon<chenxin619315@gmail.com>\n * Date 2025/11/21\n*/\npublic class SearcherPool {\n    // config instance\n    public final Config config;\n\n    // searcher pool\n    private final Queue<Searcher> pool;\n\n    // lock & conditions\n    private final ReentrantLock lock;\n    private final Condition emptyCondition;\n    private final Condition fullCondition;\n\n    // searcher numbers that was loaned out\n    private int loanCount;\n\n    // static method to create and init the searcher pool\n    public static final SearcherPool create(final Config config) throws IOException {\n        return new SearcherPool(config).init();\n    }\n\n    public static final SearcherPool create(final Config config, boolean fair) throws IOException {\n        return new SearcherPool(config, fair).init();\n    }\n\n    protected SearcherPool(Config config) throws IOException {\n        this(config, false);\n    }\n\n    protected SearcherPool(Config config, boolean fair) {\n        assert config.searchers > 0;\n        this.config = config;\n        this.pool = new LinkedList<>();\n        this.lock = new ReentrantLock(fair);\n        this.emptyCondition = this.lock.newCondition();\n        this.fullCondition = this.lock.newCondition();\n        this.loanCount = 0;\n    }\n\n    protected SearcherPool init() throws IOException {\n        // create the searchers\n        for (int i = pool.size(); i < config.searchers; i++) {\n            final Searcher searcher = new Searcher(config.ipVersion, config.xdbFile, config.vIndex, config.cBuffer);\n            pool.add(searcher);\n        }\n\n        return this;\n    }\n\n    public int getLoanCount() {\n        lock.lock();\n        int lc = this.loanCount;\n        lock.unlock();\n        return lc;\n    }\n\n    public Searcher borrowSearcher() throws InterruptedException {\n        lock.lock();\n        try {\n            while (pool.isEmpty()) {\n                emptyCondition.await();\n            }\n\n            loanCount++;\n            return pool.poll();\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    public void returnSearcher(final Searcher searcher) {\n        lock.lock();\n        try {\n            pool.add(searcher);\n            loanCount--;\n            emptyCondition.signal();\n\n            // check and signal the full condition.\n            // pool close\n            if (loanCount == 0) {\n                fullCondition.signal();\n            }\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    // close the searcher pool\n    public void close() throws InterruptedException {\n        close(10000);\n    }\n\n    public void close(long timeoutMillis) throws InterruptedException {\n        lock.lock();\n        try {\n            while (loanCount > 0) {\n                fullCondition.wait(timeoutMillis);\n            }\n\n            final Iterator<Searcher> it = pool.iterator();\n            while (it.hasNext()) {\n                final Searcher searcher = it.next();\n                try {searcher.close();} catch (IOException e) {}\n                it.remove();\n            }\n        } finally {\n            lock.unlock();\n        }\n    }\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/Header.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/23\n\npackage org.lionsoul.ip2region.xdb;\n\npublic class Header {\n    public final int version;\n    public final int indexPolicy;\n    public final long createdAt;\n    public final long startIndexPtr;\n    public final long endIndexPtr;\n\n    // since xdb 3.0 with IPv6 supporting\n    public final int ipVersion;\n    public final int runtimePtrBytes;\n\n    public final byte[] buffer;\n\n    public Header(byte[] buff) {\n        assert buff.length >= 16;\n        version = LittleEndian.getUint16(buff, 0);\n        indexPolicy = LittleEndian.getUint16(buff, 2);\n        createdAt = LittleEndian.getUint32(buff, 4);\n        startIndexPtr = LittleEndian.getUint32(buff, 8);\n        endIndexPtr = LittleEndian.getUint32(buff, 12);\n        ipVersion = LittleEndian.getUint16(buff, 16);\n        runtimePtrBytes = LittleEndian.getUint16(buff, 18);\n        buffer = buff;\n    }\n\n    @Override public String toString() {\n        return \"{\" +\n            \"Version:\" + version + ',' +\n            \"IndexPolicy:\" + indexPolicy + ',' +\n            \"CreatedAt:\" + createdAt + ',' +\n            \"StartIndexPtr:\" + startIndexPtr + ',' +\n            \"EndIndexPtr:\" + endIndexPtr + ',' + \n            \"IPVersion:\" + ipVersion + ',' + \n            \"RuntimePtrBytes:\" + runtimePtrBytes +\n        '}';\n    }\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IPv4 version implementation\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class IPv4 extends Version {\n    public IPv4() {\n        // segmentIndex: 4 + 4 + 2 + 4\n        super(4, \"IPv4\", 4, 14);\n    }\n\n    @Override\n    public int putBytes(byte[] buff, int offset, byte[] ip) {\n        // use the Little endian byte order to compatible with the old searcher implementation\n        buff[offset++] = ip[3];\n        buff[offset++] = ip[2];\n        buff[offset++] = ip[1];\n        buff[offset  ] = ip[0];\n        return ip.length;\n    }\n\n    @Override\n    public int ipSubCompare(byte[] ip1, byte[] buff, int offset) {\n        // ip1: Big endian byte order parsed from input\n        // ip2: Little endian byte order read from xdb index.\n        // @Note: to compatible with the old Litten endian index encode implementation.\n        int j = offset + ip1.length - 1;\n        for (int i = 0; i < ip1.length; i++, j--) {\n            final int i1 = (int) (ip1[i] & 0xFF);\n            final int i2 = (int) (buff[j] & 0xFF);\n            if (i1 < i2) {\n                return -1;\n            }\n\n            if (i1 > i2) {\n                return 1;\n            }\n        }\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IPv4 version implementation\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class IPv6 extends Version {\n\n    public IPv6() {\n        // segmentIndex: 16 + 16 + 2 + 4\n        super(6, \"IPv6\", 16, 38);\n    }\n\n    @Override\n    public int putBytes(byte[] buff, int offset, byte[] ip) {\n        System.arraycopy(ip, 0, buff, offset, ip.length);\n        return ip.length;\n    }\n\n    @Override\n    public int ipSubCompare(byte[] ip1, byte[] buff, int offset) {\n        return Util.ipSubCompare(ip1, buff, offset);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/InetAddressException.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\npublic class InetAddressException extends Exception {\n\n    public InetAddressException(String str) {\n        super(str);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// Little Endian basic data type decode and encode.\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class LittleEndian {\n\n    public final static int[] shiftIndex = {0, 8, 16, 24, 32, 40, 48, 56};\n    \n    // put specified bytes to the buffer started from the offset\n    public static void put(final byte[] buff, int offset, long value, int bytes) {\n        if (bytes > 8) {\n            throw new IndexOutOfBoundsException(\"bytes should be <= 8\");\n        }\n\n        for (int i = 0; i < bytes; i++) {\n            buff[offset++] = (byte)((value >>> shiftIndex[i]) & 0xFF);\n        }\n    }\n\n    // put an uint32 (4 bytes long) to the buffer from the offset\n    public static void putUint32(final byte[] buff, int offset, long value) {\n        buff[offset++] = (byte) (value & 0xFF);\n        buff[offset++] = (byte) ((value >>  8) & 0xFF);\n        buff[offset++] = (byte) ((value >> 16) & 0xFF);\n        buff[offset  ] = (byte) ((value >> 24) & 0xFF);\n    }\n\n    // put a 2-bytes int to the buffer from the specified offset\n    public static void putUint16(final byte[] buff, int offset, int value) {\n        buff[offset++] = (byte) (value & 0xFF);\n        buff[offset  ] = (byte) ((value >>  8) & 0xFF);\n    }\n\n    // get an uint32 from a byte array from the specified offset\n    public static long getUint32(final byte[] buff, int offset) {\n        return (\n            ((buff[offset++] & 0x000000FFL)) |\n            ((buff[offset++] <<  8) & 0x0000FF00L) |\n            ((buff[offset++] << 16) & 0x00FF0000L) |\n            ((buff[offset  ] << 24) & 0xFF000000L)\n        );\n    }\n\n    // get an 2 bytes int from a byte array from the specified offset\n    public static int getUint16(final byte[] buff, int offset) {\n        return (\n            ((buff[offset++]) & 0x000000FF) |\n            ((buff[offset  ] << 8) & 0x0000FF00)\n        );\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\n// simple log implementation\npublic class Log {\n\n    /* Log level constants define */\n    public static final int DEBUG = 0;\n    public static final int INFO = 1;\n    public static final int WARN = 2;\n    public static final int ERROR = 3;\n\n    // level name\n    public static final String[] level_string = new String[] {\n        \"DEBUG\",\n        \"INFO\",\n        \"WARN\",\n        \"ERROR\"\n    };\n\n    public final Class<?> baseClass;\n    private int level = INFO;\n\n    public Log(Class<?> baseClass) {\n        this.baseClass = baseClass;\n    }\n\n    public static Log getLogger(Class<?> baseClass) {\n        return new Log(baseClass);\n    }\n\n    public String format(int level, String format, Object... args) {\n        // append the datetime\n        final StringBuilder sb = new StringBuilder();\n        final SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        sb.append(String.format(\"%s %-5s \", sdf.format(new Date()), level_string[level]));\n\n        // append the class name\n        sb.append(baseClass.getName()).append(' ');\n        sb.append(String.format(format, args));\n        return sb.toString();\n    }\n\n    public void printf(int level, String format, Object... args) {\n        if (level < DEBUG || level > ERROR) {\n            throw new IndexOutOfBoundsException(\"invalid level index \" + level);\n        }\n\n        // level filter\n        if (level < this.level) {\n            return;\n        }\n\n        System.out.println(format(level, format, args));\n        System.out.flush();\n    }\n\n    public String getDebugf(String format, Object... args) {\n        return format(DEBUG, format, args);\n    }\n\n    public void debugf(String format, Object... args) {\n        printf(DEBUG, format, args);\n    }\n\n    public String getInfof(String format, Object... args) {\n        return format(INFO, format, args);\n    }\n\n    public void infof(String format, Object... args) {\n        printf(INFO, format, args);\n    }\n\n    public String getWarnf(String format, Object... args) {\n        return format(WARN, format, args);\n    }\n\n    public void warnf(String format, Object... args) {\n        printf(WARN, format, args);\n    }\n\n    public String getErrorf(String format, Object... args) {\n        return format(ERROR, format, args);\n    }\n\n    public void errorf(String format, Object... args) {\n        printf(ERROR, format, args);\n    }\n\n    public Log setLevel(int level) {\n        this.level = level;\n        return this;\n    }\n\n    public Log setLevel(String level) {\n        String v = level.toLowerCase();\n        if (\"debug\".equals(v)) {\n            this.level = DEBUG;\n        } else if (\"info\".equals(v)) {\n            this.level = INFO;\n        } else if (\"warn\".equals(v)) {\n            this.level = WARN;\n        } else if (\"error\".equals(v)) {\n            this.level = ERROR;\n        }\n\n        return this;\n    }\n\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/LongByteArray.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.io.IOException;\n\n// xdb byte buffer which used to instead of the byte array\n// when the size of the xdb file is greater than 2^32 << 2;\n// xdb file v4 is designed to be a maximum of 2^32 bytes in size.\n// @Author Leon <chenxin619315@gmail.com>\n// @Date 2025/08/22\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class LongByteArray {\n\n    // slice bytes\n    // if it is greater than the 0 we will use the fixed slice bytes\n    // or we use the dynamic slice bytes.\n    private final int sliceBytes;\n\n    // when EOF is true means we cannot call the #append anymore.\n    // for fixed slice bytes only.\n    private boolean _eof = false;\n\n    // byte buffer list\n    private final List<byte[]> buffs = new ArrayList<byte[]>();\n    private long length;\n\n    public LongByteArray() {\n        this.length = 0;\n        this.sliceBytes = -1;\n    }\n\n    public LongByteArray(int sliceBytes) {\n        assert sliceBytes != 0;\n        assert sliceBytes <= Searcher.MAX_WRITE_BYTES;\n        this.sliceBytes = sliceBytes;\n    }\n\n    // append new buffer\n    public void append(final byte[] buffer) throws IOException{\n        // check and assert the slice bytes\n        if (sliceBytes > 0) {\n            if (_eof) {\n                throw new IOException(\"buffer array closed (EOF=true)\");\n            } else if (buffer.length != sliceBytes) {\n                // mark the buffer array as closed\n                // since the last buffer block bytes is not equal to the expected #sliceBytes\n                _eof = true;\n            }\n        }\n\n        buffs.add(buffer);\n        length += buffer.length;\n    }\n\n    public long length() {\n        return length;\n    }\n\n    public int size() {\n        return buffs.size();\n    }\n\n    // internal method to determine the position of the specified offset\n    private Position determinate(final long offset) {\n        int index = 0, position = 0, buffLen = buffs.size();\n        if (sliceBytes > 0) {\n            // simply some math calcs to determine the offset\n            index = (int) (offset / sliceBytes);\n            position = (int) (offset - (index * sliceBytes));\n            // position = (int) (offset % sliceBytes);\n        } else {\n            // loop the buffer to determine the offset\n            long curIndex = 0;\n            for (index = 0; index < buffLen; index++) {\n                final byte[] buff = buffs.get(index);\n                if (curIndex + buff.length < offset) {\n                    curIndex += buff.length;\n                    continue;\n                }\n\n                // matched and calc the position\n                position = (int) (offset - curIndex);\n                break;\n            }\n        }\n\n        return new Position(index, position);\n    }\n\n    // Copy from the current buffers to a specified buffer\n    // from the specified offset with a specified length\n    public byte[] copy(final long srcPos, final byte[] dest, final int destPos, final int length) {\n        if (srcPos >= this.length) {\n            throw new IndexOutOfBoundsException(\"srcPos exceed the maximum array length `\" + this.length + \"`\");\n        }\n\n        if (destPos + length > dest.length) {\n            throw new IndexOutOfBoundsException(\"destPost+length exceed the maximum dest buffer length `\" + dest.length + \"`\");\n        }\n\n        final Position pos = determinate(srcPos);\n\n        // copy from the current buffer\n        final byte[] hBuff = buffs.get(pos.index++);\n        final int copyLen = Math.min(hBuff.length - pos.offset, length);\n        System.arraycopy(hBuff, pos.offset, dest, destPos, copyLen);\n\n        // check and copy from the rest buffer?\n        int sPos = destPos + copyLen;\n        int left = length - copyLen;\n        while (left > 0) {\n            final byte[] tBuff = buffs.get(pos.index++);\n            final int buffLen = tBuff.length;\n            if (left <= buffLen) {\n                System.arraycopy(tBuff, 0, dest, sPos, left);\n                break;\n            }\n\n            System.arraycopy(tBuff, 0, dest, sPos, buffLen);\n            sPos += buffLen;\n            left -= buffLen;\n        }\n\n        return dest;\n    }\n\n    // get a byte-buffer from the specified index with a specified length.\n    // this method will allocate a new byte buffer with length = $length.\n    public byte[] slice(long offset, int length) {\n        if (offset + length > this.length) {\n            throw new IndexOutOfBoundsException(\"offset+length exceed the maximum array length `\" + this.length + \"`\");\n        }\n\n        final byte[] buffer = new  byte[length];\n        return copy(offset, buffer, 0, length);\n    }\n\n    // get a 4-bytes uint32 integer from the specified index\n    public long getUint32(long offset) {\n        final byte[] b = new byte[4];\n        copy(offset, b, 0, 4);\n        return LittleEndian.getUint32(b, 0);\n    }\n\n    public int getInt2(long offset) {\n        final byte[] b = new byte[4];\n        copy(offset, b, 0, 4);\n        return LittleEndian.getUint16(b, 0);\n    }\n\n    // position entry class\n    public static class Position {\n        public int index;\n        public int offset;\n        public Position(int index, int offset) {\n            this.index = index;\n            this.offset = offset;\n        }\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.io.File;\n\n// xdb searcher (Not thread safe implementation)\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/23\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.RandomAccessFile;\n\npublic class Searcher {\n    // xdb structure version no\n    public static final int STRUCTURE_20 = 2;\n    public static final int STRUCTURE_30 = 3;\n\n    // constant defined copied from the xdb maker\n    public static final int HeaderInfoLength = 256;\n    public static final int VectorIndexRows  = 256;\n    public static final int VectorIndexCols  = 256;\n    public static final int VectorIndexSize  = 8;\n\n    // maximum slice bytes for dynamic buffer array.\n    // Linux max write / read bytes.\n    // Check https://mp.weixin.qq.com/s/4xHRcnQbIcjtMGdXEGrxsA \n    //  to get to know why we default to this value.\n    public static final int MAX_WRITE_BYTES = 0x7ffff000;\n\n    // default slice bytes (50 MiB) for fixed buffer array.\n    public static final int DEFAULT_SLICE_BYTES = 50 * 1024 * 1024;\n\n    // ip version\n    private final Version version;\n\n    // random access file handle for file-based search\n    private final File xdbFile;\n    private final RandomAccessFile handle;\n\n    private int ioCount = 0;\n\n    // vector index.\n    // use the byte[] instead of VectorIndex entry array to keep\n    // the minimal memory allocation.\n    private final byte[] vectorIndex;\n\n    // xdb content buffer, used for in-memory search.\n    // @Note: use the LongByteArray instead since 2025/08/22\n    // private final byte[] contentBuff;\n    private final LongByteArray contentBuff;\n\n    // --- static method to create searchers\n\n    public static Searcher newWithFileOnly(Version version, String xdbPath) throws IOException {\n        return new Searcher(version, new File(xdbPath), null, null);\n    }\n\n    public static Searcher newWithFileOnly(Version version, File xdbFile) throws IOException {\n        return new Searcher(version, xdbFile, null, null);\n    }\n\n    public static Searcher newWithVectorIndex(Version version, String xdbPath, byte[] vectorIndex) throws IOException {\n        return new Searcher(version, new File(xdbPath), vectorIndex, null);\n    }\n\n    public static Searcher newWithVectorIndex(Version version, File xdbFile, byte[] vectorIndex) throws IOException {\n        return new Searcher(version, xdbFile, vectorIndex, null);\n    }\n\n    public static Searcher newWithBuffer(Version version, LongByteArray cBuff) throws IOException {\n        return new Searcher(version, null, null, cBuff);\n    }\n\n    // --- End of creator\n\n    public Searcher(Version version, File xdbFile, byte[] vectorIndex, LongByteArray cBuff) throws IOException {\n        this.version = version;\n        this.xdbFile = xdbFile;\n        if (cBuff != null) {\n            this.handle = null;\n            this.vectorIndex = null;\n            this.contentBuff = cBuff;\n        } else {\n            this.handle = new RandomAccessFile(xdbFile, \"r\");\n            this.vectorIndex = vectorIndex;\n            this.contentBuff = null;\n        }\n    }\n\n    public void close() throws IOException {\n        if (this.handle != null) {\n            this.handle.close();\n        }\n    }\n\n    public Version getIPVersion() {\n        return version;\n    }\n\n    public int getIOCount() {\n        return ioCount;\n    }\n\n    public String search(String ipStr) throws Exception {\n        return search(Util.parseIP(ipStr));\n    }\n\n    public String search(byte[] ip) throws IOException, InetAddressException {\n        // ip version check\n        if (ip.length != version.bytes) {\n            throw new InetAddressException(\"invalid ip address (\"+version.name+\" expected)\");\n        }\n\n        // reset the global counter\n        this.ioCount = 0;\n\n        // locate the segment index block based on the vector index\n        long sPtr = 0, ePtr = 0;\n        int il0 = (int) (ip[0] & 0xFF);\n        int il1 = (int) (ip[1] & 0xFF);\n        int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;\n        // System.out.printf(\"il0: %d, il1: %d, idx: %d\\n\", il0, il1, idx);\n        if (vectorIndex != null) {\n            sPtr = LittleEndian.getUint32(vectorIndex, idx);\n            ePtr = LittleEndian.getUint32(vectorIndex, idx + 4);\n        } else if (contentBuff != null) {\n            sPtr = contentBuff.getUint32(HeaderInfoLength + idx);\n            ePtr = contentBuff.getUint32(HeaderInfoLength + idx + 4);\n        } else {\n            final byte[] buff = new byte[VectorIndexSize];\n            read(HeaderInfoLength + idx, buff);\n            sPtr = LittleEndian.getUint32(buff, 0);\n            ePtr = LittleEndian.getUint32(buff, 4);\n        }\n\n        // System.out.printf(\"sPtr: %d, ePtr: %d\\n\", sPtr, ePtr);\n        // @Note: ptr validate, zero ptr means source data missing\n        // so we could just stop here and return an empty string.\n        if (sPtr == 0 || ePtr == 0) {\n            return \"\";\n        }\n\n        // binary search the segment index block to get the region info\n        final int bytes = ip.length, dBytes = ip.length << 1;\n        final int segIndexSize = version.segmentIndexSize;\n        final byte[] buff = new byte[segIndexSize];\n        int dataLen = 0;\n        long dataPtr = 0, l = 0, h = (ePtr - sPtr) / segIndexSize;\n        while (l <= h) {\n            long m = (l + h) >> 1;\n            long p = sPtr + m * segIndexSize;\n\n            // read the segment index\n            read(p, buff);\n            if (version.ipSubCompare(ip, buff, 0) < 0) {\n                h = m - 1;\n            } else if (version.ipSubCompare(ip, buff, bytes) > 0) {\n                l = m + 1;\n            } else {\n                dataLen = LittleEndian.getUint16(buff, dBytes);\n                dataPtr = LittleEndian.getUint32(buff, dBytes + 2);\n                break;\n            }\n        }\n\n        // empty match interception\n        // System.out.printf(\"dataLen: %d, dataPtr: %d\\n\", dataLen, dataPtr);\n        if (dataLen == 0) {\n            return \"\";\n        }\n\n        // load and return the region data\n        final byte[] regionBuff = new byte[dataLen];\n        read(dataPtr, regionBuff);\n        return new String(regionBuff, \"utf-8\");\n    }\n\n    protected void read(long offset, byte[] buffer) throws IOException {\n        // check the in-memory buffer first\n        if (contentBuff != null) {\n            contentBuff.copy(offset, buffer, 0, buffer.length);\n            return;\n        }\n\n        // read from the file handle\n        assert handle != null;\n        handle.seek(offset);\n\n        this.ioCount++;\n        int rLen = handle.read(buffer);\n        if (rLen != buffer.length) {\n            throw new IOException(\"incomplete read: read bytes should be \" + buffer.length);\n        }\n    }\n\n    @Override public String toString() {\n        return String.format(\n            \"%s->{version:%s, xdb:%s, vIndex:%s, cBuffer:%s}\", \n            super.toString(),\n            version.name, xdbFile == null ? \"null\" : xdbFile.getAbsolutePath(), \n            vectorIndex == null ? \"null\" : String.valueOf(vectorIndex.length),\n            contentBuff == null ? \"null\" : String.valueOf(contentBuff.length())\n        );\n    }\n\n    // ---\n    // --- static util function\n    // --- read xdb header\n\n    public static Header loadHeader(RandomAccessFile handle) throws IOException {\n        handle.seek(0);\n        final byte[] buff = new byte[HeaderInfoLength];\n        handle.read(buff);\n        return new Header(buff);\n    }\n\n    public static Header loadHeaderFromFile(File xdbFile) throws IOException {\n        final RandomAccessFile handle = new RandomAccessFile(xdbFile, \"r\");\n        final Header header = loadHeader(handle);\n        handle.close();\n        return header;\n    }\n\n    public static Header loadHeaderFromFile(String xdbPath) throws IOException {\n        return loadHeaderFromFile(new File(xdbPath));\n    }\n\n    public static Header loadHeaderFromBuffer(LongByteArray cBuffer) throws IOException {\n        return new Header(cBuffer.slice(0, HeaderInfoLength));\n    }\n\n    // --- read xdb vector index\n\n    public static byte[] loadVectorIndex(RandomAccessFile handle) throws IOException {\n        handle.seek(HeaderInfoLength);\n        int len = VectorIndexRows * VectorIndexCols * VectorIndexSize;\n        final byte[] buff = new byte[len];\n        int rLen = handle.read(buff);\n        if (rLen != len) {\n            throw new IOException(\"incomplete read: read bytes should be \" + len);\n        }\n\n        return buff;\n    }\n\n    public static byte[] loadVectorIndexFromFile(File xdbFile) throws IOException {\n        final RandomAccessFile handle = new RandomAccessFile(xdbFile, \"r\");\n        final byte[] vIndex = loadVectorIndex(handle);\n        handle.close();\n        return vIndex;\n    }\n\n    public static byte[] loadVectorIndexFromFile(String xdbPath) throws IOException {\n        return loadVectorIndexFromFile(new File(xdbPath));\n    }\n\n    public static byte[] loadVectorIndexFromBuffer(LongByteArray cBuffer) throws IOException {\n        final int len = VectorIndexRows * VectorIndexCols * VectorIndexSize;\n        return cBuffer.slice(HeaderInfoLength, len);\n    }\n\n    // --- read xdb content\n\n    // -- load xdb buffer with random access file handle\n    \n    public static LongByteArray loadContent(RandomAccessFile handle) throws IOException {\n        return loadContent(handle, DEFAULT_SLICE_BYTES);\n    }\n\n    public static LongByteArray loadContent(RandomAccessFile handle, final int sliceBytes) throws IOException {\n        handle.seek(0);\n        // check the length and do the buff load\n        long toRead = handle.length();\n        final LongByteArray byteArray = new LongByteArray(sliceBytes);\n        while (toRead > 0) {\n            final byte[] buff = new byte[(int) Math.min(toRead, sliceBytes)];\n            final int rLen = handle.read(buff);\n            if (rLen != buff.length) {\n                throw new IOException(\"incomplete read: read bytes should be \" + buff.length + \", got `\" + rLen + \"`\");\n            }\n\n            byteArray.append(buff);\n            toRead -= rLen;\n        }\n\n        return byteArray;\n    }\n\n    // -- load xdb buffer with xdb file object\n\n    public static LongByteArray loadContentFromFile(File xdbFile) throws IOException {\n        return loadContentFromFile(xdbFile, DEFAULT_SLICE_BYTES);\n    }\n\n    public static LongByteArray loadContentFromFile(File xdbFile, final int sliceBytes) throws IOException {\n        final RandomAccessFile handle = new RandomAccessFile(xdbFile, \"r\");\n        final LongByteArray content = loadContent(handle, sliceBytes);\n        handle.close();\n        return content;\n    }\n\n    // -- load xdb buffer with xdb file path\n\n    public static LongByteArray loadContentFromFile(String xdbPath) throws IOException {\n        return loadContentFromFile(xdbPath, DEFAULT_SLICE_BYTES);\n    }\n\n    public static LongByteArray loadContentFromFile(String xdbPath, final int sliceBytes) throws IOException {\n        return loadContentFromFile(new File(xdbPath), sliceBytes);\n    }\n\n    // load xdb buffer from input stream\n\n    public static LongByteArray loadContentFromInputStream(InputStream is) throws IOException {\n        return loadContentFromInputStream(is, DEFAULT_SLICE_BYTES);\n    }\n\n    public static LongByteArray loadContentFromInputStream(InputStream is, final int sliceBytes) throws IOException {\n        final LongByteArray byteArray = new LongByteArray(sliceBytes);\n        while (true) {\n            boolean done = false;\n\n            // read at most MAX_WRITE_BYTES bytes\n            int rLen, tBytes = 0;\n            final byte[] buff = new byte[sliceBytes];\n            while (true) {\n                rLen = is.read(buff, tBytes, buff.length - tBytes);\n                if (rLen == -1) {\n                    // reach the end of the stream\n                    done = true;\n                    break;\n                } else if (rLen == 0) {\n                    // the entire buff was filled\n                    break;\n                }\n\n                tBytes += rLen;\n            }\n\n            // check and copy the buffer with its actual filled bytes\n            if (tBytes == buff.length) {\n                byteArray.append(buff);\n            } else {\n                final byte[] nBuff = new byte[tBytes];\n                System.arraycopy(buff, 0, nBuff, 0, tBytes);\n                byteArray.append(nBuff);\n            }\n\n            if (done) {\n                break;\n            }\n        }\n\n        return byteArray;\n    }\n\n    // --- verify util function\n\n    // Verify if the current Searcher could be used to search the specified xdb file.\n    // Why do we need this check ?\n    // The future features of the xdb impl may cause the current searcher not able to work properly.\n    //\n    // @Note: You Just need to check this ONCE when the service starts\n    // Or use another process (eg, A command) to check once Just to confirm the suitability.\n    public static void verify(Header header, long fileBytes) throws IOException, XdbException {\n        // get the runtime ptr bytes\n        int runtimePtrBytes = 0;\n        if (header.version == STRUCTURE_20) {\n            runtimePtrBytes = 4;\n        } else if (header.version == STRUCTURE_30) {\n            runtimePtrBytes = header.runtimePtrBytes;\n        } else {\n            throw new XdbException(\"invalid structure version `\" + header.version + \"`\");\n        }\n\n        // 1, confirm the xdb file size\n        // to ensure that the maximum file pointer does not overflow\n        final long maxFilePtr = (1L << (runtimePtrBytes * 8)) - 1;\n        if (fileBytes > maxFilePtr) {\n            throw new XdbException(\"xdb file exceeds the maximum supported bytes: \"+maxFilePtr+\"\");\n        }\n    }\n\n    public static void verify(RandomAccessFile handle) throws IOException, XdbException {\n        verify(loadHeader(handle), handle.length());\n    }\n\n    public static void verifyFromFile(File xdbFile) throws IOException, XdbException {\n        final RandomAccessFile handle = new RandomAccessFile(xdbFile, \"r\");\n        verify(handle);\n        handle.close();\n    }\n\n    public static void verifyFromFile(String xdbPath) throws IOException, XdbException {\n        verifyFromFile(new File(xdbPath));\n    }\n\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\n\npublic class Util\n{\n\n    // parse the specified IP address and return its bytes.\n    // returns: byte[4] for IPv4 and byte[16] for IPv6 and the bytes should be in Big endian order.\n    public static byte[] parseIP(String ip) throws InetAddressException {\n        try {\n            return InetAddress.getByName(ip).getAddress();\n        } catch (UnknownHostException e) {\n            throw new InetAddressException(\"invalid ip address `\"+ip+\"`\");\n        }\n    }\n\n    // convert the byte[] ip to string ip address\n    public static String ipToString(final byte[] ip) {\n        if (ip.length != 4 && ip.length != 16) {\n            return String.format(\"invalid-ip-address-length: %d\", ip.length);\n        }\n\n        try {\n            return InetAddress.getByAddress(ip).getHostAddress();\n        } catch (UnknownHostException e) {\n            return String.format(\"invalid-ip-address `%s`\", ipJoin(ip));\n        }\n    }\n\n    // implode the byte[] ip with its byte value.\n    public static String ipJoin(byte[] ip) {\n        return bytesToString(ip, 0, ip.length);\n    }\n\n    public static String bytesToString(byte[] buff, int offset, int length) {\n        final StringBuffer sb = new StringBuffer();\n        sb.append(\"[\");\n        for (int i = 0; i < length; i++) {\n            if (i > 0) {\n                sb.append(',');\n            }\n            sb.append((buff[offset+i] & 0xFF));\n        }\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n    // compare two byte ip\n    // Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\n    public static int ipCompare(byte[] ip1, byte[] ip2) {\n        return ipSubCompare(ip1, ip2, 0);\n    }\n\n    // compare the ip with the ip in the buffer start from offset\n    // Returns: -1 if ip < buff[offset], 0 if ip == buff[offset], 1 if ip > buff[offset]\n    public static int ipSubCompare(byte[] ip, byte[] buff, int offset) {\n        for (int i = 0; i < ip.length; i++) {\n            // covert the byte to int to sure the uint8 attribute\n            final int i1 = (int)(ip[i] & 0xFF);\n            final int i2 = (int)(buff[offset+i] & 0xFF);\n            if (i1 < i2) {\n                return -1;\n            }\n\n            if (i1 > i2) {\n                return 1;\n            }\n        }\n\n        return 0;\n    }\n\n    public static byte[] ipAddOne(byte[] ip) {\n        final byte[] r = new byte[ip.length];\n        System.arraycopy(ip, 0, r, 0, ip.length);\n        for (int i = ip.length - 1; i >= 0; i--) {\n            final int v = (int)(r[i] & 0xFF);\n            if (v < 255) {    // No overflow\n                r[i]++;\n                break;\n            }\n\n            r[i] = 0;\n        }\n\n        return r;\n    }\n\n    public static byte[] ipSubOne(byte[] ip) {\n        final byte[] r = new byte[ip.length];\n        System.arraycopy(ip, 0, r, 0, ip.length);\n        for (int i = ip.length - 1; i >= 0; i--) {\n            final int v = (int)(r[i] & 0xFF);\n            if (v > 0) {    // No borrow needed\n                r[i]--;\n                break;\n            }\n\n            r[i] = (byte) 0xFF; // borrow from the next byte\n        }\n\n        return r;\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IP version abstract manager (IPv4 & IPv6)\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic abstract class Version {\n    public static final int IPv4VersionNo = 4;\n    public static final int IPv6VersionNo = 6;\n\n    public static final IPv4 IPv4 = new IPv4();\n    public static final IPv6 IPv6 = new IPv6();\n\n    // version id and name\n    public final int id;\n    public final String name;\n    \n    // the numbers of bytes for one IP\n    public final int bytes;\n\n    // segment index size (bytes)\n    public final int segmentIndexSize;\n\n    public Version(int id, String name, int bytes, int segmentIndexSize) {\n        this.id = id;\n        this.name = name;\n        this.bytes = bytes;\n        this.segmentIndexSize = segmentIndexSize;\n    }\n\n    // encode the specified IP bytes to the specified buffer\n    public abstract int putBytes(byte[] buff, int offset, byte[] ip);\n\n    // compare the two IPs with the current version.\n    // Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\n    public int ipCompare(byte[] ip1, byte[] ip2) {\n        return ipSubCompare(ip1, ip2, 0);\n    }\n\n    // @see ipCompare\n    public abstract int ipSubCompare(byte[] ip1, byte[] buff, int offset);\n\n    // parse the version from an name\n    public static final Version fromName(String name) throws Exception {\n        final String n = name.toUpperCase();\n        if (n.equals(\"V4\") || n.equals(\"IPV4\")) {\n            return IPv4;\n        } else if (n.equals(\"V6\") || n.equals(\"IPV6\")) {\n            return IPv6;\n        } else {\n            throw new Exception(\"invalid version name `\"+name+\"`\");\n        }\n    }\n\n    // parse the version from header\n    public static final Version fromHeader(Header header) throws XdbException {\n        // Old 2.0 structure with IPv4 supports ONLY.\n        if (header.version == Searcher.STRUCTURE_20) {\n            return IPv4;\n        }\n\n        // structure 3.0 after IPv6 supporting\n        if (header.version != Searcher.STRUCTURE_30) {\n            throw new XdbException(\"invalid xdb structure version `\"+header.version+\"`\");\n        }\n\n        if (header.ipVersion == IPv4VersionNo) {\n            return IPv4;\n        } else if (header.ipVersion == IPv6VersionNo) {\n            return IPv6;\n        } else {\n            throw new XdbException(\"invalid ip version number `\" + header.ipVersion + \"`\");\n        }\n    }\n\n    @Override public String toString() {\n        return String.format(\"{Id:%d, Name:%s, Bytes:%d, IndexSize: %d}\", id, name, bytes, segmentIndexSize);\n    }\n}"
  },
  {
    "path": "binding/java/src/main/java/org/lionsoul/ip2region/xdb/XdbException.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\npublic class XdbException extends Exception {\n\n    public XdbException(String str) {\n        super(str);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/service/ConfigTest.java",
    "content": "package org.lionsoul.ip2region.service;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.security.CodeSource;\n\nimport org.junit.Test;\nimport org.lionsoul.ip2region.xdb.Log;\nimport org.lionsoul.ip2region.xdb.XdbException;\n\npublic class ConfigTest {\n\n    private static final Log log = Log.getLogger(ConfigTest.class).setLevel(Log.DEBUG);\n\n    public static final String getDataPath(String xdbFile) {\n        final CodeSource cs = ConfigTest.class.getProtectionDomain().getCodeSource();\n        if (cs != null) {\n            // log.debugf(\"code path: %s\", cs.getLocation().getPath().concat(\"../../../../data/\"));\n            return cs.getLocation().getPath().concat(\"../../../../data/\").concat(xdbFile);\n        } else {\n            return \"../../../../data/\".concat(xdbFile);\n        }\n    }\n\n    @Test\n    public void testBuildV4Config() throws IOException, XdbException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setXdbPath(getDataPath(\"ip2region_v4.xdb\"))\n            .setSearchers(20)\n            .asV4();\n        log.debugf(\"builded config: %s\", v4Config);\n    }\n\n    @Test\n    public void testBuildV4ConfigFromFile() throws IOException, XdbException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbFile(new File(getDataPath(\"ip2region_v4.xdb\")))\n            .setSearchers(20)\n            .asV4();\n        log.debugf(\"builded config: %s\", v4Config);\n    }\n\n    @Test\n    public void testBuildV4ConfigFromInputStream() throws IOException, XdbException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbInputStream(new FileInputStream(getDataPath(\"ip2region_v4.xdb\")))\n            .setSearchers(20)\n            .asV4();\n        log.debugf(\"builded config: buffs.size=%d, %s\", v4Config.cBuffer.size(), v4Config);\n    }\n\n    @Test\n    public void testBuildV4SliceBytes() throws IOException, XdbException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.FullCache)\n            .setCacheSliceBytes(1024 * 1024)  // 1MiB\n            .setXdbPath(getDataPath(\"ip2region_v4.xdb\"))\n            .setSearchers(20)\n            .asV4();\n        log.debugf(\"builded config: buffs.size=%d, %s\", v4Config.cBuffer.size(), v4Config);\n    }\n\n    // --- IPv6\n\n    @Test\n    public void testBuildV6Config() throws IOException, XdbException, InvalidConfigException {\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setXdbPath(getDataPath(\"ip2region_v6.xdb\"))\n            .setSearchers(20)\n            .asV6();\n        log.debugf(\"builded config: %s\", v6Config);\n    }\n\n    @Test\n    public void testBuildV6ConfigFromFile() throws IOException, XdbException, InvalidConfigException {\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbFile(new File(getDataPath(\"ip2region_v6.xdb\")))\n            .setSearchers(20)\n            .asV6();\n        log.debugf(\"builded config: %s\", v6Config);\n    }\n\n    @Test\n    public void testBuildV6ConfigFromInputStream() throws IOException, XdbException, InvalidConfigException {\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbInputStream(new FileInputStream(getDataPath(\"ip2region_v6.xdb\")))\n            .setSearchers(20)\n            .asV6();\n        log.debugf(\"builded config: buffs.size=%d, %s\", v6Config.cBuffer.size(), v6Config);\n    }\n\n    @Test\n    public void testBuildV6SliceBytes() throws IOException, XdbException, InvalidConfigException {\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setCacheSliceBytes(1024 * 1024 * 4)    // 4 MiB\n            .setXdbInputStream(new FileInputStream(getDataPath(\"ip2region_v6.xdb\")))\n            .setSearchers(20)\n            .asV6();\n        log.debugf(\"builded config: buffs.size=%d, %s\", v6Config.cBuffer.size(), v6Config);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/service/Ip2RegionTest.java",
    "content": "package org.lionsoul.ip2region.service;\n\nimport static org.junit.Assert.assertEquals;\n\nimport java.io.IOException;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport org.junit.Test;\nimport org.lionsoul.ip2region.xdb.InetAddressException;\nimport org.lionsoul.ip2region.xdb.Log;\nimport org.lionsoul.ip2region.xdb.Util;\nimport org.lionsoul.ip2region.xdb.XdbException;\n\npublic class Ip2RegionTest {\n\n    private static final Log log = Log.getLogger(Ip2RegionTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void TestConfigCreate() throws IOException, XdbException, InetAddressException, InterruptedException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.NoCache)\n            .setSearchers(10)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setSearchers(10)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v6.xdb\"))\n            .asV6();\n\n        byte[] v4Bytes = Util.parseIP(\"113.92.157.29\");\n        byte[] v6Bytes = Util.parseIP(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\");\n        final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);\n        for (int i = 0; i < 50; i++) {\n            v4Bytes = Util.ipAddOne(v4Bytes);\n            v6Bytes = Util.ipAddOne(v6Bytes);\n            final String v4Region = ip2Region.search(v4Bytes);\n            final String v6Region = ip2Region.search(v6Bytes);\n            log.debugf(\"search(%s)=%s, search(%s)=%s\", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);\n        }\n\n        ip2Region.close();\n        log.debugf(\"ip2region closed gracefully\");\n    }\n\n    @Test\n    public void TestPathCreate() throws InetAddressException, IOException, XdbException, InterruptedException, InvalidConfigException {\n        byte[] v4Bytes = Util.parseIP(\"113.92.157.29\");\n        byte[] v6Bytes = Util.parseIP(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\");\n        final Ip2Region ip2Region = Ip2Region.create(ConfigTest.getDataPath(\"ip2region_v4.xdb\"), ConfigTest.getDataPath(\"ip2region_v6.xdb\"));\n        for (int i = 0; i < 50; i++) {\n            v4Bytes = Util.ipAddOne(v4Bytes);\n            v6Bytes = Util.ipAddOne(v6Bytes);\n            final String v4Region = ip2Region.search(v4Bytes);\n            final String v6Region = ip2Region.search(v6Bytes);\n            log.debugf(\"search(%s)=%s, search(%s)=%s\", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);\n        }\n\n        ip2Region.close();\n        log.debugf(\"ip2region closed gracefully\");\n    }\n\n    @Test\n    public void TestInMemSearch() throws IOException, XdbException, InetAddressException, InterruptedException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.BufferCache)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v6.xdb\"))\n            .asV6();\n\n        byte[] v4Bytes = Util.parseIP(\"113.92.157.29\");\n        byte[] v6Bytes = Util.parseIP(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\");\n        final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);\n        for (int i = 0; i < 50; i++) {\n            v4Bytes = Util.ipAddOne(v4Bytes);\n            v6Bytes = Util.ipAddOne(v6Bytes);\n            final String v4Region = ip2Region.search(v4Bytes);\n            final String v6Region = ip2Region.search(v6Bytes);\n            log.debugf(\"search(%s)=%s, search(%s)=%s\", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);\n        }\n\n        ip2Region.close();\n        log.debugf(\"ip2region closed gracefully\");\n    }\n\n    @Test\n    public void TestConcurrentCall() throws IOException, XdbException, InetAddressException, InterruptedException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setSearchers(15)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setSearchers(15)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v6.xdb\"))\n            .asV6();\n\n        byte[] v4Bytes = Util.parseIP(\"113.92.157.29\");\n        byte[] v6Bytes = Util.parseIP(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\");\n        final int threads = 100;\n        final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);\n        final CountDownLatch latch = new CountDownLatch(threads);\n        final AtomicInteger count = new AtomicInteger(0);\n        final long tStart = System.nanoTime();\n        for (int i = 0; i < threads; i++) {\n            final Runnable t = new Runnable() {\n                @Override\n                public void run() {\n                    for (int i = 0; i < 5000; i++) {\n                        final byte[] ipBytes = i % 2 == 0 ? v4Bytes : v6Bytes;\n                        try {\n                            final String region = ip2Region.search(ipBytes);\n                            if (ipBytes.length == 4) {\n                                assertEquals(\"v4 region not equals\", region, \"中国|广东省|深圳市|电信|CN\");\n                            } else {\n                                assertEquals(\"v6 region not equals\", region, \"中国|广东省|深圳市|电信|CN\");\n                            }\n                        } catch (InetAddressException | IOException | InterruptedException e) {\n                            log.errorf(\"failed to search(%s): %s\", Util.ipToString(ipBytes), e.getMessage());\n                        }\n\n                        count.incrementAndGet();\n                    }\n\n                    latch.countDown();\n                }\n            };\n            t.run();\n        }\n\n        latch.await();\n        final long costs = System.nanoTime() - tStart;\n        log.debugf(\"%d searches finished in %dms, avg took: %dµs\", count.get(), costs / 1000_000, costs / count.get() / 1000);\n        ip2Region.close();\n        log.debugf(\"ip2region closed gracefully\");\n    }\n\n    @Test\n    public void TestV4Only() throws IOException, XdbException, InetAddressException, InterruptedException, InvalidConfigException {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.NoCache)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n        byte[] v4Bytes = Util.parseIP(\"113.92.157.29\");\n        byte[] v6Bytes = Util.parseIP(\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\");\n        final Ip2Region ip2Region = Ip2Region.create(v4Config, null);\n        for (int i = 0; i < 10; i++) {\n            v4Bytes = Util.ipAddOne(v4Bytes);\n            final String v4Region = ip2Region.search(v4Bytes);\n            final String v6Region = ip2Region.search(v6Bytes);\n            log.debugf(\"search(%s)=%s, search(%s)=%s\", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);\n        }\n\n        ip2Region.close();\n        log.debugf(\"ip2region closed gracefully\");\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/service/SearcherPoolTest.java",
    "content": "package org.lionsoul.ip2region.service;\n\nimport org.junit.Test;\nimport org.lionsoul.ip2region.xdb.Log;\nimport org.lionsoul.ip2region.xdb.Searcher;\n\npublic class SearcherPoolTest {\n\n    private static final Log log = Log.getLogger(SearcherPoolTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testV4SearcherPool() throws Exception {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setSearchers(5)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n\n        final String ipStr = \"58.250.36.41\";\n        final SearcherPool v4Pool = SearcherPool.create(v4Config);\n        for (int i = 0; i < 20; i++) {\n            final Searcher searcher = v4Pool.borrowSearcher();\n            log.debugf(\"borrowed searcher %d: %s\", i, searcher.toString());\n            final String region = searcher.search(ipStr);\n            log.debugf(\"search(%s)=%s\", ipStr, region);\n            v4Pool.returnSearcher(searcher);\n            log.debugf(\"return searcher %d\", i);\n        }\n\n        v4Pool.close();\n        log.debugf(\"v4 searcher pool closed gracefully\");\n    }\n\n    @Test\n    public void testInMemV4SearcherPool() throws Exception {\n        final Config v4Config = Config.custom()\n            .setCachePolicy(Config.FullCache)\n            .setSearchers(5)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v4.xdb\"))\n            .asV4();\n\n\n        final String ipStr = \"58.250.36.41\";\n        final SearcherPool v4Pool = SearcherPool.create(v4Config);\n        for (int i = 0; i < 20; i++) {\n            final Searcher searcher = v4Pool.borrowSearcher();\n            log.debugf(\"borrowed searcher %d: %s\", i, searcher.toString());\n            final String region = searcher.search(ipStr);\n            log.debugf(\"search(%s)=%s\", ipStr, region);\n            v4Pool.returnSearcher(searcher);\n            log.debugf(\"return searcher %d\", i);\n        }\n\n        v4Pool.close();\n        log.debugf(\"v4 searcher pool closed gracefully\");\n    }\n\n    @Test\n    public void testV6SearcherPool() throws Exception {\n        final Config v6Config = Config.custom()\n            .setCachePolicy(Config.VIndexCache)\n            .setSearchers(5)\n            .setXdbPath(ConfigTest.getDataPath(\"ip2region_v6.xdb\"))\n            .asV6();\n\n\n        final String ipStr = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";\n        final SearcherPool v4Pool = SearcherPool.create(v6Config);\n        for (int i = 0; i < 20; i++) {\n            final Searcher searcher = v4Pool.borrowSearcher();\n            log.debugf(\"borrowed searcher %d: %s\", i, searcher.toString());\n            final String region = searcher.search(ipStr);\n            log.debugf(\"search(%s)=%s\", ipStr, region);\n            v4Pool.returnSearcher(searcher);\n            log.debugf(\"return searcher %d\", i);\n        }\n\n        v4Pool.close();\n        log.debugf(\"v6 searcher pool closed gracefully\");\n    }\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/xdb/BufferTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.security.CodeSource;\n\nimport org.junit.Test;\n\npublic class BufferTest {\n\n    private static final Log log = Log.getLogger(VersionTest.class).setLevel(Log.DEBUG);\n\n    public static final String getDataPath(String xdbFile) {\n        final CodeSource cs = BufferTest.class.getProtectionDomain().getCodeSource();\n        if (cs != null) {\n            // log.debugf(\"code path: %s\", cs.getLocation().getPath().concat(\"../../../../data/\"));\n            return cs.getLocation().getPath().concat(\"../../../../data/\").concat(xdbFile);\n        } else {\n            return \"../../../../data/\".concat(xdbFile);\n        }\n    }\n\n    // --- v4\n\n    @Test\n    public void testV4InputStreamBuffer() throws Exception {\n        final LongByteArray cBuffer = Searcher.loadContentFromInputStream(\n            new FileInputStream(getDataPath(\"ip2region_v4.xdb\"))\n        );\n        log.debugf(\"cBuffer->{length:%d, size:%d}\", cBuffer.length(), cBuffer.size());\n    }\n\n    @Test\n    public void testV4FixedBuffer() throws Exception {\n        final LongByteArray cBuffer = Searcher.loadContentFromFile(\n            new File(getDataPath(\"ip2region_v4.xdb\")), 2 * 1024 * 1024\n        );\n        final Header header = Searcher.loadHeaderFromBuffer(cBuffer);\n        log.debugf(\"cBuffer->{length:%d, size:%d}\", cBuffer.length(), cBuffer.size());\n        log.debugf(\"Header->%s\", header);\n    }\n\n    @Test\n    public void testV4BufferAssert() throws Exception {\n        final LongByteArray m2Bufer = Searcher.loadContentFromFile(\n            new File(getDataPath(\"ip2region_v4.xdb\")), 2 * 1024 * 1024\n        );\n        final LongByteArray m5Bufer = Searcher.loadContentFromFile(\n            new File(getDataPath(\"ip2region_v4.xdb\")), 5 * 1024 * 1024\n        );\n\n        final int[] offsets = new int[]{0, 10, 512, 1024, 39672, 1024 * 1024 * 2};\n        for (int idx : offsets) {\n            final long m2Val = m2Bufer.getUint32(idx);\n            final long m5Val = m5Bufer.getUint32(idx);\n            log.debugf(\"m2Buffer[%8d:4]: %10d, m5Buffer[%8d:4]: %10d, equals ? %s\", idx, m2Val, idx, m5Val, m2Val == m5Val ? \"true\" : \"false\");\n        }\n    }\n\n    @Test\n    public void testV4BufferEOF() throws IOException {\n        final LongByteArray buffer = Searcher.loadContentFromFile(\n            new File(getDataPath(\"ip2region_v4.xdb\")), 2 * 1024 * 1024\n        );\n\n        try {\n            buffer.append(new byte[1024]);\n        } catch (IOException e) {\n            log.debugf(\"failed to append: %s\", e.getMessage());\n        }\n    }\n\n    // --- v6\n\n    @Test\n    public void testV6InputStreamBuffer() throws Exception {\n        final LongByteArray cBuffer = Searcher.loadContentFromInputStream(\n            new FileInputStream(getDataPath(\"ip2region_v6.xdb\"))\n        );\n        log.debugf(\"cBuffer->{length:%d, size:%d}\", cBuffer.length(), cBuffer.size());\n    }\n\n    @Test\n    public void testV6FixedBuffer() throws Exception {\n        final LongByteArray cBuffer = Searcher.loadContentFromFile(\n            new File(getDataPath(\"ip2region_v6.xdb\")), 5 * 1024 * 1024\n        );\n        final Header header = Searcher.loadHeaderFromBuffer(cBuffer);\n        log.debugf(\"cBuffer->{length:%d, size:%d}\", cBuffer.length(), cBuffer.size());\n        log.debugf(\"Header->%s\", header);\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/xdb/IPv4Test.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport org.junit.Test;\n\npublic class IPv4Test {\n\n    private static final Log log = Log.getLogger(IPv4Test.class);\n\n    @Test\n    public void testIpSubCompare() throws InetAddressException {\n        final byte[] sip = Util.parseIP(\"0.255.255.255\");\n        final byte[] eip = Util.parseIP(\"1.0.0.2\");\n        final byte[] buff = new byte[Version.IPv4.segmentIndexSize];\n        Version.IPv4.putBytes(buff, 0, sip);\n        Version.IPv4.putBytes(buff, 4, eip);\n        log.infof(\"bytesToString(buff): %s\", Util.bytesToString(buff, 0, 8));\n\n        final byte[] ip = Util.parseIP(\"1.0.0.0\");\n\n        // compare the sip\n        log.infof(\"ipSubCompare(%s, %s): %d\", \n            Util.ipToString(ip), \n            Util.bytesToString(buff, 0, 4), \n            Util.ipSubCompare(ip, buff, 0)\n        );\n        log.infof(\"IPv4.ipSubCompare(%s, %s): %d\", \n            Util.ipToString(ip), \n            Util.bytesToString(buff, 0, 4), \n            Version.IPv4.ipSubCompare(ip, buff, 0)\n        );\n\n        // compare the eip\n        log.infof(\"ipSubCompare(%s, %s): %d\", \n            Util.ipToString(ip), \n            Util.bytesToString(buff, 4, 4), \n            Util.ipSubCompare(ip, buff, 4)\n        );\n        log.infof(\"IPv4.ipSubCompare(%s, %s): %d\", \n            Util.ipToString(ip), \n            Util.bytesToString(buff, 4, 4), \n            Version.IPv4.ipSubCompare(ip, buff, 4)\n        );\n    }\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\npublic class LittleEndianTest {\n\n    private static final Log log = Log.getLogger(LittleEndianTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testAll() {\n        final byte[] buff = new byte[14];\n\n        // encode\n        // do the put\n        LittleEndian.put(buff, 0, 1L, 4);\n        LittleEndian.put(buff, 4, 2L, 4);\n\n        // putUint32\n        LittleEndian.putUint16(buff, 8, 24);\n        LittleEndian.putUint32(buff, 10, 1024L);\n\n        // decode\n        assertEquals(LittleEndian.getUint32(buff, 0), 1);\n        assertEquals(LittleEndian.getUint32(buff, 4), 2);\n        assertEquals(LittleEndian.getUint16(buff, 8), 24);\n        assertEquals(LittleEndian.getUint32(buff, 10), 1024);\n\n        log.debugf(\"uint32(buff, 0): %d\", LittleEndian.getUint32(buff, 0));\n        log.debugf(\"uint32(buff, 4): %d\", LittleEndian.getUint32(buff, 4));\n        log.debugf(\"int2(buff, 8): %d\", LittleEndian.getUint16(buff, 8));\n        log.debugf(\"uint32(buff, 10): %d\", LittleEndian.getUint32(buff, 10));\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport org.junit.Test;\n\npublic class UtilTest {\n\n    private static final Log log = Log.getLogger(UtilTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testCheckIP() throws InetAddressException {\n        final String[] ips = new String[]{\n            \"192.168.1.102\",\n            \"219.133.111.87\",\n            \"::\",\n            \"3000::\",\n            \"::1001:ffff\",\n            \"2001:2:0:ffff:ffff:ffff:ffff:ffff\",\n            \"::ffff:114.114.114.114\"\n        };\n\n        for (String ip : ips) {\n            final byte[] ipBytes = Util.parseIP(ip);\n            log.debugf(\"%s(v=%s) => %s\", ip, Util.ipJoin(ipBytes), Util.ipToString(ipBytes));\n        }\n    }\n\n    @Test\n    public void testIpCompare() throws InetAddressException {\n        final String[][] ipPairs = new String[][]{\n            {\"1.0.0.0\", \"1.0.0.1\"},\n            {\"192.168.1.101\", \"192.168.1.90\"},\n            {\"219.133.111.87\", \"114.114.114.114\"},\n            {\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"},\n            {\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\"},\n            {\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\"}\n        };\n\n        for (String[] ips : ipPairs) {\n            final byte[] ip1 = Util.parseIP(ips[0]);\n            final byte[] ip2 = Util.parseIP(ips[1]);\n            log.debugf(\"compare(%s, %s): %d\", ips[0], ips[1], Util.ipCompare(ip1, ip2));\n        }\n    }\n\n    @Test\n    public void testIpSubCompare() throws InetAddressException {\n        final String[][] ipPairs = new String[][] {\n            {\"1.0.0.0\", \"1.0.0.1\"},\n            {\"192.168.1.100\", \"192.168.2.100\"},\n            {\"10.100.1.10\", \"11.100.2.10\"},\n            {\"11.100.1.10\", \"10.100.2.10\"}\n        };\n\n        for (final String[] ips : ipPairs) {\n            final byte[] ip1 = Util.parseIP(ips[0]);\n            final byte[] ip2 = Util.parseIP(ips[1]);\n            log.debugf(\"ipSubCompare(%s, %s): %d\", Util.ipToString(ip1), Util.ipToString(ip2), Util.ipSubCompare(ip1, ip2, 0));\n        }\n    }\n\n}\n"
  },
  {
    "path": "binding/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\npublic class VersionTest {\n\n    private static final Log log = Log.getLogger(VersionTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testFromName() throws Exception {\n        final String[] vers = new String[]{\"IPv4\", \"IPv6\"};\n        final Version v4 = Version.fromName(vers[0]);\n        assertEquals(v4.name, vers[0]);\n        final Version v6 = Version.fromName(vers[1]);\n        assertEquals(v6.name, vers[1]);\n\n        log.debugf(\"v4: %s\", v4);\n        log.debugf(\"v6: %s\", v6);\n    }\n\n    @Test\n    public void testFromHeader() throws XdbException {\n        // create the header buffer\n        final byte[] buff1 = new byte[Searcher.HeaderInfoLength];\n        // structure version\n        LittleEndian.put(buff1, 0, 2, 2);\n        LittleEndian.put(buff1, 16, Version.IPv4VersionNo, 2);\n\n        final byte[] buff2 = new byte[Searcher.HeaderInfoLength];\n        LittleEndian.put(buff2, 0, 3, 2);\n        LittleEndian.put(buff2, 16, Version.IPv6VersionNo, 2);\n\n        final Version ver1 = Version.fromHeader(new Header(buff1));\n        final Version ver2 = Version.fromHeader(new Header(buff2));\n        log.debugf(\"ver1: %s\", ver1.toString());\n        log.debugf(\"ver2: %s\", ver2.toString());\n    }\n\n}\n"
  },
  {
    "path": "binding/javascript/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region JavaScript Query Client\n\n# Usage\n\n### Install `ip2region.js`\n\n```bash\nnpm install ip2region.js --save \n```\n\n### About Query API\n\nThe prototype of the Query API is:\n\n```javascript\n// Query via a string IP or a binary IP (Buffer type) parsed by parseIP\nsearch(ip: string | Buffer): string;\n```\n\nIf an error occurs during the query, an exception will be thrown. If the query is successful, the `region` information string will be returned. If the specified IP cannot be found, an empty string `\"\"` will be returned.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. The usage is as follows:\n\n```javascript\nimport {IPv4, IPv6} from 'ip2region.js';\n\n// For IPv4: Set xdb path to the v4 xdb file, and set IP version to Version.IPv4\nlet dbPath  = \"../../data/ip2region_v4.xdb\";  // or your ipv4 xdb path\nlet version = IPv4;\n\n// For IPv6: Set xdb path to the v6 xdb file, and set IP version to Version.IPv6\nlet dbPath  = \"../../data/ip2region_v6.xdb\";  // or your ipv6 xdb path\nlet version = IPv6;\n\n// The IP version of the xdb specified by dbPath must match the version specified; otherwise, an error will occur during execution.\n// Note: The following demonstrations directly use the dbPath and version variables.\n```\n\n### File Verification\n\nIt is recommended that you proactively verify the applicability of the xdb file. Some new features in the future may cause the current Searcher version to be incompatible with the xdb file you are using. Verification helps avoid unpredictable errors during runtime. You do not need to verify every time; for example, verify once when the service starts or manually call the command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```javascript\nimport {verifyFromFile} from 'ip2region.js';\n\ntry {\n    verifyFromFile(dbPath);\n} catch (e) {\n    // Applicability verification failed!!!\n    // The current query client implementation is not applicable for queries on the xdb file specified by dbPath.\n    // You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation that fits dbPath.\n    console.log(`binding is not applicable for xdb file '${dbPath}': ${e.message}`);\n    return;\n}\n\n// Verification passed. The current Searcher can be safely used for query operations on the xdb pointed to by dbPath.\n```\n\n### File-Only Query\n\n```javascript\nimport {newWithFileOnly} from 'ip2region.js';\n\n// 1. Create a file-only query object using the version and dbPath mentioned above\nlet searcher;\ntry {\n    searcher = newWithFileOnly(version, dbPath);\n} catch(e) {\n    console.log(`failed to newWithFileOnly: ${err.message}`);\n    return;\n}\n\n\n// 2. Query; the interface is the same for both IPv4 and IPv6 addresses\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n\n// 3. Close resources\nsearcher.close();\n\n// Note: Each thread needs to create an independent Searcher object separately\n```\n\n### Caching `VectorIndex`\n\nWe can pre-load the `VectorIndex` data from the `xdb` file and cache it globally. Using a global VectorIndex cache every time a Searcher object is created can reduce one fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```javascript\nimport {loadVectorIndexFromFile, newWithVectorIndex} from 'ip2region.js';\n\n// 1. Pre-load VectorIndex cache from dbPath and keep this data as a global variable for subsequent repeated use.\nlet vIndex;\ntry {\n    vIndex = loadVectorIndexFromFile(dbPath);\n} catch (e) {\n    console.log(`failed to load vector index from ${dbPath}: ${e.message}`);\n    return;\n}\n\n// 2. Create a query object with VectorIndex cache using the global vIndex.\nlet searcher;\ntry {\n    searcher = newWithVectorIndex(version, dbPath, vIndex);\n} catch(e) {\n    console.log(`failed to newWithVectorIndex: ${err.message}`);\n    return;\n}\n\n\n// 3. Query; the interface is the same for both IPv4 and IPv6 addresses\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n\n// 4. Close resources\nsearcher.close();\n\n// Note: Each thread needs to create a separate independent Searcher object, but they all share the global read-only vIndex cache.\n```\n\n### Caching the entire `xdb` file\n\nWe can also pre-load the data of the entire xdb file into memory and then create a query object based on this data to achieve a completely memory-based query, similar to the previous memory search.\n\n```javascript\nimport {loadContentFromFile, newWithBuffer} from 'ip2region.js';\n\n// 1. Load the entire xdb from dbPath into memory.\nlet cBuffer;\ntry {\n    cBuffer = loadContentFromFile(dbPath);\n} catch (e) {\n    console.log(`failed to load content from ${dbPath}: ${e.message}`);\n    return;\n}\n\n// 2. Use the cBuff above to create a completely memory-based query object.\nlet searcher;\ntry {\n    searcher = newWithBuffer(version, cBuffer);\n} catch(e) {\n    console.log(`failed to newWithBuffer: ${err.message}`);\n    return;\n}\n\n// 3. Query; the interface is the same for both IPv4 and IPv6 addresses\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n        \n// 4. Close resources - This searcher object is safe for concurrent use; close the searcher only when the entire service shuts down.\n// searcher.close();\n\n// Note: For concurrent use, the query object created with the entire xdb data cache can be safely used concurrently, meaning you can make this searcher object a global object for cross-thread access.\n```\n\n# Query Test\n\nYou can test queries using the `node tests/search.app.js` command:\n\n```bash\n➜  javascript git:(fr_javascript_ipv6) node tests/search.app.js                                                                 \nusage: Usage node tests/search.app.js [command options]\n\nip2region search script\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\nExample: Using the default data/ip2region_v4.xdb file for IPv4 query testing:\n\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/search.app.js --db=../../data/ip2region_v4.xdb                                \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 657.035 μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 2, took: 169.927 μs}\n```\n\nExample: Using the default data/ip2region_v6.xdb file for IPv6 query testing:\n\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/search.app.js --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 98.953 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 287.703 μs}\n```\n\nEnter an IP to perform a query test. You can also set `cache-policy` to file/vectorIndex/content respectively to test the effects of the three different cache implementations.\n\n# bench Test\n\nYou can perform a bench test via the `node tests/bench.app.js` command, which ensures the `xdb` file is error-free and evaluates query performance:\n\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/bench.app.js \nusage: Usage node tests/bench.app.js [command options]\n\nip2region bench script\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --src SRC             source ip text file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\nExample: Perform an IPv4 bench test using the default data/ip2region_v4.xdb and data/ipv4_source.txt files:\n\n```bash\nnode tests/bench.app.js --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\nExample: Perform an IPv6 bench test using the default data/ip2region_v6.xdb and data/ipv6_source.txt files:\n\n```bash\nnode tests/bench.app.js --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\nYou can test the effects of the three different cache implementations by setting `cache-policy` to file/vectorIndex/content.\n@Note: Ensure the src file used for bench is the same source file used to generate the corresponding xdb file.\n\n### Third-party Library Support:\n\n1. [ts-ip2region2](https://github.com/Steven-Qiang/ts-ip2region2) - Based on the official C extension, providing higher execution efficiency than pure JS.\n"
  },
  {
    "path": "binding/javascript/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region javascript 查询客户端\n\n# 使用方式\n\n### 安装 `ip2region.js`\n```bash\nnpm install ip2region.js --save \n```\n\n### 关于查询 API\n查询 API 的原型为：\n```javascript \n// 通过字符串 IP 或者 parseIP 解析得到的二进制 IP (Buffer类型) 进行查询\nsearch(ip: string | Buffer): string;\n```\n如果查询出错会抛异常，查询成功则会返回字符的 `region` 信息，如果指定的 IP 查询不到则会返回空字符串 `\"\"`。\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```javascript\nimport {IPv4, IPv6} from 'ip2region.js';\n\n// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 Version.IPv4\nlet dbPath  = \"../../data/ip2region_v4.xdb\";  // 或者你的 ipv4 xdb 的路径\nlet version = IPv4;\n\n// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 Version.IPv6\nlet dbPath  = \"../../data/ip2region_v6.xdb\";  // 或者你的 ipv6 xdb 路径\nlet version = IPv6;\n\n// dbPath 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n// 备注：以下演示直接使用 dbPath 和 version 变量\n```\n\n### 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```javascript\nimport {verifyFromFile} from 'ip2region.js';\n\ntry {\n    verifyFromFile(dbPath);\n} catch (e) {\n    // 适用性验证失败！！！\n    // 当前查询客户端实现不适用于 dbPath 指定的 xdb 文件的查询.\n    // 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 dbPath 的 Searcher 实现。\n    console.log(`binding is not applicable for xdb file '${dbPath}': ${e.message}`);\n    return;\n}\n\n// 验证通过，当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n\n```javascript\nimport {newWithFileOnly} from 'ip2region.js';\n\n// 1，使用上述的 version 和 dbPath 创建完全基于文件的查询对象\nlet searcher;\ntry {\n    searcher = newWithFileOnly(version, dbPath);\n} catch(e) {\n    console.log(`failed to newWithFileOnly: ${err.message}`);\n    return;\n}\n\n\n// 2、查询，IPv4 或者 IPv6 的地址都是同一个接口\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n\n// 3、关闭资源\nsearcher.close();\n\n// 备注：每个线程需要单独创建一个独立的 Searcher 对象\n```\n\n### 缓存 `VectorIndex` 索引\n\n我们可以提前从 `xdb` 文件中加载出来 `VectorIndex` 数据，然后全局缓存，每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作，从而加速查询，减少 IO 压力。\n```javascript\nimport {loadVectorIndexFromFile, newWithVectorIndex} from 'ip2region.js';\n\n// 1、从 dbPath 中预先加载 VectorIndex 缓存，并且把这个得到的数据作为全局变量，后续反复使用。\nlet vIndex;\ntry {\n    vIndex = loadVectorIndexFromFile(dbPath);\n} catch (e) {\n    console.log(`failed to load vector index from ${dbPath}: ${e.message}`);\n    return;\n}\n\n// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。\nlet searcher;\ntry {\n    searcher = newWithVectorIndex(version, dbPath, vIndex);\n} catch(e) {\n    console.log(`failed to newWithVectorIndex: ${err.message}`);\n    return;\n}\n\n\n// 3、查询，IPv4 或者 IPv6 的地址都是同一个接口\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n\n// 4、关闭资源\nsearcher.close();\n\n// 备注：每个线程需要单独创建一个独立的 Searcher 对象，但是都共享全局的只读 vIndex 缓存。\n```\n\n### 缓存整个 `xdb` 文件\n\n我们也可以预先加载整个 xdb 文件的数据到内存，然后基于这个数据创建查询对象来实现完全基于内存的查询，类似之前的 memory search。\n```javascript\nimport {loadContentFromFile, newWithBuffer} from 'ip2region.js';\n\n// 1、从 dbPath 加载整个 xdb 到内存。\nlet cBuffer;\ntry {\n    cBuffer = loadContentFromFile(dbPath);\n} catch (e) {\n    console.log(`failed to load content from ${dbPath}: ${e.message}`);\n    return;\n}\n\n// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。\nlet searcher;\ntry {\n    searcher = newWithBuffer(version, cBuffer);\n} catch(e) {\n    console.log(`failed to newWithBuffer: ${err.message}`);\n    return;\n}\n\n// 3、查询，IPv4 或者 IPv6 的地址都是同一个接口\nlet ip = \"1.2.3.4\";\n// ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\";  // IPv6\ntry {\n    let region = await searcher.search(ip);\n    console.log(`search(${ip}): {region: ${region}`);\n} catch(e) {\n    console.log(`${err.message}`);\n}\n        \n// 4、关闭资源 - 该 searcher 对象可以安全用于并发，等整个服务关闭的时候再关闭 searcher\n// searcher.close();\n\n// 备注：并发使用，用整个 xdb 数据缓存创建的查询对象可以安全的用于并发，也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。\n```\n\n\n# 查询测试\n\n可以通过 `node tests/search.app.js` 命令来测试查询：\n```bash\n➜  javascript git:(fr_javascript_ipv6) node tests/search.app.js                                                                 \nusage: Usage node tests/search.app.js [command options]\n\nip2region search script\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 文件进行 IPv4 的查询测试：\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/search.app.js --db=../../data/ip2region_v4.xdb                                \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 657.035 μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 2, took: 169.927 μs}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 文件进行 IPv6 的查询测试：\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/search.app.js --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 98.953 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 287.703 μs}\n```\n\n输入 ip 即可进行查询测试，也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。\n\n\n# bench 测试\n\n可以通过 `node tests/bench.app.js` 命令来进行 bench 测试，一方面确保 `xdb` 文件没有错误，一方面可以评估查询性能：\n```bash\n➜  javascript git:(fr_javascript_ipv6) ✗ node tests/bench.app.js \nusage: Usage node tests/bench.app.js [command options]\n\nip2region bench script\n\noptional arguments:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --src SRC             source ip text file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 文件进行 IPv4 的 bench 测试：\n```bash\nnode tests/bench.app.js --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 文件进行 IPv6 的 bench 测试：\n```bash\nnode tests/bench.app.js --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\n可以通过分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。\n@Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。\n\n### 第三方库支持：\n1. [ts-ip2region2](https://github.com/Steven-Qiang/ts-ip2region2) - 基于官方的 C 扩展，比纯 JS 有更高的运行效率。\n"
  },
  {
    "path": "binding/javascript/index.d.ts",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// Type definitions for ip2region\n// @Author Lion <chenxin619315@gmail.com>\n\nexport declare class Header {\n    version: number;\n    indexPolicy: number;\n    createdAt: number;\n    startIndexPtr: number;\n    endIndexPtr: number;\n    ipVersion: number;\n    runtimePtrBytes: number;\n    buff: Buffer;\n    \n    constructor(buff: Buffer);\n    toString(): string;\n}\n\nexport declare class Version {\n    id: number;\n    name: string;\n    bytes: number;\n    indexSize: number;\n    ipCompareFunc: (ip1: Buffer, ip2: Buffer, offset: number) => number;\n    \n    constructor(id: number, name: string, bytes: number, indexSize: number, ipCompareFunc: (ip1: Buffer, ip2: Buffer, offset: number) => number);\n    ipCompare(ip1: Buffer, ip2: Buffer): number;\n    ipSubCompare(ip1: Buffer, ip2: Buffer, offset: number): number;\n    toString(): string;\n}\n\nexport declare class Searcher {\n    ioCount: number;\n    version: Version;\n    handle: number | null;\n    vectorIndex: Buffer | null;\n    cBuffer: Buffer | null;\n    \n    constructor(version: Version, dbPath: string | null, vectorIndex: Buffer | null, cBuffer: Buffer | null);\n    getIPVersion(): Version;\n    getIOCount(): number;\n    search(ip: string | Buffer): string;\n    read(offset: number, buff: Buffer, stats?: any): void;\n    close(): void;\n}\n\n// Constants\nexport declare const XdbStructure20: 2;\nexport declare const XdbStructure30: 3;\nexport declare const XdbIPv4Id: 4;\nexport declare const XdbIPv6Id: 6;\nexport declare const HeaderInfoLength: 256;\nexport declare const VectorIndexRows: 256;\nexport declare const VectorIndexCols: 256;\nexport declare const VectorIndexSize: 8;\n\n// Version instances\nexport declare const IPv4: Version;\nexport declare const IPv6: Version;\n\n// Utility functions\nexport declare function parseIP(ipString: string): Buffer;\nexport declare function ipToString(ipBytes: Buffer, compress?: boolean): string;\nexport declare function ipBytesString(ipBytes: Buffer): string;\nexport declare function ipSubCompare(ip1: Buffer, buff: Buffer, offset: number): number;\nexport declare function ipCompare(ip1: Buffer, ip2: Buffer): number;\nexport declare function versionFromName(name: string): Version | null;\nexport declare function versionFromHeader(h: Header): Version | null;\n\n// File operations\nexport declare function loadHeader(fd: number): Header;\nexport declare function loadHeaderFromFile(dbPath: string): Header;\nexport declare function loadVectorIndex(fd: number): Buffer;\nexport declare function loadVectorIndexFromFile(dbPath: string): Buffer;\nexport declare function loadContent(fd: number): Buffer;\nexport declare function loadContentFromFile(dbPath: string): Buffer;\n\n// Searcher factory functions\nexport declare function newWithFileOnly(version: Version, dbPath: string): Searcher;\nexport declare function newWithVectorIndex(version: Version, dbPath: string, vectorIndex: Buffer): Searcher;\nexport declare function newWithBuffer(version: Version, cBuffer: Buffer): Searcher;"
  },
  {
    "path": "binding/javascript/index.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ip2region JavaScript binding with IPv4 and IPv6 support.\n// @Author Lion <chenxin619315@gmail.com>\n\nexport {\n    Header,\n    Version,\n    IPv4,\n    IPv6,\n    XdbStructure20,\n    XdbStructure30,\n    XdbIPv4Id,\n    XdbIPv6Id,\n    HeaderInfoLength,\n    VectorIndexRows,\n    VectorIndexCols,\n    VectorIndexSize,\n    parseIP,\n    ipToString,\n    ipBytesString,\n    ipSubCompare,\n    ipCompare,\n    versionFromName,\n    versionFromHeader,\n    loadHeader,\n    loadHeaderFromFile,\n    loadVectorIndex,\n    loadVectorIndexFromFile,\n    loadContent,\n    loadContentFromFile,\n    verify,\n    verifyFromFile\n} from './util.js';\n\nexport { \n    Searcher, \n    newWithFileOnly, \n    newWithVectorIndex, \n    newWithBuffer \n} from './searcher.js';"
  },
  {
    "path": "binding/javascript/package.json",
    "content": "{\n  \"name\": \"ip2region.js\",\n  \"version\": \"3.1.8\",\n  \"description\": \"official javascript binding for ip2region with both IPv4 and IPv6 supported \",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"scripts\": {\n    \"test\": \"NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lionsoul2014/ip2region.git\"\n  },\n  \"keywords\": [\n    \"ip2region\",\n    \"ip-address\",\n    \"ip-region\",\n    \"ip-location\",\n    \"ip-lookup\",\n    \"ip-search\",\n    \"ipv4-address\",\n    \"ipv4-region\",\n    \"ipv4-location\",\n    \"ipv4-lookup\",\n    \"ipv4-search\",\n    \"ipv6-address\",\n    \"ipv6-region\",\n    \"ipv6-location\",\n    \"ipv6-lookup\",\n    \"ipv6-search\"\n  ],\n  \"author\": \"lionsoul2014\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/lionsoul2014/ip2region/issues\"\n  },\n  \"homepage\": \"https://github.com/lionsoul2014/ip2region#readme\",\n  \"devDependencies\": {\n    \"@types/jest\": \"^30.0.0\",\n    \"argparse\": \"^2.0.1\",\n    \"jest\": \"^30.2.0\",\n    \"n-readlines\": \"^1.0.1\",\n    \"ts-jest\": \"^29.4.5\",\n    \"typescript\": \"^5.9.3\"\n  }\n}\n"
  },
  {
    "path": "binding/javascript/searcher.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// searcher implementation\n// @Author Lion <chenxin619315@gmail.com>\n\nimport fs from 'fs';\nimport {\n    parseIP, \n    HeaderInfoLength, VectorIndexCols, VectorIndexSize,\n    ipToString\n} from './util.js';\n\nexport class Searcher {\n    constructor(version, dbPath, vectorIndex, cBuffer) {\n        this.ioCount = 0;\n        this.dbPath  = dbPath;\n        this.version = version;\n        if (cBuffer != null) {\n            this.handle = null;\n            this.vectorIndex = null;\n            this.cBuffer = cBuffer;\n        } else {\n            this.handle = fs.openSync(dbPath, 'r');\n            this.vectorIndex = vectorIndex;\n            this.cBuffer = null;\n        }\n    }\n\n    getIPVersion() {\n        return this.version;\n    }\n\n    getIOCount() {\n        return this.ioCount;\n    }\n\n    async search(ip) {\n        // check and parse the string ip\n        const ipBytes = Buffer.isBuffer(ip) ? ip : parseIP(ip);\n\n        // ip version check\n        if (ipBytes.length != this.version.bytes) {\n            throw new Error(`invalid ip address '${ipToString(ipBytes)}' (${this.version.name} expected)`);\n        }\n\n        // reset the global counter\n        this.ioCount = 0;\n\n        // located the segment index block based on the vector index\n        let sPtr = 0, ePtr = 0;\n        let il0 = ipBytes[0], il1 = ipBytes[1];\n        let idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;\n        if (this.vectorIndex != null) {\n            sPtr = this.vectorIndex.readUint32LE(idx);\n            ePtr = this.vectorIndex.readUint32LE(idx + 4);\n        } else if (this.cBuffer != null) {\n            sPtr = this.cBuffer.readUint32LE(HeaderInfoLength + idx);\n            ePtr = this.cBuffer.readUint32LE(HeaderInfoLength + idx + 4);\n        } else {\n            const buff = Buffer.alloc(VectorIndexSize);\n            this.read(HeaderInfoLength + idx, buff);\n            sPtr = buff.readUInt32LE(0);\n            ePtr = buff.readUInt32LE(4);\n        }\n\n        // console.log(`sPtr: ${sPtr}, ePtr: ${ePtr}`);\n        // @Note: ptr validate, zero ptr means source data missing\n        // so we could just stop here and return an empty string.\n        if (sPtr == 0 || ePtr == 0) {\n            return \"\";\n        }\n\n        // binary search the segment index block to get the region info\n        const bytes = ipBytes.length, dBytes = ipBytes.length << 1;\n        const indexSize = this.version.indexSize;\n        const buff = Buffer.alloc(indexSize);\n        let dLen = 0, dPtr = 0, l = 0, h = (ePtr - sPtr) / indexSize;\n        while (l <= h) {\n            const m = (l + h) >> 1;\n            const p = sPtr + m * indexSize;\n\n            // read the segment index block\n            this.read(p, buff);\n            if (this.version.ipSubCompare(ipBytes, buff, 0) < 0) {\n                h = m - 1;\n            } else if (this.version.ipSubCompare(ipBytes, buff, bytes) > 0) {\n                l = m + 1;\n            } else {\n                dLen = buff.readUint16LE(dBytes);\n                dPtr = buff.readUint32LE(dBytes + 2);\n                break;\n            }\n        }\n\n        // empty match interception.\n        // and this could be a case.\n        if (dLen == 0) {\n            return \"\";\n        }\n\n        // console.log(`dLen: ${dLen}, dPtr: ${dPtr}`);\n        const region = Buffer.alloc(dLen);\n        this.read(dPtr, region);\n        return region.toString('utf-8');\n    }\n\n    read(offset, buff, stats) {\n        // check the in-memory buffer first\n        if (this.cBuffer != null) {\n            this.cBuffer.copy(buff, 0, offset, offset + buff.length);\n            return;\n        }\n\n        // increase the io counts\n        this.ioCount++;\n        \n        // read the data\n        let rBytes = fs.readSync(this.handle, buff, 0, buff.length, offset);\n        if (rBytes != buff.length) {\n            throw new Error(`incomplete read: read bytes should be ${buff.length}`);\n        }\n    }\n\n    // close the searcher\n    close() {\n        if (this.handle != null) {\n            fs.close(this.handle);\n        }\n    }\n\n    toString() {\n        const vn = this.version.name;\n        const vi = this.vectorIndex == null ? 'null' : this.vectorIndex.length;\n        const cf = this.cBuffer == null ? 'null' : this.cBuffer.length;\n        return `{\"version\": ${vn}, \"dbPath\": ${this.dbPath}, \"handle\": ${this.handle}, \"vectorIndex\": ${vi} \"cBuffer\": ${cf}}`;\n    }\n}\n\nexport function newWithFileOnly(version, dbPath) {\n    return new Searcher(version, dbPath, null, null);\n}\n\nexport function newWithVectorIndex(version, dbPath, vectorIndex) {\n    return new Searcher(version, dbPath, vectorIndex, null);\n}\n\nexport function newWithBuffer(version, cBuffer) {\n    return new Searcher(version, null, null, cBuffer);\n}"
  },
  {
    "path": "binding/javascript/tests/bench.app.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// app to do the xdb bench\n// @Author Lion <chenxin619315@gmail.com>\n\nimport * as xdb from '../index.js';\nimport {ArgumentParser} from 'argparse';\nimport LineByLine from 'n-readlines';\nimport fs from 'fs';\n\n\nconst parser = new ArgumentParser({\n    add_help: true,\n    description: 'ip2region bench script',\n    prog: 'node tests/bench.app.js',\n    usage: 'Usage %(prog)s [command options]'\n});\n\nparser.add_argument('--db', {help: 'ip2region binary xdb file path'});\nparser.add_argument('--src', {help: 'source ip text file path'});\nparser.add_argument('--cache-policy', {help: 'cache policy: file/vectorIndex/content, default: vectorIndex'});\n\nconst args = parser.parse_args();\nconst dbPath  = args.db || '';\nconst srcPath = args.src || '';\nconst cachePolicy = args.cache_policy || 'vectorIndex';\n\n// create the searcher\nconst createSearcher = () => {\n    const handle = fs.openSync(dbPath, 'r');\n\n    // verify the xdb file\n    // @Note: do NOT call it every time you create a searcher since this will slow\n    // down the search response.\n    // @see the verify function for details.\n    xdb.verify(handle);\n\n    // get the ip version from the header\n    const version = xdb.versionFromHeader(xdb.loadHeader(handle));\n\n    let searcher = null;\n    switch(cachePolicy) {\n    case 'file':\n        searcher = xdb.newWithFileOnly(version, dbPath);\n        break;\n    case 'vectorIndex':\n        const vIndex = xdb.loadVectorIndexFromFile(dbPath);\n        searcher = xdb.newWithVectorIndex(version, dbPath, vIndex);\n        break;\n    case 'content':\n        const cBuffer = xdb.loadContentFromFile(dbPath);\n        searcher = xdb.newWithBuffer(version, cBuffer);\n        break;\n    default:\n        fs.closeSync(handle);\n        throw new Error(`invalid cache-policy '${cachePolicy}'`);\n    }\n\n    fs.closeSync(handle);\n    return searcher;\n}\n\nconst _split = (line) => {\n    const ps = [];\n    const s1 = line.indexOf('|');\n    if (s1 === -1) {\n        ps.push(line);\n        return ps;\n    }\n\n    ps.push(line.substring(0, s1));\n    const s2 = line.indexOf('|', s1 + 1);\n    if (s2 === -1) {\n        ps.push(line.substring(s1+1));\n        return ps;\n    }\n\n    ps.push(line.substring(s1 + 1, s2));\n    ps.push(line.substring(s2 + 1));\n    return ps;\n}\n\nconst main = async () => {\n    if (dbPath.length < 1 || srcPath.length < 1) {\n        parser.print_help();\n        return;\n    }\n\n    const searcher = createSearcher();\n    console.log(`Searcher: ${searcher.toString()}`);\n\n    // read the source line and do the search bench\n    let totalMicroSecs = 0, count = 0, line = null;\n    const rl = new LineByLine(srcPath);\n    while (line = rl.next()) {\n        const ps  = _split(line.toString('utf-8'));\n        const sTime  = process.hrtime();\n        const sip = xdb.parseIP(ps[0]);\n        const eip = xdb.parseIP(ps[1]);\n        if (xdb.ipCompare(sip, eip) > 0) {\n            throw new Error(`start ip(${ps[0]}) should not be greater than end ip(${ps[1]})`);\n        }\n\n        const test_list = [sip, eip];\n        for (let i = 0; i < test_list.length; i++) {\n            const region = await searcher.search(test_list[i]);\n            if (region != ps[2]) {\n                throw new Error(`failed to search(${xdb.ipToString(test_list[i])}) with (${region} != ${ps[2]})`);\n            }\n            count++;\n        }\n        const diff = process.hrtime(sTime);\n        const took = diff[0] * 1_000_000 + diff[1] / 1e3;\n        totalMicroSecs += took;\n    }\n\n    const tookSec = totalMicroSecs / 1e6;\n    const _eachUs = count == 0 ? 0 : totalMicroSecs / count;\n    console.log(`Bench finished, {cachePolicy: ${cachePolicy}, total: ${count}, took: ${tookSec} s, cost: ${_eachUs} μs/op}`);\n}\n\nmain();"
  },
  {
    "path": "binding/javascript/tests/search.app.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// app to do the xdb search\n// @Author Lion <chenxin619315@gmail.com>\n\nimport * as xdb from '../index.js';\nimport {ArgumentParser} from 'argparse';\nimport fs from 'fs';\n\nconst parser = new ArgumentParser({\n    add_help: true,\n    description: 'ip2region search script',\n    prog: 'node tests/search.app.js',\n    usage: 'Usage %(prog)s [command options]'\n});\n\nparser.add_argument('--db', {help: 'ip2region binary xdb file path'});\nparser.add_argument('--cache-policy', {help: 'cache policy: file/vectorIndex/content, default: vectorIndex'});\n\nconst args = parser.parse_args();\nconst dbPath  = args.db || '';\nconst cachePolicy = args.cache_policy || 'vectorIndex';\n\n// create the searcher\nconst createSearcher = () => {\n    const handle = fs.openSync(dbPath, 'r');\n\n    // verify the xdb file\n    // @Note: do NOT call it every time you create a searcher since this will slow\n    // down the search response.\n    // @see the verify function for details.\n    xdb.verify(handle);\n\n    // get the ip version from the header\n    const version = xdb.versionFromHeader(xdb.loadHeader(handle));\n\n    let searcher = null;\n    switch(cachePolicy) {\n    case 'file':\n        searcher = xdb.newWithFileOnly(version, dbPath);\n        break;\n    case 'vectorIndex':\n        const vIndex = xdb.loadVectorIndexFromFile(dbPath);\n        searcher = xdb.newWithVectorIndex(version, dbPath, vIndex);\n        break;\n    case 'content':\n        const cBuffer = xdb.loadContentFromFile(dbPath);\n        searcher = xdb.newWithBuffer(version, cBuffer);\n        break;\n    default:\n        fs.closeSync(handle);\n        throw new Error(`invalid cache-policy '${cachePolicy}'`);\n    }\n\n    fs.closeSync(handle);\n    return searcher;\n}\n\nconst readlineSync = () => {\n    return new Promise((resolve, reject) => {\n        process.stdin.resume();\n        process.stdin.on('data', (buff) => {\n            process.stdin.pause();\n            resolve(buff.toString('utf-8'));\n        });\n    });\n}\n\nconst main = async () => {\n    if (dbPath.length < 1) {\n        parser.print_help();\n        return;\n    }\n\n    const searcher = createSearcher();\n    console.log(`ip2region xdb searcher test program\nsource xdb: ${dbPath} (${searcher.getIPVersion().name}, ${cachePolicy})\ntype 'quit' to exit`);\n\n    while (true) {\n        process.stdout.write('ip2region>> ');\n\n        // get the input ip\n        const ipString = (await readlineSync()).trim();\n        if (ipString.length == 0) {\n            continue;\n        }\n\n        if (ipString == 'quit') {\n            break;\n        }\n\n        // parse the ip address\n        let ipBytes = null;\n        try {\n            ipBytes = xdb.parseIP(ipString);\n        } catch (e) {\n            console.log(`failed to parse ip: ${e.message}`);\n            continue;\n        }\n\n        // do the search\n        const sTime = process.hrtime();\n        let region = null;\n        try {\n            region = await searcher.search(ipBytes);\n        } catch (e) {\n            console.log(`{err: ${e.message}, ioCount: ${searcher.getIOCount()}}`);\n            continue;\n        }\n\n        const diff = process.hrtime(sTime);\n        const took = diff[0] * 1_000_000 + diff[1] / 1e3;\n        console.log(`{region: ${region}, ioCount: ${searcher.getIOCount()}, took: ${took} μs}`);\n    }\n}\n\nmain();"
  },
  {
    "path": "binding/javascript/tests/searcher.test.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// searcher new tester\n// @Author Lion <chenxin619315@gmail.com>\n\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport {\n    IPv4, IPv6, XdbIPv4Id, \n    parseIP, ipToString, verifyFromFile,\n    loadVectorIndexFromFile, loadContentFromFile, \n    newWithFileOnly, newWithVectorIndex, newWithBuffer\n} from '../index.js';\nimport { fail } from 'assert';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst dbPath = {\n    v4: path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v4.xdb'),\n    v6: path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v6.xdb')\n}\ntest('xdb file verify', () => {\n    // verify the xdb file\n    // @Note: do NOT call it every time you create a searcher since this will slow\n    // down the search response.\n    // @see the verify function for details.\n    for (k in dbPath) {\n        if (!dbPath.hasOwnProperty(k)) {\n            continue;\n        }\n\n        try {\n            verifyFromFile(dbPath[k]);\n            console.log(`xdb file '${dbPath[k]}' verified`);\n        } catch (e) {\n            throw new Error(`binding is not applicable for xdb file '${dbPath[k]}': ${e.message}`);\n        }\n    }\n});\n\n// ---\n// search api testing\n\ntest('ipv4 search test', async () => {\n    let searcher = newWithFileOnly(IPv4, dbPath.v4);\n    let ip_list  = [\n        '1.0.0.0',\n        parseIP('113.118.112.93'),\n        '240e:3b7::'\n    ];\n\n    for (var i = 0; i < ip_list.length; i++) {\n        let ip = ip_list[i];\n        searcher.search(ip).then((region)=>{\n            let ipStr = Buffer.isBuffer(ip) ? ipToString(ip) : ip;\n            console.log(`search(${ipStr}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n        }).catch((err) => {\n            console.log(`${err.message}`);\n        });\n    }\n\n    // close searcher\n    searcher.close();\n});\n\ntest('ipv6 search test', async () => {\n    let searcher = newWithFileOnly(IPv6, dbPath.v6);\n    let ip_list  = [\n        '2a02:26f7:c409:4001::',\n        parseIP('2a11:8080:200::a:a05c'),\n        '240e:3b7::',\n        '120.229.45.92'\n    ];\n\n    for (var i = 0; i < ip_list.length; i++) {\n        let ip = ip_list[i];\n        searcher.search(ip).then((region)=>{\n            let ipStr = Buffer.isBuffer(ip) ? ipToString(ip) : ip;\n            console.log(`search(${ipStr}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);\n        }).catch((err) => {\n            console.log(`${err.message}`);\n        });\n    }\n\n    // close searcher\n    searcher.close();\n});\n\n\n// ---\n// searcher with different cache policy testing\n\nfunction _get_creater_list(version) {\n    let dbFile = version.id == XdbIPv4Id ? dbPath.v4 : dbPath.v6;\n     return [function(){\n        return [\"newWithFileOnly\", newWithFileOnly(version, dbFile)];\n    }, function(){\n        const vIndex = loadVectorIndexFromFile(dbFile);\n        return [\"newWithVectorIndex\", newWithVectorIndex(version, dbFile, vIndex)];\n    }, function(){\n        const cBuffer = loadContentFromFile(dbFile);\n        return [\"newWithBuffer\", newWithBuffer(version, cBuffer)];\n    }];\n}\n\ntest('ipv4 searcher test', async () => {\n    const ip_Str = '120.229.45.92';\n    const c_list = _get_creater_list(IPv4);\n    try {\n        let bRegion = null;\n        for (var i = 0; i < c_list.length; i++) {\n            const meta = c_list[i]();\n            const region = await meta[1].search(ip_Str);\n            if (bRegion != null) {\n                expect(region).toBe(region);\n            }\n            bRegion = region;\n            console.log(`${meta[0]}.search(${ip_Str}): ${region}`);\n\n            // searcher close\n            meta[1].close();\n        }\n    } catch (e) {\n        console.error(`${e.message}`);\n    }\n});\n\ntest('ipv6 searcher test', async () => {\n    const ip_Str = '240e:57f:32ff:ffff:ffff:ffff:ffff:ffff';\n    const c_list = _get_creater_list(IPv6);\n    try {\n        let bRegion = null;\n        for (var i = 0; i < c_list.length; i++) {\n            const meta = c_list[i]();\n            const region = await meta[1].search(ip_Str);\n            if (bRegion != null) {\n                expect(region).toBe(region);\n            }\n            bRegion = region;\n            console.log(`${meta[0]}.search(${ip_Str}): ${region}`);\n\n            // searcher close\n            meta[1].close();\n        }\n    } catch (e) {\n        console.error(`${e.message}`);\n    }\n});"
  },
  {
    "path": "binding/javascript/tests/util.test.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// util test script\n// @Author Lion <chenxin619315@gmail.com>\n\nimport * as util from '../util.js';\nimport path from 'node:path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst dbPath = path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v4.xdb')\n\ntest('const print', () => {\n    console.log(\"IPv4: \", util.IPv4.toString());\n    console.log(\"IPv6: \", util.IPv6.toString());\n});\n\ntest(\"test version from name\", () => {\n    let vs = [\"v4\", \"ipv4\", \"v4x\", \"v6\", \"ipv6\", \"v6x\"];\n    vs.forEach(ele => {\n        let v = util.versionFromName(ele);\n        if (v == null) {\n            console.log(`invalid version name ${ele}`);\n            return;\n        }\n\n        console.log(`versionFrom(${ele}): ${v.toString()}, id=${v.id}, name=${v.name}`);\n    });\n});\n\ntest(\"test version ip compare\", () => {\n    let ip_list = [\n        [\"1.0.0.0\", \"0.0.1.2\", 1],\n        [\"192.168.1.101\", \"192.168.1.90\", 1],\n        [\"219.133.111.87\", \"114.114.114.114\", 1],\n        [\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\", 1]\n    ];\n\n    ip_list.forEach(ips => {\n        const ip1 = util.parseIP(ips[0]);\n        const ip2 = util.parseIP(ips[1]);\n        if (ip1.length != ip2.length) {\n            fail(`ip1 and ip2 are not the same ip type`);\n        }\n\n        const version = ip1.length == 4 ? util.IPv4 : util.IPv6;\n        const cmp = version.ipSubCompare(ip1, ip1.length == 4 ? ip2.reverse() : ip2, 0);\n        expect(cmp).toBe(ips[2]);\n        console.log(`compare(${ips[0]}, ${ips[1]}): ${cmp}`);\n    });\n});\n\n\n\ntest('parse ip address', () => {\n    let ip_list = [\n        \"1.0.0.0\", \"58.251.30.115\", \"192.168.1.100\", \"126.255.32.255\", \"219.xx.xx.11\", \n        \"::\", \"::1\", \"fffe::\", \"2c0f:fff0::\", \"2c0f:fff0::1\", \"2a02:26f7:c409:4001::\",\n        \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"240e:982:e617:ffff:ffff:ffff:ffff:ffff\", \"::xx:ffff\"\n    ];\n\n    ip_list.forEach(ipString => {\n        let ipBytes = null;\n        try {\n            ipBytes = util.parseIP(ipString);\n        } catch (e) {\n            console.log(`failed to parse ip '${ipString}': ${e.message}`);\n            return;\n        }\n\n        let to_Str = util.ipToString(ipBytes, true);\n        let toByte = util.ipBytesString(ipBytes);\n        console.log(`parseIP(${ipString}): {Bytes: ${toByte}, String: ${to_Str}}`);\n        expect(ipString).toBe(to_Str);\n    });\n});\n\ntest('ip compare', () => {\n    let ip_list = [\n        [\"1.0.0.0\", \"1.0.0.1\", -1],\n        [\"192.168.1.101\", \"192.168.1.90\", 1],\n        [\"219.133.111.87\", \"114.114.114.114\", 1],\n        [\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\", 1]\n    ];\n\n    ip_list.forEach(ips => {\n        const ip1 = util.parseIP(ips[0]);\n        const ip2 = util.parseIP(ips[1]);\n        const cmp = util.ipCompare(ip1, ip2);\n        expect(cmp).toBe(ips[2]);\n        console.log(`compare(${ips[0]}, ${ips[1]}): ${cmp}`);\n    });\n});\n\ntest('test load header', () => {\n    let header = util.loadHeaderFromFile(dbPath);\n    console.log(`dbPath: ${dbPath}, header: ${header.toString()}}`);\n});\n\ntest('test load vector index', () => {\n    let vIndex = util.loadVectorIndexFromFile(dbPath);\n    console.log(`dbPath: ${dbPath}, vIndex: ${vIndex.length}}`);\n});\n\ntest('test load content', () => {\n    let content = util.loadContentFromFile(dbPath);\n    console.log(`dbPath: ${dbPath}, content: ${content.length}}`);\n});"
  },
  {
    "path": "binding/javascript/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ES2020\",\n    \"moduleResolution\": \"node\",\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"declaration\": true,\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\n    \"*.js\",\n    \"*.d.ts\",\n    \"tests/**/*\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\"\n  ]\n}"
  },
  {
    "path": "binding/javascript/util.js",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// util functions\n// @Author Lion <chenxin619315@gmail.com>\n\nimport fs from 'fs';\n\nexport const XdbStructure20 = 2;\nexport const XdbStructure30 = 3;\nexport const XdbIPv4Id = 4;\nexport const XdbIPv6Id = 6;\n\nexport const HeaderInfoLength = 256;\nexport const VectorIndexRows  = 256;\nexport const VectorIndexCols  = 256;\nexport const VectorIndexSize  = 8;\n\nexport class Header {\n    constructor(buff) {\n        this.version = buff.readUInt16LE(0);\n        this.indexPolicy = buff.readUInt16LE(2);\n        this.createdAt = buff.readUInt32LE(4);\n        this.startIndexPtr = buff.readUInt32LE(8);\n        this.endIndexPtr = buff.readUInt32LE(12);\n\n        // since IPv6 supporting\n        this.ipVersion = buff.readUInt16LE(16);\n        this.runtimePtrBytes = buff.readUInt16LE(18);\n\n        // keep the raw data\n        this.buff = buff;\n    }\n\n    toString() {\n        return `{\n            \"version\":${this.version}, \n            \"index_policy\":${this.indexPolicy},\n            \"start_index_ptr\": ${this.startIndexPtr},\n            \"end_index_ptr\": ${this.endIndexPtr},\n            \"ipVersion\": ${this.ipVersion},\n            \"runtime_ptr_bytes\": ${this.runtimePtrBytes}\n        }`;\n    }\n}\n\n// ---\n\n// parse ipv4 address\nfunction _parse_ipv4_addr(v4String) {\n    let ps = v4String.split('.', 4);\n    if (ps.length != 4) {\n        throw new Error('invalid ipv4 address');\n    }\n\n    var v;\n    const ipBytes =  Buffer.alloc(4);\n    for (var i = 0; i < ps.length; i++) {\n        v = parseInt(ps[i], 10);\n        if (isNaN(v)) {\n            throw new Error(`invalid ipv4 part '${ps[i]}', a valid number expected`);\n        }\n\n        if (v < 0 || v > 255) {\n            throw new Error(`invalid ipv4 part '${ps[i]}' should >= 0 and <= 255`);\n        }\n\n        ipBytes[i] = (v & 0xFF);\n    }\n\n    return ipBytes;\n}\n\n// parse ipv6 address\nfunction _parse_ipv6_addr(v6String) {\n    let ps = v6String.split(':', 8);\n    if (ps.length < 3) {\n        throw new Error('invalid ipv6 address');\n    }\n\n    let dc_num = 0, offset = 0;\n    const ipBytes = Buffer.alloc(16);\n    for (var i = 0; i < ps.length; i++) {\n        let s = ps[i].trim();\n\n        // Double colon check and auto padding\n        if (s.length == 0) {\n            // ONLY one double colon allow\n            if (dc_num > 0) {\n                throw new Error('invalid ipv6 address: multi double colon detected');\n            }\n\n            let start = i, mi = ps.length - 1;\n            // clear all the consecutive spaces\n            for (i++;;) {\n                s = ps[i].trim();\n                if (s.length > 0) {\n                    i--;\n                    break;\n                }\n\n                if (i >= mi) {\n                    break;\n                }\n\n                i++;\n            }\n\n            dc_num = 1;\n            // padding = 8 - start - left\n            let padding = 8 - start - (mi - i);\n            offset += 2 * padding;\n            continue;\n        }\n\n        let v = parseInt(s, 16);\n        if (isNaN(v)) {\n            throw new Error(`invalid ipv6 part '${ps[i]}', a valid hex number expected`);\n        }\n\n        if (v < 0 || v > 0xFFFF) {\n            throw new Error(`invalid ipv6 part '${ps[i]}' should >= 0 and <= 65534`);\n        }\n\n        ipBytes.writeUint16BE(v, offset);\n        offset += 2;\n    }\n\n    return ipBytes;\n}\n\n\n// parse the specified string ip and return its bytes\n// @param  ip string\n// @return Buffer\nexport function parseIP(ipString) {\n    let sDot = ipString.indexOf('.');\n    let cDot = ipString.indexOf(':');\n    if (sDot > -1 && cDot == -1) {\n        return _parse_ipv4_addr(ipString);\n    } else if (cDot > -1) {\n        return _parse_ipv6_addr(ipString);\n    } else {\n        throw new Error(`invalid ip address '${ipString}'`);\n    }\n}\n\n// ---\n\n// ipv4 bytes to string\nfunction _ipv4_to_string(v4Bytes) {\n    return v4Bytes.join('.');\n}\n\n// ipv6 bytes to string\nfunction _ipv6_to_string(v6Bytes, compress) {\n    let ps = [], needCompress = false;\n    let lastHex = -1, hex = 0;\n    for (var i = 0; i < v6Bytes.length; i += 2) {\n        hex = v6Bytes.readUint16BE(i).toString(16);\n        ps.push(hex);\n     \n        // check the necessity for compress\n        if (lastHex > -1 \n            && hex == 0 && lastHex == 0) {\n            needCompress = true;\n        }\n\n        // reset the last hex\n        lastHex = hex;\n    }\n\n    if (needCompress == false || compress === false) {\n        return ps.join(':');\n    }\n\n    // auto compression of consecutive zero\n    let _ = [], mi = ps.length - 1;\n    for (i = 0; i < ps.length; i++) {\n        if (i >= mi) {\n            _.push(ps[i]);\n            continue;\n        }\n\n        if (ps[i] != '0' || ps[i+1] != '0') {\n            _.push(ps[i]);\n            continue;\n        }\n\n        // find the first two zero part\n        // and keep find all the zero part\n        for (i += 2; i < ps.length;) {\n            if (ps[i] != '0') {\n                i--;\n                break;\n            }\n            i++;\n        }\n\n        // make sure there is an empty head.\n        if (_.length == 0) {\n            _.push('');\n        }\n\n        _.push(''); // empty for double colon\n\n        // make sure there is an empty tail\n        if (i == ps.length && _.length < ps.length) {\n            _.push('');\n        }\n    }\n\n    return _.join(':');\n}\n\n// bytes ip to humen-readable string ip\nexport function ipToString(ipBytes, compress) {\n    if (!Buffer.isBuffer(ipBytes)) {\n        throw new Error('invalid bytes ip, not a Buffer');\n    }\n\n    if (ipBytes.length == 4) {\n        return _ipv4_to_string(ipBytes, compress);\n    } else if (ipBytes.length == 16) {\n        return _ipv6_to_string(ipBytes, compress);\n    } else {\n        throw new Error('invalid bytes ip with length not 4 or 16');\n    }\n}\n\nexport function ipBytesString(ipBytes) {\n    if (!Buffer.isBuffer(ipBytes)) {\n        throw new Error('invalid bytes ip, not a Buffer');\n    }\n\n    let ps = [];\n    for (var i = 0; i < ipBytes.length; i++) {\n        ps.push(ipBytes[i] & 0xFF);\n    }\n\n    return ps.join('.');\n}\n\n// compare two byte ips\n// ip2 = buff[offset:ip1.length]\n// returns: -1 if ip1 < ip2, 1 if ip1 > ip2 or 0\nexport function ipSubCompare(ip1, buff, offset) {\n    return ip1.compare(buff, offset, offset + ip1.length);\n}\n\nexport function ipCompare(ip1, ip2) {\n    return ipSubCompare(ip1, ip2, 0);\n}\n\n// ---\n\nexport class Version {\n    constructor(id, name, bytes, indexSize, ipCompareFunc) {\n        this.id = id;\n        this.name = name;\n        this.bytes = bytes;\n        this.indexSize = indexSize;\n        this.ipCompareFunc = ipCompareFunc;\n    }\n\n    ipCompare(ip1, ip2) {\n        return this.ipCompareFunc(ip1, ip2, 0);\n    }\n\n    ipSubCompare(ip1, ip2, offset) {\n        return this.ipCompareFunc(ip1, ip2, offset);\n    }\n\n    toString() {\n        return `{\"id\": ${this.id}, \"name\": \"${this.name}\", \"bytes\":${this.bytes}, \"index_size\": ${this.indexSize}}`;\n    }\n}\n\n// 14 = 4 + 4 + 2 + 4\nexport const IPv4 = new Version(XdbIPv4Id, \"IPv4\", 4, 14, function(ip1, buff, offset){\n    // ip1: Big endian byte order parsed from input\n    // ip2: Little endian byte order read from xdb index.\n    // @Note: to compatible with the old Litten endian index encode implementation.\n    let i, j = offset + ip1.length - 1;\n    for (i = 0; i < ip1.length; i++, j--) {\n        const i1 = ip1[i] & 0xFF;\n        const i2 = buff[j] & 0xFF;\n        if (i1 < i2) {\n            return -1;\n        }\n\n        if (i1 > i2) {\n            return 1;\n        }\n    }\n\n    return 0;\n});\n\n// 38 = 16 + 16 + 2 + 4\nexport const IPv6 = new Version(XdbIPv6Id, \"IPv6\", 16, 38, ipSubCompare);\n\nexport function versionFromName(name) {\n    let n = name.toUpperCase();\n    if (n == \"V4\" || n == \"IPV4\") {\n        return IPv4;\n    } else if (n == \"V6\" || n == \"IPV6\") {\n        return IPv6;\n    } else {\n        return null;\n    }\n}\n\nexport function versionFromHeader(h) {\n    // old structure with ONLY IPv4 supporting\n    if (h.version == XdbStructure20) {\n        return IPv4;\n    }\n\n    // structure 3.0 with IPv6 supporting\n    if (h.version != XdbStructure30) {\n        return null;\n    }\n\n    let ipVer = h.ipVersion;\n    if (ipVer == XdbIPv4Id) {\n        return IPv4;\n    } else if (ipVer == XdbIPv6Id) {\n        return IPv6;\n    } else {\n        return null;\n    }\n}\n\n// ---\n\nexport function loadHeader(fd) {\n    const buffer = Buffer.alloc(HeaderInfoLength);\n    const rBytes = fs.readSync(fd, buffer, 0, HeaderInfoLength, 0);\n    if (rBytes != HeaderInfoLength) {\n        throw new Error(`incomplete read (${rBytes} read, ${header.HeaderInfoLength} expected)`);\n    }\n    return new Header(buffer);\n}\n\nexport function loadHeaderFromFile(dbPath) {\n    const fd = fs.openSync(dbPath, \"r\");\n    const header = loadHeader(fd);\n    fs.closeSync(fd);\n    return header;\n}\n\nexport function loadVectorIndex(fd) {\n    const vBytes = VectorIndexCols * VectorIndexRows * VectorIndexSize;\n    const buffer = Buffer.alloc(vBytes);\n    const rBytes = fs.readSync(fd, buffer, 0, vBytes, HeaderInfoLength);\n    if (rBytes != vBytes) {\n        throw new Error(`incomplete read (${rBytes} read, ${vBytes} expected)`);\n    }\n    return buffer;\n}\n\nexport function loadVectorIndexFromFile(dbPath) {\n    const fd = fs.openSync(dbPath, \"r\");\n    const vIndex = loadVectorIndex(fd);\n    fs.closeSync(fd);\n    return vIndex;\n}\n\nexport function loadContent(fd) {\n    const stats = fs.fstatSync(fd);\n    const buffer = Buffer.alloc(stats.size);\n    const rBytes = fs.readSync(fd, buffer, 0, buffer.length, 0);\n    if (rBytes != stats.size) {\n        throw new Error(`incomplete read (${rBytes} read, ${stats.size} expected)`);\n    }\n    return buffer;\n}\n\nexport function loadContentFromFile(dbPath) {\n    const fd = fs.openSync(dbPath, \"r\");\n    const content = loadContent(fd);\n    fs.closeSync(fd);\n    return content;\n}\n\n// --- \n\n// Verify if the current Searcher could be used to search the specified xdb file.\n// Why do we need this check ?\n// The future features of the xdb impl may cause the current searcher not able to work properly.\n//\n// @Note: You Just need to check this ONCE when the service starts\n// Or use another process (eg, A command) to check once Just to confirm the suitability.\nexport function verify(fd) {\n    const header = loadHeader(fd);\n\n    // get the runtime ptr bytes\n    let runtimePtrBytes = 0;\n    if (header.version == XdbStructure20) {\n        runtimePtrBytes = 4;\n    } else if (header.version == XdbStructure30) {\n        runtimePtrBytes = header.runtimePtrBytes;\n    } else {\n        throw new Error(`invalid structure version ${header.version}`);\n    }\n\n    // 1, confirm the xdb file size\n    // to ensure that the maximum file pointer does not overflow\n    const maxFilePtr = (1n << BigInt(runtimePtrBytes * 8)) - 1n;\n    const _fileBytes = BigInt(fs.fstatSync(fd).size);\n    if (_fileBytes > maxFilePtr) {\n        throw new Error(`xdb file exceeds the maximum supported bytes: ${maxFilePtr}`);\n    }\n}\n\nexport function verifyFromFile(dbPath) {\n    const fd = fs.openSync(dbPath, \"r\");\n    verify(fd);\n    fs.closeSync(fd);\n}"
  },
  {
    "path": "binding/lua/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region lua query client\n\n#### Note: Please prioritize the use of the lua_c extension query client, as its performance is much faster than the pure lua implementation!!!\n\n# Version Compatibility\n\nThis implementation is compatible with lua `5.3` and `5.4`, and no longer provides compatibility for lower versions. If you need to use it under lower versions of Lua, please consider using the `[lua_c](../lua_c/)` extension.\n\n# Usage\n\n### About Query API\n\nThe prototype of the query API is as follows:\n\n```lua\n-- Query via IP string\nsearch_by_string(ip_string) (region, error)\n-- Query via bytes IP returned by parse_ip\nsearch(ip_bytes) (region, error)\n```\n\nIf the query fails, it returns a non-`nil` error string. If successful, it returns the `region` information as a string. If the IP address cannot be found, it returns an empty string `\"\"`.\n\n### About IPv4 and IPv6\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- For IPv4: Set xdb path to the v4 xdb file, specify IP version as Version.IPv4\nlocal dbPath  = \"../../data/ip2region_v4.xdb\"  -- or your ipv4 xdb path\nlocal version = xdb.IPv4\n\n-- For IPv6: Set xdb path to the v6 xdb file, specify IP version as Version.IPv6\nlocal dbPath  = \"../../data/ip2region_v6.xdb\"  -- or your ipv6 xdb path\nlocal version = xdb.IPv6\n\n-- The IP version of the xdb specified by dbPath must match the version specified, otherwise an error will occur during query execution\n-- Note: The following demonstration directly uses the dbPath and version variables\n```\n\n### File Verification\n\nIt is recommended that you actively verify the suitability of the xdb file, as some new features in the future may cause the current Searcher version to be incompatible with the xdb file you are using. Verification can avoid unpredictable errors during runtime. You don't need to verify every time; for example, verify when the service starts or by manually calling the verification command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```lua\nlocal xdb = require('xdb_searcher')\n\nlocal err = xdb.verify(dbPath);\nif err ~= nil then\n    -- Suitability verification failed!!!\n    -- The current query client implementation is not suitable for the xdb file specified by dbPath.\n    -- You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with dbPath.\n    print(string.format(\"binding is not applicable for xdb file '%s': %s\", dbPath, err))\n    return\nend\n\n-- Verification passed, the current Searcher can safely be used for query operations on the xdb pointed to by dbPath\n```\n\n### Entirely File-Based Query\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Create an entirely file-based query object using the version and dbPath mentioned above\nlocal searcher, err = xdb.new_with_file_only(version, db_path)\nif err ~= nil then\n    print(string.format(\"failed to create searcher: %s\", err))\n    return\nend\n\n-- 2. Call the query API; with both IPv4 and IPv6 addresses supported\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 3. Close resources\nsearcher:close()\n\n-- Note: For concurrent use, each coroutine needs to create a separate xdb query object\n```\n\n### Caching `VectorIndex`\n\nIf supported by your `lua` environment, you can pre-load the vectorIndex cache and make it a global variable. Using the global vectorIndex every time a Searcher is created can reduce one fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Load vectorIndex cache from the specified db_path and make the v_index object below a global variable.\n-- vectorIndex only needs to be loaded once; it is recommended to load it as a global object when the service starts.\nv_index, err = xdb.load_vector_index(dbPath)\nif err ~= nil then\n    print(string.format(\"failed to load vector index from '%s'\", db_path))\n    return\nend\n\n-- 2. Use the global v_index to create a query object with vectorIndex cache.\nsearcher, err = xdb.new_with_vector_index(version, dbPath, v_index)\nif err ~= nil then\n    print(string.format(\"failed to create vector index searcher: %s\", err))\n    return\nend\n\n-- 3. Call the query API; the same interface is used for both IPv4 and IPv6\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 4. Close resources\nsearcher:close()\n\n-- Note: For concurrent use, each coroutine needs to create a separate xdb query object, but they share the global v_index object\n```\n\n### Caching the Entire `xdb` File\n\nIf supported by your `lua` environment, you can pre-load the entire xdb data into memory to achieve completely memory-based queries, similar to the previous memory search.\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Load the entire xdb into memory from the specified dbPath.\n-- xdb content only needs to be loaded once; it is recommended to load it as a global object when the service starts.\ncontent, err = xdb.load_content(dbPath)\nif err ~= nil then\n    print(string.format(\"failed to load xdb content: %s\", err))\n    return\nend\n\n-- 2. Use the global content to create an entirely memory-based query object.\nsearcher, err = xdb.new_with_buffer(version, content)\nif err ~= nil then\n    print(string.format(\"failed to create content buffer searcher: %s\", err))\n    return\nend\n\n-- 3. Call the query API; the same interface is used for both IPv4 and IPv6\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 4. Close resources - This searcher object can be safely used for concurrency; close the searcher only when the entire service is shut down\n-- searcher:close()\n\n-- Note: For concurrent use, query objects created with the entire xdb cache can be safely used concurrently.\n-- It is recommended to create a global searcher object when the service starts and then use it globally and concurrently.\n```\n\n# Query Testing\n\nPerform query tests via the `lua search_test.lua` script:\n\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua   \nlua search_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: using the default data/ip2region_v4.xdb file for IPv4 query testing:\n\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua  --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 0μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, io_count: 2, took: 0μs}\n```\n\nFor example: using the default data/ip2region_v6.xdb file for IPv6 query testing:\n\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua  --db=../../data/ip2region_v6.xdb                                                           \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 0μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, io_count: 13, took: 0μs}\n```\n\nEnter an IP to perform a query test. You can also set `cache-policy` to file/vectorIndex/content respectively to test the efficiency of the three different cache implementations.\n\n# Bench Testing\n\nPerform automatic bench testing via the `lua bench_test.lua` script. This ensures that the `xdb` file has no errors and tests average query performance through a large number of queries:\n\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua bench_test.lua                                                                                              \nlua bench_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: perform IPv4 bench testing using default data/ip2region_v4.xdb and data/ipv4_source.txt files:\n\n```bash\nlua bench_test.lua --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\nFor example: perform IPv6 bench testing using default data/ip2region_v6.xdb and data/ipv6_source.txt files:\n\n```bash\nlua bench_test.lua --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\nYou can test the performance of the three different cache implementations (file/vectorIndex/content) by setting the `cache-policy` parameter.\n@Note: Please note that the src file used for the bench must be the same source file used to generate the corresponding xdb file.\n"
  },
  {
    "path": "binding/lua/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region lua 查询客户端\n\n#### 备注：请优先使用 lua_c 扩展 xdb 查询客户端，性能比纯 lua 实现的要快很多！！！\n\n# 版本兼容\n该实现兼容 lua `5.3`, `5.4`，且不再提供更低版本的兼容，如果需要在更低版本的 Lua 下使用请考虑使用 `[lua_c](../lua_c/)` 扩展。\n\n# 使用方式\n\n### 关于查询 API\n查询 API 的原型如下：\n```lua\n-- 通过字符串 IP 查询\nsearch_by_string(ip_string) (region, error)\n-- 通过 parse_ip 返回的 bytes IP 查询\nsearch(ip_bytes) (region, error)\n```\n如果查询出错则会返回非 `nil` 的 error 字符串信息，如果查询成功则会返回字符串的 `region` 信息，如果查询的 IP 地址找不到则会返回空字符串 `\"\"`。\n\n### 关于 IPv4 和 IPv6\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 Version.IPv4\nlocal dbPath  = \"../../data/ip2region_v4.xdb\"  -- 或者你的 ipv4 xdb 的路径\nlocal version = xdb.IPv4\n\n-- 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 Version.IPv6\nlocal dbPath  = \"../../data/ip2region_v6.xdb\"  -- 或者你的 ipv6 xdb 路径\nlocal version = xdb.IPv6\n\n-- dbPath 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n-- 备注：以下演示直接使用 dbPath 和 version 变量\n```\n\n### 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```lua\nlocal xdb = require('xdb_searcher')\n\nlocal err = xdb.verify(dbPath);\nif err ~= nil then\n    -- 适用性验证失败！！！\n    -- 当前查询客户端实现不适用于 dbPath 指定的 xdb 文件的查询.\n    -- 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 dbPath 的 Searcher 实现。\n    print(string.format(\"binding is not applicable for xdb file '%s': %s\", dbPath, err))\n    return\nend\n\n-- 验证通过，当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1，使用上述的 version 和 dbPath 创建完全基于文件的查询对象\nlocal searcher, err = xdb.new_with_file_only(version, db_path)\nif err ~= nil then\n    print(string.format(\"failed to create searcher: %s\", err))\n    return\nend\n\n-- 2、调用查询 API 进行查询，IPv4 或者 IPv6 的地址都是同一个接口\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 3、关闭资源\nsearcher:close()\n\n-- 备注：并发使用，每个协程需要创建单独的 xdb 查询对象\n```\n\n### 缓存 `VectorIndex` 索引\n\n如果你的 `lua` 母环境支持，可以预先加载 vectorIndex 缓存，然后做成全局变量，每次创建 Searcher 的时候使用全局的 vectorIndex，可以减少一次固定的 IO 操作从而加速查询，减少 io 压力。\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1、从指定的 db_path 加载 vectorIndex 缓存，把下述的 v_index 对象做成全局变量。\n-- vectorIndex 加载一次即可，建议在服务启动的时候加载为全局对象。\nv_index, err = xdb.load_vector_index(dbPath)\nif err ~= nil then\n    print(string.format(\"failed to load vector index from '%s'\", db_path))\n    return\nend\n\n-- 2、使用全局的 v_index 创建带 vectorIndex 缓存的查询对象。\nsearcher, err = xdb.new_with_vector_index(version, dbPath, v_index)\nif err ~= nil then\n    print(string.format(\"failed to create vector index searcher: %s\", err))\n    return\nend\n\n-- 3、调用查询 API，IPv4 或者 IPv6 都是同一个接口\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 4、关闭资源\nsearcher:close()\n\n-- 备注：并发使用，每个协程需要创建单独的 xdb 查询对象，但是共享全局的 v_index 对象\n```\n\n### 缓存整个 `xdb` 文件\n\n如果你的 `lua` 母环境支持，可以预先加载整个 xdb 的数据到内存，这样可以实现完全基于内存的查询，类似之前的 memory search 查询。\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1、从上述的 dbPath 加载整个 xdb 到内存。\n-- xdb内容加载一次即可，建议在服务启动的时候加载为全局对象。\ncontent, err = xdb.load_content(dbPath)\nif err ~= nil then\n    print(string.format(\"failed to load xdb content: %s\", err))\n    return\nend\n\n-- 2、使用全局的 content 创建带完全基于内存的查询对象。\nsearcher, err = xdb.new_with_buffer(version, content)\nif err ~= nil then\n    print(string.format(\"failed to create content buffer searcher: %s\", err))\n    return\nend\n\n-- 3、调用查询 API，IPv4 或者 IPv6 都是同一个接口\nlocal ip_str = \"1.2.3.4\"\n-- local ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" -- IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search_by_string(ip_str)\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, io_count: %d, took: %.5f μs}\", region, searcher:get_io_count(), xdb.now() - s_time))\n\n-- 4、关闭资源 - 该 searcher 对象可以安全用于并发，等整个服务关闭的时候再关闭 searcher\n-- searcher:close()\n\n-- 备注：并发使用，用 xdb 整个缓存创建的查询对象可以安全的用于并发。\n-- 建议在服务启动的时候创建好全局的 searcher 对象，然后全局并发使用。\n```\n\n\n# 查询测试\n\n通过 `lua search_test.lua` 脚本来进行查询测试：\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua   \nlua search_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 文件进行 IPv4 的查询测试：\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua  --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 0μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, io_count: 2, took: 0μs}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 文件进行 IPv6 的查询测试：\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua search_test.lua  --db=../../data/ip2region_v6.xdb                                                           \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 0μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, io_count: 13, took: 0μs}\n```\n\n输入 ip 即可进行查询测试。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效率。\n\n\n# bench 测试\n\n通过 `lua bench_test.lua` 脚本来进行自动 bench 测试，一方面确保 `xdb` 文件没有错误，另一方面通过大量的查询测试平均查询性能：\n```bash\n➜  lua git:(fr_lua_ipv6) ✗ lua bench_test.lua                                                                                              \nlua bench_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 文件进行 IPv4 的 bench 测试：\n```bash\nlua bench_test.lua --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 文件进行 IPv6 的 bench 测试：\n```bash\nlua bench_test.lua --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\n可以通过设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 三种不同的缓存实现的的性能。\n@Note：请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件的相同的源文件。\n"
  },
  {
    "path": "binding/lua/bench_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/06/30\n\n-- set the package to load the current xdb_searcher.so\npackage.path = \"./?.lua\" .. package.path\npackage.cpath = \"./?.so\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\nfunction printHelp()\n    print(\"lua bench_test.lua [command options]\")\n    print(\"options: \")\n    print(\" --db string             ip2region binary xdb file path\")\n    print(\" --src string            source ip text file path\")\n    print(\" --cache-policy string   cache policy: file/vectorIndex/content\")\nend\n\nif #arg < 2 then\n    printHelp()\n    return\nend\n\n-- parser the command line args\nlocal dbFile, srcFile = \"\", \"\"\nlocal cachePolicy = \"vectorIndex\"\nfor _, r in ipairs(arg) do\n    if string.len(r) < 5 then\n        goto continue\n    end\n\n    if string.sub(r, 1, 2) ~= \"--\" then\n        goto continue\n    end\n\n    for k, v in string.gmatch(string.sub(r, 3), \"([^=]+)=([^%s]+)\") do\n        if k == \"db\" then\n            dbFile = v\n        elseif k == \"src\" then\n            srcFile = v\n        elseif k == \"cache-policy\" then\n            cachePolicy = v\n        else\n            print(string.format(\"undefined option `%s`\", r))\n            return\n        end\n\n        -- break the match iterate\n        break\n    end\n\n    -- continue this loop\n    ::continue::\nend\n\n-- print(string.format(\"dbFile=%s, srcFile=%s, cachePolicy=%s\", dbFile, srcFile, cachePolicy))\nif string.len(dbFile) < 2 or string.len(srcFile) < 2 then\n    printHelp()\n    return\nend\n\n-- open the dbFile\nlocal handle, closer, err = xdb.open_file(dbFile, \"rb\")\nif err ~= nil then\n    print(string.format(\"failed to open %s: %s\", dbFile, err))\n    return\nend\n\n-- verify the xdb\nerr = xdb.verify(handle)\nif err ~= nil then\n    closer()\n    print(string.format(\"verify(%s): %s\", dbFile, err))\n    return\nend\n\n-- load the header and define the ip version\nlocal header, err = xdb.load_header(handle)\nif err ~= nil then\n    closer()\n    print(string.format(\"failed to load the header: %s\", err))\n    return\nend\n\nlocal version, err = xdb.version_from_header(header)\nif err ~= nil then\n    closer()\n    print(string.format(\"failed to detect version from header: %s\", err))\n    return\nend\n\n-- file close\ncloser(\"bench_test\")\n\n\n-- create the searcher based on the cache-policy\nlocal searcher, v_index, content\nif cachePolicy == \"file\" then\n    searcher, err = xdb.new_with_file_only(version, dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to create searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"vectorIndex\" then\n    v_index, err = xdb.load_vector_index(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load vector index: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_vector_index(version, dbFile, v_index)\n    if err ~= nil then\n        print(string.format(\"failed to create vector index searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"content\" then\n    content, err = xdb.load_content(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load xdb content: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_buffer(version, content)\n    if err ~= nil then\n        print(string.format(\"failed to create content buffer searcher: %s\", err))\n        return\n    end\nelse\n    print(string.format(\"undefined cache-policy `%s`\", cachePolicy))\n    return\nend\n\n-- do the bench test\nlocal handle = io.open(srcFile, \"r\")\nif handle == nil then\n    print(string.format(\"failed to open src text file `%s`\", handle))\n    return\nend\n\nlocal lines = handle:lines()\nlocal sip_str, eip_str, s_region, region = \"\", \"\", \"\", nil\nlocal sip, eip, err = nil, nil, nil\nlocal count, c_time = 0, 0\nlocal s_time = xdb.now()\nfor l in lines do\n    if string.len(l) < 1 then\n        goto continue\n    end\n\n    for v1, v2, v3 in string.gmatch(l, \"([^|]+)|([^|]+)|([^\\n]+)\") do\n        sip_str = v1\n        eip_str = v2\n        s_region = v3\n        break\n    end\n    -- print('sip', sip_str, 'eip', eip_str, 'region', s_region)\n\n    sip, err = xdb.parse_ip(sip_str)\n    if err ~= nil then\n        print(string.format(\"invalid start ip `%s`\", sip_str))\n        return\n    end\n\n    eip, err = xdb.parse_ip(eip_str)\n    if err ~= nil then\n        print(string.format(\"invalid end ip `%s`\", sip_str))\n        return\n    end\n\n    if xdb.ip_compare(sip, eip) > 0 then\n        print(string.format(\"start ip(%s) should not be greater than end ip(%s)\\n\", sip_str, eip_str))\n        return\n    end\n\n    local t_time = xdb.now()\n    for _, ip in ipairs({sip, eip}) do\n        region, err = searcher:search(ip)\n        if err ~= nil then\n            print(string.format(\"failed to search ip `%s`\", xdb.ip_to_string(ip)))\n            return\n        end\n\n        -- check the region\n        if region ~= s_region then\n            print(string.format(\"failed search(%s) with (%s != %s)\\n\", xdb.ip_to_string(ip), region, s_region))\n            return\n        end\n\n        count = count + 1\n    end\n    c_time = c_time + xdb.now() - t_time\n\n    ::continue::\nend\n\n-- resource cleanup\nsearcher:close()\n\n-- print the stats\nlocal total_costs = (xdb.now() - s_time) / 1e6\nlocal avg_costs = 0\nif count > 0 then\n    avg_costs = c_time / count\nend\nprint(string.format(\n    \"Bench finished, {cachePolicy: %s, total: %d, took: {total: %0.3fs, search: %0.3fs}, cost: %.3f μs/op}\", \n    cachePolicy, count, total_costs, c_time/1e6, avg_costs\n))\n"
  },
  {
    "path": "binding/lua/search_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/06/30\n\n-- set the package to load the current xdb_searcher.so\npackage.path = \"./?.lua\" .. package.path\npackage.cpath = \"./?.so\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\nfunction printHelp()\n    print(\"lua search_test.lua [command options]\")\n    print(\"options: \")\n    print(\" --db string             ip2region binary xdb file path\")\n    print(\" --cache-policy string   cache policy: file/vectorIndex/content\")\nend\n\nif #arg < 1 then\n    printHelp()\n    return\nend\n\n-- parser the command line args\nlocal dbFile = \"\"\nlocal cachePolicy = \"vectorIndex\"\nfor _, r in ipairs(arg) do\n    if string.len(r) < 5 then\n        goto continue\n    end\n\n    if string.sub(r, 1, 2) ~= \"--\" then\n        goto continue\n    end\n\n    for k, v in string.gmatch(string.sub(r, 3), \"([^=]+)=([^%s]+)\") do\n        if k == \"db\" then\n            dbFile = v\n        elseif k == \"cache-policy\" then\n            cachePolicy = v\n        else\n            print(string.format(\"undefined option `%s`\", r))\n            return\n        end\n\n        -- break the match iterate\n        break\n    end\n\n    -- continue this loop\n    ::continue::\nend\n\n-- print(string.format(\"dbFile=%s, cachePolicy=%s\", dbFile, cachePolicy))\nif string.len(dbFile) < 2 then\n    printHelp()\n    return\nend\n\n-- open the dbFile\nlocal handle, closer, err = xdb.open_file(dbFile, \"rb\")\nif err ~= nil then\n    print(string.format(\"failed to open %s: %s\", dbFile, err))\n    return\nend\n\n-- verify the xdb\nerr = xdb.verify(handle)\nif err ~= nil then\n    closer(\"xdb_verify\")\n    print(string.format(\"verify(%s): %s\", dbFile, err))\n    return\nend\n\n-- load the header and define the ip version\nlocal header, err = xdb.load_header(handle)\nif err ~= nil then\n    closer()\n    print(string.format(\"failed to load the header: %s\", err))\n    return\nend\n\nlocal version, err = xdb.version_from_header(header)\nif err ~= nil then\n    closer()\n    print(string.format(\"failed to detect version from header: %s\", err))\n    return\nend\n\n-- file close\ncloser(\"search_test\")\n\n\n-- create the searcher based on the cache-policy\nlocal searcher, v_index, content\nif cachePolicy == \"file\" then\n    searcher, err = xdb.new_with_file_only(version, dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to create searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"vectorIndex\" then\n    v_index, err = xdb.load_vector_index(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load vector index: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_vector_index(version, dbFile, v_index)\n    if err ~= nil then\n        print(string.format(\"failed to create vector index searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"content\" then\n    content, err = xdb.load_content(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load xdb content: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_buffer(version, content)\n    if err ~= nil then\n        print(string.format(\"failed to create content buffer searcher: %s\", err))\n        return\n    end\nelse\n    print(string.format(\"undefined cache-policy `%s`\", cachePolicy))\n    return\nend\n\n-- do the search\nprint(string.format([[\nip2region xdb searcher test program\nsource xdb: %s (%s, %s)\ntype 'quit' to exit]], dbFile, version.name, cachePolicy))\nlocal region, err = nil, nil\nlocal ip_bytes, s_time, c_time = nil, 0, 0\nwhile true do\n    io.write(\"ip2region>> \");\n    io.input(io.stdin);\n    local line = io.read();\n    if line == nil then\n        break\n    end\n\n    if #line < 1 then\n        goto continue\n    end\n\n    if line == \"quit\" then\n        break\n    end\n\n    s_time = xdb.now()\n    ip_bytes, err = xdb.parse_ip(line)\n    if err ~= nil then\n        print(string.format(\"failed to parse ip `%s`: %s\", line, err))\n        goto continue\n    end\n\n    -- do the search\n    region, err = searcher:search(ip_bytes)\n    if err ~= nil then\n        print(string.format(\"{err: %s, io_count: %d}\", err, searcher:get_io_count()))\n    else\n        c_time = xdb.now() - s_time\n        print(string.format(\"{region: %s, io_count: %d, took: %dμs}\", region, searcher:get_io_count(), c_time))\n    end\n\n    ::continue::\nend\n\n-- resource cleanup\nsearcher:close()"
  },
  {
    "path": "binding/lua/util_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/07/05\n\npackage.path = \"./?.lua\" .. package.path\npackage.cpath = \"./?.so\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\nfunction test_version_parse()\n    print(\"IPv4\", xdb.IPv4)\n    print(\"IPv6\", xdb.IPv6)\n\n    -- version name parse\n    local v_list = {\"v4\", \"ipv4\", \"v4x\", \"v6\", \"ipv6\", \"v6x\"}\n    for _, name in ipairs(v_list) do\n        local version, err = xdb.version_from_name(name)\n        if err ~= nil then\n            print(string.format(\"version_from_name(%s): %s\", name, err))\n        else\n            print(string.format(\"version_from_name(%s): %s\", name, version))\n        end\n    end\nend\n\nfunction test_parse_ip()\n    local ip_list = {\n        \"1.0.0.0\", \"58.251.30.115\", \"192.168.1.100\", \"126.255.32.255\", \"219.xx.xx.11\", \n        \"::\", \"::1\", \"fffe::\", \"2c0f:fff0::\", \"2c0f:fff0::1\", \"2a02:26f7:c409:4001::\",\n        \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"240e:982:e617:ffff:ffff:ffff:ffff:ffff\", \"::xx:ffff\"\n    }\n\n    for _, ip_str in ipairs(ip_list) do\n        ip_bytes, err = xdb.parse_ip(ip_str)\n        if err ~= nil then\n            print(string.format(\"failed to parse ip address `%s`: %s\", ip_str, err))\n        else\n            local ip_to_str = xdb.ip_to_string(ip_bytes)\n            print(string.format(\n                \"parse_ip(`%s`)->{bytes:%d, to_string:%s, equal:%s}\", \n                ip_str, #ip_bytes, ip_to_str, tostring(ip_str == ip_to_str)\n            ))\n        end\n    end\nend\n\nfunction test_ip_compare()\n    local ip_list = {\n        {\"1.0.0.0\", \"1.0.0.1\", -1},\n        {\"192.168.1.101\", \"192.168.1.90\", 1},\n        {\"219.133.111.87\", \"114.114.114.114\", 1},\n        {\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", -1},\n        {\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\", -1},\n        {\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\", 1}\n    }\n\n    for _,ip_pair in ipairs(ip_list) do\n        local ip1 = xdb.parse_ip(ip_pair[1])\n        local ip2 = xdb.parse_ip(ip_pair[2])\n        local cmp = xdb.ip_compare(ip1, ip2)\n        print(string.format(\"compare(%s, %s): %d ? %s\", ip_pair[1], ip_pair[2], cmp, tostring(cmp == ip_pair[3])))\n    end\nend\n\n---- buffer loading test\nfunction test_load_header()\n    header, err = xdb.load_header(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load header: \", err)\n    else\n        print(\"xdb header buffer loaded\")\n\n        local tpl = [[\n    header: {\n        version: %d\n        index_policy: %d\n        created_at: %d\n        start_index_ptr: %d\n        end_index_ptr: %d\n        ip_version: %d\n        runtime_ptr_bytes: %d\n    }]]\n\n        print(string.format(tpl,\n            header[\"version\"], header[\"index_policy\"],\n            header[\"created_at\"], header[\"start_index_ptr\"], header[\"end_index_ptr\"],\n            header[\"ip_version\"], header[\"runtime_ptr_bytes\"]\n        ))\n    end\nend\n\nfunction test_load_vector_index()\n    v_index, err = xdb.load_vector_index(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load vector index: \", err)\n    else\n        print(\"xdb vector index buffer loaded, length=\", #v_index)\n    end\nend\n\nfunction test_load_content()\n    c_buffer, err = xdb.load_content(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load content: \", err)\n    else\n        print(\"xdb content buffer loaded, length=\", #c_buffer)\n    end\nend\n\nfunction test_verify()\n    local xdb_files = {\n        \"../../data/ip2region_v4.xdb\",\n        \"../../data/ip2region_v6.xdb\"\n    }\n\n    for _, path in ipairs(xdb_files) do\n        local err = xdb.verify(path)\n        if err ~= nil then\n            print(string.format(\"verify(%s): %s\", path, err))\n        else\n            print(string.format(\"verify(%s): Ok\", path))\n        end\n    end\nend\n\nfunction test_ip_search()\n    local test_list = {\n        -- ipv4\n        {\"1.2.3.4\", xdb.IPv4, \"../../data/ip2region_v4.xdb\"},\n        -- ipv6\n        {\"240e:3b7:3272:d8d0:db09:c067:8d59:539e\", xdb.IPv6, \"../../data/ip2region_v6.xdb\"}\n    }\n\n    for _, test in ipairs(test_list) do\n        -- ipv6\n        local ip_str = test[1]\n        searcher, err = xdb.new_with_file_only(test[2], test[3])\n        t_start = xdb.now()\n        region, err = searcher:search_by_string(ip_str)\n        if err ~= nil then\n            print(string.format(\"failed to search(%s): %s\", ip_str, err))\n        else\n            local c_time = xdb.now() - t_start\n            print(string.format(\n                \"search(%s): {region:%s, io_count:%d, took:%dμs}\", \n                ip_str, region, searcher:get_io_count(), c_time\n            ))\n        end\n        searcher:close()\n    end\nend\n\n\n-- check and call the function\n\nlocal func_name = arg[1]\nif func_name == nil then\n    print(\"please specified the function to test\")\n    return\nend\n\nif (_G[func_name] == nil) then\n    print(string.format(\"undefined function `%s` to call\", func_name))\n    return\nend\n\nlocal s_time = xdb.now();\nprint(string.format(\"+---calling test function %s ...\", func_name))\n_G[func_name]();\nlocal cost_time = xdb.now() - s_time\nprint(string.format(\"|---done, elapsed %.3fμs\", cost_time))"
  },
  {
    "path": "binding/lua/xdb_searcher.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/07/05\n\n-- constants define\nlocal header_info_length  = 256\nlocal vector_index_rows   = 256\nlocal vector_index_cols   = 256\nlocal vector_index_size   = 8\nlocal vector_index_length = 524288  -- cols x rows * 8\n\nlocal xdb_structure_20 = 2\nlocal xdb_structure_30 = 3\nlocal xdb_ipv4_id = 4\nlocal xdb_ipv6_id = 6\n\nlocal xdb = {\n    -- ip version\n    version = nil,\n\n    -- xdb file handle\n    handle = nil,\n\n    -- header info\n    header = nil,\n    io_count = 0,\n\n    -- vector index\n    vector_index = nil,\n\n    -- xdb content buffer\n    content_buff = nil\n}\n\n-- index and to string attribute set\nxdb.__index = xdb\nxdb.__tostring = function(self)\n    return \"xdb searcher object (lua)\"\nend\n\n-- construct functions\nfunction new_base(version, db_path, v_index, c_buffer)\n    local obj = setmetatable({}, xdb)\n    obj.version = version\n    if c_buffer ~= nil then\n        obj.io_count = 0\n        obj.vector_index = nil\n        obj.content_buff = c_buffer\n    else\n        obj.io_count = 0\n        obj.vector_index = v_index\n        obj.handle = io.open(db_path, \"r\")\n        if obj.handle == nil then\n            return nil, string.format(\"failed to open xdb file `%s`\", db_path)\n        end\n    end\n\n    return obj, nil\nend\n\nfunction xdb.new_with_file_only(version, db_path)\n    return new_base(version, db_path, nil, nil)\nend\n\nfunction xdb.new_with_vector_index(version, db_path, v_index)\n    return new_base(version, db_path, v_index, nil)\nend\n\nfunction xdb.new_with_buffer(version, c_buffer)\n    return new_base(version, nil, nil, c_buffer)\nend\n\n-- End of constructors\n\n-- object api impl, must call via ':'\n\nfunction xdb:search_by_string(ip_str)\n    local ip_bytes, err = xdb.parse_ip(ip_str)\n    if err ~= nil then\n        return \"\", string.format(\"failed to parse string ip `%s`: %s\", ip_str, err)\n    end\n\n    return self:search(ip_bytes)\nend\n\nfunction xdb:search(ip_bytes)\n    -- check the bytes ip\n    if type(ip_bytes) ~= \"string\" then\n        return \"\", string.format(\"invalid bytes ip `%s`\", ip_bytes)\n    end\n\n    -- ip version check\n    local version = self.version\n    if #ip_bytes ~= version.bytes then\n        return \"\", string.format(\"invalid ip address `%s` (%s expected)\", xdb.ip_to_string(ip_bytes), version.name);\n    end\n\n    -- reset the global counter\n    -- and global resource local cache\n    self.io_count = 0\n    local vector_index = self.vector_index\n    local content_buff = self.content_buff\n    local read_data = self.read\n\n    -- locate the segment index based on the vector index\n    local il0 = string.byte(ip_bytes, 1) & 0xFF\n    local il1 = string.byte(ip_bytes, 2) & 0xFF\n    local idx = il0 * vector_index_cols * vector_index_size + il1 * vector_index_size\n    local s_ptr, e_ptr = 0, 0\n    if vector_index ~= nil then\n        s_ptr = le_get_uint32(vector_index, idx + 1)\n        e_ptr = le_get_uint32(vector_index, idx + 5)\n    elseif content_buff ~= nil then\n        s_ptr = le_get_uint32(content_buff, header_info_length + idx + 1)\n        e_ptr = le_get_uint32(content_buff, header_info_length + idx + 5)\n    else\n        -- load from the file\n        buff, err = read_data(self, header_info_length + idx, vector_index_size)\n        if err ~= nil then\n            return \"\", string.format(\"read buffer: %s\", err)\n        end\n\n        s_ptr = le_get_uint32(buff, 1)\n        e_ptr = le_get_uint32(buff, 5)\n    end\n\n    -- print(string.format(\"s_ptr: %d, e_ptr: %d\", s_ptr, e_ptr))\n    -- @Note: ptr validate, zero ptr means source data missing\n    -- so we could just stop here and return an empty string.\n    if s_ptr == 0 or e_ptr == 0 then\n        return \"\", nil\n    end\n\n\n    -- binary search to get the data\n    local index_size, ip_sub_compare = version.index_size, version.ip_sub_compare\n    local bytes, d_bytes = version.bytes, version.bytes << 1\n    local data_ptr, data_len, p = 0, 0, 0\n    local buff, err = nil, nil\n    local l, m, h = 0, 0, (e_ptr - s_ptr) / index_size\n    while l <= h do\n        m = (l + h) >> 1\n        p = s_ptr + m * index_size\n\n        -- read the segment index\n        buff, err = read_data(self, p, index_size)\n        if err ~= nil then\n            return \"\", string.format(\"read segment index at %d\", p)\n        end\n\n        -- check the index\n        if ip_sub_compare(ip_bytes, buff, 1) < 0 then\n            h = m - 1\n        elseif ip_sub_compare(ip_bytes, buff, bytes + 1) > 0 then\n            l = m + 1\n        else\n            data_len = le_get_uint16(buff, d_bytes + 1)\n            data_ptr = le_get_uint32(buff, d_bytes + 3)\n            break\n        end\n    end\n\n    -- matching nothing interception\n    -- print(string.format(\"data_len=%d, data_ptr=%d\", data_len, data_ptr))\n    if data_len == 0 then\n        return \"\", nil\n    end\n\n    -- load and return the region data\n    buff, err = read_data(self, data_ptr, data_len)\n    if err ~= nil then\n        return \"\", string.format(\"read data at %d:%d\", data_ptr, data_len)\n    end\n\n    return buff, nil\nend\n\n\n-- read specified bytes from the specified index\n\nfunction xdb:read(offset, length)\n    -- local cache\n    local content_buff = self.content_buff\n    local handle = self.handle\n\n    -- check the in-memory buffer first\n    if content_buff ~= nil then\n        return string.sub(content_buff, offset + 1, offset + length), nil\n    end\n\n    -- read from the file\n    local r = handle:seek(\"set\", offset)\n    if r == nil then\n        return nil, string.format(\"seek to offset %d\", offset)\n    end\n\n    self.io_count = self.io_count + 1\n    local buff = handle:read(length)\n    if buff == nil then\n        return nil, string.format(\"read %d bytes\", length)\n    end\n\n    return buff, nil\nend\n\nfunction xdb:get_ip_version()\n    return self.version\nend\n\nfunction xdb:get_io_count()\n    return self.io_count\nend\n\nfunction xdb:close()\n    if self.handle ~= nil then\n        self.handle:close()\n    end\nend\n\n---\n-- internal function to decode buffer\nfunction le_get_uint32(buff, idx)\n    local i1 = (string.byte(buff, idx))\n    local i2 = (string.byte(buff, idx+1) << 8)\n    local i3 = (string.byte(buff, idx+2) << 16)\n    local i4 = (string.byte(buff, idx+3) << 24)\n    return (i1 | i2 | i3 | i4)\nend\n\nfunction le_get_uint16(buff, idx)\n    local i1 = (string.byte(buff, idx))\n    local i2 = (string.byte(buff, idx+1) << 8)\n    return (i1 | i2)\nend\n\n-- static util functions\n\nfunction xdb.open_file(db_path, mode)\n    local t, handle = type(db_path), nil\n    local _closer = nil\n    if t == \"userdata\" then\n        handle = db_path   -- file handle\n        _closer = function(caller) end\n    elseif t == \"string\" then\n        handle = io.open(db_path, mode)\n        if handle == nil then\n            return nil, nil, string.format(\"failed to open xdb file `%s`\", db_path)\n        end\n\n        _closer = function(caller) \n            handle:close()\n        end\n    end\n\n    return handle, _closer, nil\nend\n\nfunction xdb.load_header(db_path)\n    local handle, closer, err = xdb.open_file(db_path, \"rb\")\n    if err ~= nil then\n        return nil, err\n    end\n\n    local r = handle:seek(\"set\", 0)\n    if r == nil then\n        closer(\"load_header\")\n        return nil, \"failed to seek to 0\"\n    end\n\n    local c = handle:read(header_info_length)\n    if c == nil then\n        closer(\"load_header\")\n        return nil, string.format(\"failed to read %d bytes\", header_info_length)\n    end\n\n    closer(\"load_header\")\n    return {\n        [\"version\"] = le_get_uint16(c, 1),\n        [\"index_policy\"] = le_get_uint16(c, 3),\n        [\"created_at\"] = le_get_uint32(c, 5),\n        [\"start_index_ptr\"] = le_get_uint32(c, 9),\n        [\"end_index_ptr\"] = le_get_uint32(c, 13),\n\n        -- xdb 3.0 since IPv6 supporting\n        [\"ip_version\"] = le_get_uint16(c, 17),\n        [\"runtime_ptr_bytes\"] = le_get_uint16(c, 19),\n\n        [\"raw_data\"] = c\n    }, nil\nend\n\nfunction xdb.load_vector_index(db_path)\n    local handle, closer, err = xdb.open_file(db_path, \"rb\")\n    if err ~= nil then\n        return nil, err\n    end\n\n    local r = handle:seek(\"set\", header_info_length)\n    if r == nil then\n        closer(\"load_vector_index\")\n        return nil, string.format(\"failed to seek to %d\", header_info_length)\n    end\n\n    local c = handle:read(vector_index_length)\n    if c == nil then\n        closer(\"load_vector_index\")\n        return nil, string.format(\"failed to read %d bytes\", vector_index_length)\n    end\n\n    closer(\"load_vector_index\")\n    return c, nil\nend\n\nfunction xdb.load_content(db_path)\n    local handle, closer, err = xdb.open_file(db_path, \"rb\")\n    local c = handle:read(\"*a\")\n    if c == nil then\n        closer(\"load_content\")\n        return nil, string.format(\"failed to read xdb content\")\n    end\n\n    closer(\"load_content\")\n    return c, nil\nend\n\n-- Verify if the current Searcher could be used to search the specified xdb file.\n-- Why do we need this check ?\n-- The future features of the xdb impl may cause the current searcher not able to work properly.\n--\n-- @Note: You Just need to check this ONCE when the service starts\n-- Or use another process (eg, A command) to check once Just to confirm the suitability.\nfunction xdb.verify(db_path)\n    local handle, closer, err = xdb.open_file(db_path, \"rb\")\n    if err ~= nil then\n        return err\n    end\n\n    -- load the header from handle\n    local header, err = xdb.load_header(handle)\n    if err ~= nil then\n        closer()\n        return string.format(\"failed to load header: %s\", err)\n    end\n\n    -- get the runtime ptr bytes\n    local runtime_ptr_bytes = 0\n    if header.version == xdb_structure_20 then\n        runtime_ptr_bytes = 4\n    elseif header.version == xdb_structure_30 then\n        runtime_ptr_bytes = header.runtime_ptr_bytes\n    else\n        closer()\n        return string.format(\"invalid structure version %d\", header.version);\n    end\n\n    -- 1, confirm the xdb file size\n    -- to ensure that the maximum file pointer does not overflow\n    local max_file_ptr = ((1 << (runtime_ptr_bytes * 8)) & 0xFFFFFFFFFFFFFFFF) - 1\n    local _file_bytes  = handle:seek(\"end\", 0)\n    -- print(\"max_file_ptr=\", max_file_ptr, \"_file_bytes\", _file_bytes)\n    if _file_bytes > max_file_ptr then\n        closer()\n        return string.format(\"xdb file exceeds the maximum supported bytes: %d\", max_file_ptr);\n    end\n\n    closer()\n    return nil\nend\n\n-- \n-- parse ip string\nfunction split(str, sep)\n    local ps, sIndex, length = {}, 1, #str\n    -- loop to find all parts\n    while true do\n        local mi = string.find(str, sep, sIndex, true)\n        if mi == nil then\n            table.insert(ps, string.sub(str, sIndex))\n            break\n        end\n\n        if sIndex == mi then\n            table.insert(ps, \"\")\n        else \n            table.insert(ps, string.sub(str, sIndex, mi - 1))\n        end\n\n        -- reset the start index\n        sIndex = mi + 1\n    end\n\n    return ps\nend\n\nfunction _parse_ipv4_addr(v4_str)\n    local ps = split(v4_str, \".\")\n    if #ps ~= 4 then\n        return nil, string.format(\"invalid ipv4 address `%s`\", v4_str)\n    end\n\n    local bytes = {0x00, 0x00, 0x00, 0x00}\n    for i, s in ipairs(ps) do\n        local v = tonumber(s)\n        if v == nil then\n            return nil, string.format(\"invalid ipv4 part `%s`, a valid number expected\", s)\n        end\n\n        if v < 0 or v > 255 then\n            return nil, string.format(\"invalid ipv4 part `%s`, should <=0 and <= 255\", s)\n        end\n\n        bytes[i] = v\n    end\n\n    return string.char(table.unpack(bytes)), nil\nend\n\nfunction _parse_ipv6_addr(v6_str)\n    local ps = split(v6_str, ':')\n    if #ps < 3 or #ps > 8 then\n        return nil, string.format(\"invalid ipv6 address `%s`\", v6_str)\n    end\n\n    local bytes = {\n        0x00, 0x00, 0x00, 0x00,\n        0x00, 0x00, 0x00, 0x00,\n        0x00, 0x00, 0x00, 0x00,\n        0x00, 0x00, 0x00, 0x00\n    }\n\n    local i, v, dc_num, offset, length = 1, 0, 0, 1, #ps\n\n    -- process the v6 parts\n    while i <= length do\n        local s = ps[i]:match(\"^%s*(.-)%s*$\")\n        -- Double colon check and auto padding\n        if #s == 0 then\n            -- ONLY one double colon allow\n            if dc_num > 0 then\n                return nil, \"invalid ipv6 address: multi double colon detected\"\n            end\n\n            -- clear all the consecutive spaces\n            local start = i\n            i = i + 1\n            while true do\n                s = ps[i]:match(\"^%s*(.-)%s*$\")\n                if #s > 0 then\n                    i = i - 1\n                    break\n                end\n\n                if i >= length then\n                    break\n                end\n\n                i = i + 1\n            end\n\n            dc_num = 1\n            -- padding = 9 - start - left\n            local padding = 9 - start - (length - i)\n            offset = offset + 2 * padding\n            -- print(\"-> i \", i, \"start\", start, \"padding: \", padding, \"offset\", offset)\n            goto continue\n        end\n\n        v = tonumber(s, 16);\n        if v == nil then\n            return nil, string.format(\"invalid ipv6 part `%s`, a valid hex number expected\", ps[i])\n        end\n\n        if v < 0 or v > 0xFFFF then\n            return nil, string.format(\"invalid ipv6 part `%s` should >= 0 and <= 65534\", ps[i])\n        end\n\n        bytes[offset] = (v >> 8) & 0xFF\n        bytes[offset+1] = (v  & 0xFF)\n        offset = offset + 2\n\n        ::continue::\n        i = i + 1\n    end\n\n    return string.char(table.unpack(bytes))\nend\n\nfunction xdb.parse_ip(ip_str)\n    local s_dot = string.find(ip_str, \".\", 1, true)\n    local c_dot = string.find(ip_str, \":\", 1, true)\n    if s_dot ~= nil and c_dot == nil then\n        return _parse_ipv4_addr(ip_str)\n    elseif c_dot ~= nil then\n        return _parse_ipv6_addr(ip_str)\n    else\n        return nil, string.format(\"invalid ip address `%s`\", ip_str)\n    end\nend\n\n--\n-- ip to string\nfunction _ipv4_to_string(ip_bytes)\n    return string.format(\n        \"%d.%d.%d.%d\", \n        string.byte(ip_bytes, 1), \n        string.byte(ip_bytes, 2),\n        string.byte(ip_bytes, 3),\n        string.byte(ip_bytes, 4)\n    ), nil\nend\n\nfunction _ipv6_to_string(ip_bytes, compress)\n    local ps, i, hex = {}, 0, 0\n    local last_hex, need_compress = -1, false\n    for i = 1, #ip_bytes, 2 do\n        hex = (string.byte(ip_bytes, i) << 8) | string.byte(ip_bytes, i + 1)\n        table.insert(ps, string.format(\"%x\", hex))\n\n        -- check the necessity for compress\n        if last_hex > -1 \n            and hex == 0 \n            and last_hex == 0 then\n            need_compress = true\n        end\n\n        -- reset the last hex\n        last_hex = hex\n    end\n\n    -- print('need_compress', need_compress)\n    if need_compress == false\n        or (compress ~= nil and compress == false) then\n        return table.concat(ps, ':'), nil\n    end\n\n    -- auto compression of consecutive zero\n    local i, j, length, mi = 1, 0, #ps, #ps + 1\n    local _ = {}\n    while i <= length do\n        if i >= length or j > 0 then\n            table.insert(_, ps[i])\n            goto continue\n        end\n\n        if ps[i] ~= '0' or ps[i+1] ~= '0' then\n            table.insert(_, ps[i])\n            goto continue\n        end\n\n        -- find the first two zero part\n        -- and keep find all the zero part\n        i = i + 2\n        while i <= length do\n            if ps[i] ~= '0' then\n                i = i - 1\n                break\n            end\n            i = i + 1\n        end\n\n        -- make sure there is an empty head.\n        if #_ == 0 then\n            table.insert(_, '')\n        end\n\n        table.insert(_, '') -- empty for double colon\n\n        -- make sure there is an empty tail\n        if i == mi and #_ < length then\n            table.insert(_, '')\n        end\n        \n        --continue\n        ::continue::\n        i = i + 1\n    end\n\n    return table.concat(_, ':')\nend\n\nfunction xdb.ip_to_string(ip_bytes, compress)\n    local l = #ip_bytes\n    if l == 4 then\n        return _ipv4_to_string(ip_bytes)\n    elseif l == 16 then\n        return _ipv6_to_string(ip_bytes, compress)\n    else\n        return nil, string.format(\"invalid bytes ip with length not 4 or 6\")\n    end\nend\n\n-- \n-- ip bytes compare\nfunction xdb.ip_sub_compare(ip1, buff, offset)\n    local ip2 = string.sub(buff, offset, offset + #ip1 - 1)\n    if ip1 > ip2 then\n        return 1\n    elseif ip1 < ip2 then\n        return -1\n    else\n        return 0\n    end\nend\n\nfunction xdb.ip_compare(ip1, ip2)\n    return xdb.ip_sub_compare(ip1, ip2, 1)\nend\n\n-- this is a bit weird\n-- but we have no better choice for now\nfunction xdb.now()\n    return os.time() * 1e6\nend\n\n---\n-- ip version\n\nlocal Version = {\n    __tostring = function(t)\n        return string.format(\n            '{id:%d, name:%s, bytes:%d, index_size:%d}',\n            t.id, t.name, t.bytes, t.index_size\n        )\n    end\n}\n\nlocal IPv4 = {\n    id = xdb_ipv4_id,\n    name = \"IPv4\",\n    bytes = 4,\n    index_size = 14,  -- 14 = 4 + 4 + 2 + 4\n    ip_sub_compare = function(ip1, buff, offset)\n        -- ip1: Big endian byte order parsed from input\n        -- ip2: Little endian byte order read from xdb index.\n        -- @Note: to compatible with the old Litten endian index encode implementation.\n        local l = #ip1\n        local j = offset + l - 1\n        for i = 1, l, 1 do\n            local i1 = string.byte(ip1, i)\n            local i2 = string.byte(buff, j)\n            if i1 > i2 then\n                return 1\n            end\n\n            if i1 < i2 then\n                return -1\n            end\n            \n            j = j - 1\n        end\n\n        return 0\n    end\n}\n\nlocal IPv6 = {\n    id = xdb_ipv6_id,\n    name = \"IPv6\",\n    bytes = 16,\n    index_size = 38,  -- 38 = 16 + 16 + 2 + 4\n    ip_sub_compare = xdb.ip_sub_compare\n}\n\nsetmetatable(IPv4, Version)\nsetmetatable(IPv6, Version)\n\nxdb.version_from_name = function(name)\n    local n = string.upper(name)\n    if n == \"V4\" or n == \"IPV4\" then\n        return IPv4, nil\n    elseif n == \"V6\" or n == \"IPV6\" then\n        return IPv6, nil\n    else\n        return nil, string.format(\"invalid version name `%s`\", name)\n    end\nend\n\nxdb.version_from_header = function(header)\n    -- old structure with ONLY IPv4 supporting\n    if header.version == xdb_structure_20 then\n        return IPv4, nil\n    end\n\n    -- structure 3.0 with IPv6 supporting\n    if header.version ~= xdb_structure_30 then\n        return nil, string.format(\"unsupported structure version `%d`\", header.version)\n    end\n\n    local ip_ver = header.ip_version\n    if ip_ver == xdb_ipv4_id then\n        return IPv4, nil\n    elseif ip_ver == xdb_ipv6_id then\n        return IPv6, nil\n    else\n        return nil, string.format(\"unkown ip version id `%d`\", ip_ver)\n    end\nend\n\n-- constants register\nxdb.ipv4_id = xdb_ipv4_id\nxdb.ipv6_id = xdb_ipv6_id\nxdb.structure_20 = xdb_structure_20\nxdb.structure_30 = xdb_structure_30\nxdb.IPv4 = IPv4\nxdb.IPv6 = IPv6\n\nreturn xdb\n"
  },
  {
    "path": "binding/lua_c/Makefile",
    "content": "LuaVersion ?= 5.4\nLIB_DIR = /usr/local/share/lua/$(LuaVersion)\n\nall: ../c/xdb_api.h ../c/xdb_util.c ../c/xdb_searcher.c xdb_searcher.c\n\tgcc -std=c99 -Wall -O2 -I../c/ -I/usr/include/lua$(LuaVersion) ../c/xdb_util.c ../c/xdb_searcher.c xdb_searcher.c -fPIC -shared -o xdb_searcher.so\n\ninstall:\n\tsudo mkdir -p $(LIB_DIR); \\\n\tsudo cp xdb_searcher.so $(LIB_DIR);\\\n\techo \"install xdb searcher to $(LIB_DIR) successfully.\";\\\n\nclean:\n\tfind . -name \\*.so | xargs rm -f\n\tfind . -name \\*.o  | xargs rm -f\n\n.PHONY: clean\n"
  },
  {
    "path": "binding/lua_c/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region lua c extension query client\n\n# Version Compatibility\n\nThis implementation is compatible with lua `5.1`, `5.2`, `5.3`, and `5.4`.\n\n# Compilation and Installation\n\n### Default Compilation\n\nUse the following commands to compile and install the default `Lua5.4` version of the extension:\n\n```bash\n# cd to the root directory of lua_c binding\nmake\nsudo make install\n```\n\n### Specify Lua Version\n\nSpecify the Lua version for compilation using the `LuaVersion` parameter, for example: `5.1` / `5.2` / `5.3` / `5.4`\n\n```bash\n# cd to the root directory of lua_c binding\n# For example, compile the extension compatible with version 5.1\nmake LuaVersion=5.1\nsudo make install\n```\n\nNote: Please use the same version of `lua` to run the following tests as the one used to compile the extension. For example:\n\n```bash\n# Compile extension using lua 5.1\nmake LuaVersion=5.1\n\n# Run query test using lua5.1\nlua5.1 search_test.py --db=../../data/ip2region_v4.xdb\n```\n\n# Usage\n\n### About Query API\n\nThe prototype of the query API is as follows:\n\n```lua\n-- Query via IP string or binary IP parsed by xdb.parse_ip\nsearch(ip_string | ip_bytes) (region, error)\n```\n\nIf the query fails, `error` will be a non-`nil` error description string. If successful, it returns the `region` information as a string. If the IP address is not found, it returns an empty string `\"\"`.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. Usage is as follows:\n\n```lua\n-- Import xdb searcher extension\nlocal xdb = require(\"xdb_searcher\")\n\n-- For IPv4: Set xdb path to v4 xdb file, specify IP version as IPv4\nlocal db_path  = \"../../data/ip2region_v4.xdb\"  -- or your ipv4 xdb path\nlocal version = xdb.IPv4\n\n-- For IPv6: Set xdb path to v6 xdb file, specify IP version as IPv6\nlocal db_path  = \"../../data/ip2region_v6.xdb\";  -- or your ipv6 xdb path\nlocal version = xdb.IPv6\n\n-- The IP version of the xdb specified by db_path must match the version specified, otherwise an error will occur during query execution\n-- Note: The following demonstration directly uses the db_path and version variables\n```\n\n### XDB File Verification\n\nIt is recommended to actively verify the suitability of the xdb file. New features in the future may cause the current Searcher version to be incompatible with the xdb file you are using. Verification helps avoid unpredictable errors during runtime. You don't need to verify every time; for example, verify when the service starts or by manually calling the verification command. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- verify the xdb\nif xdb.verify(db_path) == false then\n    -- Suitability verification failed!!!\n    -- The current query client implementation is not suitable for the xdb file specified by db_path.\n    -- You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with db_path.\n    print(string.format(\"failed to verify the xdb file: %s\", db_path))\n    return\nend\n\n-- Verification passed, the current Searcher can safely be used for query operations on the xdb pointed to by db_path\n```\n\n### Entirely File-Based Query\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Create a file-based xdb query object from db_path using version\nlocal searcher, err = xdb.new_with_file_only(version, db_path)\nif err ~= nil then\n    print(string.format(\"failed to create searcher: %s\", err))\n    return\nend\n\n-- 2. Call the query API; both IPv4 and IPv6 are supported\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() - s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- Note: For concurrent use, each coroutine needs to create a separate xdb query object\n\n-- 3. Close the xdb searcher\nsearcher:close()\n\n--\n-- 4. Module resource cleanup, only call before the entire service is completely shut down\nxdb.cleanup()\n```\n\n### Caching `VectorIndex`\n\nIf supported by your `lua` environment, you can pre-load the `vectorIndex` cache and make it a global variable. Using the global `vectorIndex` every time a Searcher is created can reduce one fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Load VectorIndex cache from the specified db_path and make the v_index object below a global variable.\n-- vectorIndex only needs to be loaded once; it is recommended to load it as a global object when the service starts.\nv_index, err = xdb.load_vector_index(db_path)\nif err ~= nil then\n    print(string.format(\"failed to load vector index from '%s'\", db_path))\n    return\nend\n\n-- 2. Use the global v_index to create a query object with VectorIndex cache.\nsearcher, err = xdb.new_with_vector_index(version, db_path, v_index)\nif err ~= nil then\n    print(string.format(\"failed to create vector index searcher: %s\", err))\n    return\nend\n\n-- 3. Call the query API; both IPv4 and IPv6 are supported\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() = s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- Note: For concurrent use, each coroutine needs to create a separate xdb query object, but they share the global v_index object\n\n-- 4. Close the xdb searcher\nsearcher:close()\n\n--\n-- 5. Module resource cleanup, only call before the entire service is completely shut down\nxdb.cleanup()\n```\n\n### Caching the Entire `xdb` File\n\nIf supported by your `lua` environment, you can pre-load the entire xdb data into memory to achieve completely memory-based queries, similar to the previous memory search.\n\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1. Load the entire xdb into memory from the specified db_path.\n-- xdb content only needs to be loaded once; it is recommended to load it as a global object when the service starts.\nlocal content = xdb.load_content(db_path)\nif content == nil then\n    print(string.format(\"failed to load xdb content from '%s'\", db_path))\n    return\nend\n\n-- 2. Use the global content to create a query object based entirely on memory.\nsearcher, err = xdb.new_with_buffer(version, content)\nif err ~= nil then\n    print(string.format(\"failed to create content buffer searcher: %s\", err))\n    return\nend\n\n-- 3. Call the query API; both IPv4 and IPv6 are supported\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() - s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- Note: For concurrent use, query objects created with the entire xdb cache can be safely used concurrently.\n-- It is recommended to create a global searcher object when the service starts and then use it globally and concurrently.\n\n-- 4. Close the xdb searcher\nsearcher:close()\n\n--\n-- 5. Module resource cleanup, only call before the entire service is completely shut down\nxdb.cleanup()\n```\n\n# Query Testing\n\nPerform query tests via the `search_test.lua` script:\n\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./search_test.lua \nlua search_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: using the default `data/ip2region_v4.xdb` for IPv4 query testing:\n\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./search_test.lua --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 17μs}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 40μs}\n```\n\nFor example: using the default `data/ip2region_v6.xdb` for IPv6 query testing:\n\n```bash\n➜  lua_c git:(master) lua ./search_test.lua --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , io_count: 1, took: 48μs}\nip2region>> 240e:3b7:3276:33b0:958f:f34c:d04f:f6a\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 52μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, io_count: 13, took: 35μs}\n```\n\nEnter an IP to perform a query test. You can also set `cache-policy` to `file`/`vectorIndex`/`content` respectively to test the efficiency of the three different cache implementations.\n\n# Bench Testing\n\nPerform automatic bench testing via the `bench_test.lua` script. This ensures that the `xdb` file has no errors and tests average query performance through a large number of queries:\n\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua \nlua bench_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: perform IPv4 bench testing using default `data/ip2region_v4.xdb` and `data/ipv4_source.txt`:\n\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\nBench finished, {cachePolicy: vectorIndex, total: 1367686, took: 8.593 s, cost: 5.433 μs/op}\n```\n\nFor example: perform IPv6 bench testing using default `data/ip2region_v6.xdb` and `data/ipv6_source.txt`:\n\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt                       \nBench finished, {cachePolicy: vectorIndex, total: 34159862, took: 829.008 s, cost: 23.176 μs/op}\n```\n\nYou can test the performance of the three different cache implementations (`file`/`vectorIndex`/`content`) by setting the `cache-policy` parameter.\n@Note: Please ensure that the `src` file used for the bench is the same source file used to generate the corresponding `xdb` file.\n"
  },
  {
    "path": "binding/lua_c/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region lua c 扩展查询客户端\n\n# 版本兼容\n该实现兼容 lua `5.1`，`5.2`，`5.3`, `5.4`\n\n# 编译安装\n\n### 默认编译\n通过如下方式来编译安装默认的 `Lua5.4` 版本的扩展：\n```bash\n# cd 到 lua_c binding 的根目录\nmake\nsudo make install\n```\n\n### 指定 Lua 版本\n通过如下的 `LuaVersion` 参数指定 Lua 版本编译，例如：`5.1` / `5.2` / `5.3` / `5.4`\n```bash\n# cd 到 lua_c binding 的根目录\n# 例如，编译 5.1 版本兼容的扩展\nmake LuaVersion=5.1\nsudo make install\n```\n\n备注：使用了指定的版本的 lua 编译的扩展就请使用相同版本的`lua`去运行以下的测试，例如：\n```bash\n# 使用 lua 5.1 编译扩展\nmake LuaVersion=5.1\n\n# 使用 lua5.1 运行查询测试\nlua5.1 search_test.py --db=../../data/ip2region_v4.xdb\n```\n\n\n\n# 使用方式\n\n### 关于查询 API\n查询 API 的原型如下：\n```lua\n-- 通过字符串 IP 或者 xdb.parse_ip 解析得到的二进制 IP 进行查询\nsearch(ip_string | ip_bytes) (region, error)\n```\n如果查询失败则 error 将会为一个非 `nil` 的错误描述字符串，查询成功将会返回字符串的 `region` 信息，如果查询的 IP 地址没找到则会返回一个空字符 `\"\"`。\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```lua\n-- 引入 xdb searcher 扩展\nlocal xdb = require(\"xdb_searcher\")\n\n-- 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 IPv4\nlocal db_path  = \"../../data/ip2region_v4.xdb\"  -- 或者你的 ipv4 xdb 的路径\nlocal version = xdb.IPv4\n\n-- 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 IPv6\nlocal db_path  = \"../../data/ip2region_v6.xdb\";  -- 或者你的 ipv6 xdb 路径\nlocal version = xdb.IPv6\n\n-- db_path 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n-- 备注：以下演示直接使用 db_path 和 version 变量\n```\n\n### XDB 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- verify the xdb\nif xdb.verify(db_path) == false then\n    -- 适用性验证失败！！！\n    -- 当前查询客户端实现不适用于 db_path 指定的 xdb 文件的查询.\n    -- 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 db_path 的 Searcher 实现。\n    print(string.format(\"failed to verify the xdb file: %s\", db_path))\n    return\nend\n\n-- 验证通过，当前使用的 Searcher 可以安全的用于对 db_path 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1、使用 version 从 db_path 创建基于文件的 xdb 查询对象\nlocal searcher, err = xdb.new_with_file_only(version, db_path)\nif err ~= nil then\n    print(string.format(\"failed to create searcher: %s\", err))\n    return\nend\n\n-- 2、调用查询 API 进行查询，IPv4 和 IPv6 都支持\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() - s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- 备注：并发使用，每个协程需要创建单独的 xdb 查询对象\n\n-- 3，关闭 xdb 查询器\nsearcher:close()\n\n--\n-- 4，模块资源清理，仅在需要将整个服务完全关闭前调用\nxdb.cleanup()\n```\n\n### 缓存 `VectorIndex` 索引\n\n如果你的 `lua` 母环境支持，可以预先加载 vectorIndex 缓存，然后做成全局变量，每次创建 Searcher 的时候使用全局的 vectorIndex，可以减少一次固定的 IO 操作从而加速查询，减少 io 压力。\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1、从指定的 db_path 加载 VectorIndex 缓存，把下述的 v_index 对象做成全局变量。\n-- vectorIndex 加载一次即可，建议在服务启动的时候加载为全局对象。\nv_index, err = xdb.load_vector_index(db_path)\nif err ~= nil then\n    print(string.format(\"failed to load vector index from '%s'\", db_path))\n    return\nend\n\n-- 2、使用全局的 v_index 创建带 VectorIndex 缓存的查询对象。\nsearcher, err = xdb.new_with_vector_index(version, db_path, v_index)\nif err ~= nil then\n    print(string.format(\"failed to create vector index searcher: %s\", err))\n    return\nend\n\n-- 3、调用查询 API ，IPv4 和 IPv6 都支持\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() = s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- 备注：并发使用，每个协程需要创建单独的 xdb 查询对象，但是共享全局的 v_index 对象\n\n-- 4，关闭 xdb 查询器\nsearcher:close()\n\n--\n-- 5，模块资源清理，仅在需要将整个服务完全关闭前调用\nxdb.cleanup()\n```\n\n### 缓存整个 `xdb` 文件\n\n如果你的 `lua` 母环境支持，可以预先加载整个 xdb 的数据到内存，这样可以实现完全基于内存的查询，类似之前的 memory search 查询。\n```lua\nlocal xdb = require(\"xdb_searcher\")\n\n-- 1、从指定的 db_path 加载整个 xdb 到内存。\n-- xdb内容加载一次即可，建议在服务启动的时候加载为全局对象。\nlocal content = xdb.load_content(db_path)\nif content == nil then\n    print(string.format(\"failed to load xdb content from '%s'\", db_path))\n    return\nend\n\n-- 2、使用全局的 content 创建带完全基于内存的查询对象。\nsearcher, err = xdb.new_with_buffer(version, content)\nif err ~= nil then\n    print(string.format(\"failed to create content buffer searcher: %s\", err))\n    return\nend\n\n-- 3、调用查询 API ，IPv4 和 IPv6 都支持\nlocal ip_str = \"1.2.3.4\"\n-- ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\" // IPv6\nlocal s_time = xdb.now()\nregion, err = searcher:search(ip_str)\nlocal c_time = xdb.now() - s_time\nif err ~= nil then\n    print(string.format(\"failed to search(%s): %s\", ip_str, err))\n    return\nend\n\nprint(string.format(\"{region: %s, took: %.5f μs}\", region, c_time))\n\n-- 备注：并发使用，用 xdb 整个缓存创建的查询对象可以安全的用于并发。\n-- 建议在服务启动的时候创建好全局的 searcher 对象，然后全局并发使用。\n\n-- 4，关闭 xdb 查询器\nsearcher:close()\n\n--\n-- 5，模块资源清理，仅在需要将整个服务完全关闭前调用\nxdb.cleanup()\n```\n\n\n# 查询测试\n\n通过 `search_test.lua` 脚本来进行查询测试：\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./search_test.lua \nlua search_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 进行 IPv4 查询测试：\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./search_test.lua --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, io_count: 5, took: 17μs}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, io_count: 3, took: 40μs}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 进行 IPv6 查询测试：\n```bash\n➜  lua_c git:(master) lua ./search_test.lua --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , io_count: 1, took: 48μs}\nip2region>> 240e:3b7:3276:33b0:958f:f34c:d04f:f6a\n{region: 中国|广东省|深圳市|电信|CN, io_count: 8, took: 52μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, io_count: 13, took: 35μs}\n```\n\n输入 ip 即可进行查询测试。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效率。\n\n\n# bench 测试\n\n通过 `bench_test.lua` 脚本来进行自动 bench 测试，一方面确保 `xdb` 文件没有错误，另一方面通过大量的查询测试平均查询性能：\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua \nlua bench_test.lua [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 来进行 IPv4 的 bench 测试：\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\nBench finished, {cachePolicy: vectorIndex, total: 1367686, took: 8.593 s, cost: 5.433 μs/op}\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 来进行 IPv6 的 bench 测试：\n```bash\n➜  lua_c git:(fr_lua_c_ipv6) ✗ lua ./bench_test.lua --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt                       \nBench finished, {cachePolicy: vectorIndex, total: 34159862, took: 829.008 s, cost: 23.176 μs/op}\n```\n\n可以通过设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 三种不同的缓存实现的的性能。\n@Note：请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件的相同的源文件。\n"
  },
  {
    "path": "binding/lua_c/bench_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/06/30\n\n-- set the package to load the current xdb_searcher.so\npackage.path = \"./?.lua;\" .. package.path\npackage.cpath = \"./?.so;\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\nfunction printHelp()\n    print(\"lua bench_test.lua [command options]\")\n    print(\"options: \")\n    print(\" --db string             ip2region binary xdb file path\")\n    print(\" --src string            source ip text file path\")\n    print(\" --cache-policy string   cache policy: file/vectorIndex/content\")\nend\n\nif #arg < 2 then\n    printHelp()\n    return\nend\n\n-- parser the command line args\nlocal dbFile, srcFile = \"\", \"\"\nlocal cachePolicy = \"vectorIndex\"\nfor _, r in ipairs(arg) do\n    if string.len(r) < 5 then\n        -- continue and do nothing here\n    elseif string.sub(r, 1, 2) ~= \"--\" then\n        -- continue and do nothing here\n    else\n        for k, v in string.gmatch(string.sub(r, 3), \"([^=]+)=([^%s]+)\") do\n            if k == \"db\" then\n                dbFile = v\n            elseif k == \"src\" then\n                srcFile = v\n            elseif k == \"cache-policy\" then\n                cachePolicy = v\n            else\n                print(string.format(\"undefined option `%s`\", r))\n                return\n            end\n\n            -- break the match iterate\n            break\n        end\n    end\nend\n\n-- print(string.format(\"dbFile=%s, srcFile=%s, cachePolicy=%s\", dbFile, srcFile, cachePolicy))\nif string.len(dbFile) < 2 or string.len(srcFile) < 2 then\n    printHelp()\n    return\nend\n\n-- verify the xdb from header\nif xdb.verify(dbFile) == false then\n    print(string.format(\"failed to verify the xdb file: %s\", dbFile))\n    return\nend\n\n-- detect the version from the xdb header\nheader, err = xdb.load_header(dbFile)\nif err ~= nil then\n    print(string.format(\"failed to load header: %s\", err))\n    return\nend\n\nversion, err = xdb.version_from_header(header);\nif err ~= nil then\n    print(string.format(\"failed to detect version from header: %s\", err))\n    return\nend\n\n-- create the searcher based on the cache-policy\nlocal searcher, v_index, content\nif cachePolicy == \"file\" then\n    searcher, err = xdb.new_with_file_only(version, dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to create searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"vectorIndex\" then\n    v_index, err = xdb.load_vector_index(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load vector index: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_vector_index(version, dbFile, v_index)\n    if err ~= nil then\n        print(string.format(\"failed to create vector index searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"content\" then\n    content, err = xdb.load_content(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load xdb content from '%s'\", dbFile))\n        return\n    end\n\n    searcher, err = xdb.new_with_buffer(version, content)\n    if err ~= nil then\n        print(string.format(\"failed to create content buffer searcher: %s\", err))\n        return\n    end\nelse\n    print(string.format(\"undefined cache-policy `%s`\", cachePolicy))\n    return\nend\n\n-- do the bench test\nlocal handle = io.open(srcFile, \"r\")\nif handle == nil then\n    print(string.format(\"failed to open src text file `%s`\", handle))\n    return\nend\n\nlocal lines = handle:lines()\nlocal sip_str, eip_str, s_region, region, err = \"\", \"\", \"\", \"\", 0\nlocal count, t_time, c_time = 0, 0, 0\nlocal s_time = xdb.now()\nfor l in lines do\n    if string.len(l) < 1 then\n        -- continue and do nothing here\n    else\n        for v1, v2, v3 in string.gmatch(l, \"([^|]+)|([^|]+)|([^\\n]+)\") do\n            -- print(sip_str, eip_str, region)\n            sip_str = v1\n            eip_str = v2\n            s_region = v3\n            break\n        end\n\n        t_time = xdb.now()\n        sip_bytes, err = xdb.parse_ip(sip_str)\n        if err ~= nil then\n            print(string.format(\"invalid start ip `%s`\", sip_str))\n            return\n        end\n\n        eip_bytes, err = xdb.parse_ip(eip_str)\n        if err ~= nil then\n            print(string.format(\"invalid end ip `%s`\", sip_str))\n            return\n        end\n\n        if xdb.ip_compare(sip_bytes, eip_bytes) > 0 then\n            print(string.format(\"start ip(%s) should not be greater than end ip(%s)\\n\", sip_str, eip_str))\n            return\n        end\n\n        for _, ip_bytes in ipairs({sip_bytes, eip_bytes}) do\n            region, err = searcher:search(ip_bytes)\n            if err ~= nil then\n                print(string.format(\"failed to search ip `%s`\", xdb.ip_to_string(ip_bytes)))\n                return\n            end\n\n            -- check the region\n            if region ~= s_region then\n                print(string.format(\"failed search(%s) with (%s != %s)\\n\", xdb.ip_to_string(ip_bytes), region, s_region))\n                return\n            end\n\n            count = count + 1\n        end\n\n        -- increase the time costs\n        c_time = c_time + xdb.now() - t_time\n    end\nend\n\n-- resource cleanup\nsearcher:close()\nif v_index ~= nil then\n    v_index:close()\nend\nif content ~= nil then\n    content:close()\nend\n\nxdb.cleanup()\n\n-- print the stats\nlocal avg_costs = 0\nif count > 0 then\n    avg_costs = c_time / count\nend\nprint(string.format(\"Bench finished, {cachePolicy: %s, total: %d, took: %.3f s, cost: %.3f μs/op}\",\n         cachePolicy, count, (xdb.now() - s_time)/1e6, c_time / count))"
  },
  {
    "path": "binding/lua_c/search_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/06/30\n\n-- set the package to load the current xdb_searcher.so\npackage.path = \"./?.lua;\" .. package.path\npackage.cpath = \"./?.so;\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\nfunction printHelp()\n    print(\"lua search_test.lua [command options]\")\n    print(\"options: \")\n    print(\" --db string             ip2region binary xdb file path\")\n    print(\" --cache-policy string   cache policy: file/vectorIndex/content\")\nend\n\nif #arg < 1 then\n    printHelp()\n    return\nend\n\n-- parser the command line args\nlocal dbFile = \"\"\nlocal cachePolicy = \"vectorIndex\"\nfor _, r in ipairs(arg) do\n    if string.len(r) < 5 then\n        -- continue and do nothing here\n    elseif string.sub(r, 1, 2) ~= \"--\" then\n        -- continue and do nothing here\n    else\n        for k, v in string.gmatch(string.sub(r, 3), \"([^=]+)=([^%s]+)\") do\n            if k == \"db\" then\n                dbFile = v\n            elseif k == \"cache-policy\" then\n                cachePolicy = v\n            else\n                print(string.format(\"undefined option `%s`\", r))\n                return\n            end\n\n            -- break the match iterate\n            break\n        end\n    end\nend\n\n-- print(string.format(\"dbFile=%s, cachePolicy=%s\", dbFile, cachePolicy))\nif string.len(dbFile) < 2 then\n    printHelp()\n    return\nend\n\n-- verify the xdb\nif xdb.verify(dbFile) == false then\n    print(string.format(\"failed to verify the xdb file: %s\", dbFile))\n    return\nend\n\n-- detect the version from the xdb header\nheader, err = xdb.load_header(dbFile)\nif err ~= nil then\n    print(string.format(\"failed to load header: %s\", err))\n    return\nend\n\nversion, err = xdb.version_from_header(header);\nif err ~= nil then\n    print(string.format(\"failed to detect version from header: %s\", err))\n    return\nend\n\n-- create the searcher based on the cache-policy\nlocal searcher, v_index, content\nif cachePolicy == \"file\" then\n    searcher, err = xdb.new_with_file_only(version, dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to create searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"vectorIndex\" then\n    v_index, err = xdb.load_vector_index(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load vector index: %s\", err))\n        return\n    end\n\n    searcher, err = xdb.new_with_vector_index(version, dbFile, v_index)\n    if err ~= nil then\n        print(string.format(\"failed to create vector index searcher: %s\", err))\n        return\n    end\nelseif cachePolicy == \"content\" then\n    content, err = xdb.load_content(dbFile)\n    if err ~= nil then\n        print(string.format(\"failed to load xdb content from '%s'\", dbFile))\n        return\n    end\n\n    searcher, err = xdb.new_with_buffer(version, content)\n    if err ~= nil then\n        print(string.format(\"failed to create content buffer searcher: %s\", err))\n        return\n    end\nelse\n    print(string.format(\"undefined cache-policy `%s`\", cachePolicy))\n    return\nend\n\n-- do the search\nprint(string.format([[\nip2region xdb searcher test program\nsource xdb: %s (%s, %s)\ntype 'quit' to exit]], dbFile, xdb.version_info(version).name, cachePolicy))\nlocal region, err = \"\", nil\nlocal s_time, c_time =  0, 0\nwhile ( true ) do\n    io.write(\"ip2region>> \");\n    io.input(io.stdin);\n    local line = io.read();\n    if (line == nil) then\n        break\n    end\n\n    if ( line == \"quit\" ) then\n        break\n    end\n\n    -- empty string ignore\n    line = line:gsub(\"^%s*(.-)%s*$\", \"%1\")\n    if string.len(line) < 1 then\n        -- continue and do nothing here\n    else\n        s_time = xdb.now()\n        ip_bytes, err = xdb.parse_ip(line)\n        -- print(string.format(\"parse(%s): %s, err: %s\", line, xdb.ip_to_string(ip_bytes), err))\n        if err ~= nil then\n            print(string.format(\"invalid ip address `%s`\", line))\n        else\n            -- do the search\n            region, err = searcher:search(ip_bytes)\n            c_time = xdb.now() - s_time\n            if err ~= nil then\n                print(string.format(\"{err: %s, io_count: %d}\", err, searcher:get_io_count()))\n            else\n                print(string.format(\"{region: %s, io_count: %d, took: %dμs}\", region, searcher:get_io_count(), c_time))\n            end\n        end\n    end\nend\n\n-- resource cleanup\nsearcher:close()\nif v_index ~= nil then\n    v_index:close()\nend\nif content ~= nil then\n    content:close()\nend\n\nxdb.cleanup();"
  },
  {
    "path": "binding/lua_c/util_test.lua",
    "content": "-- Copyright 2022 The Ip2Region Authors. All rights reserved.\n-- Use of this source code is governed by a Apache2.0-style\n-- license that can be found in the LICENSE file.\n--\n-- ---\n-- @Author Lion <chenxin619315@gmail.com>\n-- @Date   2022/06/30\n\n-- set the package to load the current xdb_searcher.so\npackage.path = \"./?.lua;\" .. package.path\npackage.cpath = \"./?.so;\" .. package.cpath\nlocal xdb = require(\"xdb_searcher\")\n\n---- ip checking testing\nfunction test_parse_ip()\n    local ip_list = {\n        \"1.2.3.4\", \"192.168.2.3\", \"120.24.78.129\", \"255.255.255.0\", \"invalid-ipv.4\",\n        \"::\", \"3000::\", \"240e:3b7:3276:33b0:4844:6f28:f69c:1eee\", \"2001:4:112::\", \"invalid-ipv::6\"\n    }\n\n    local s_time = xdb.now()\n    for _, ip_src in ipairs(ip_list) do\n        ip_bytes, err = xdb.parse_ip(ip_src)\n        if err ~= nil then\n            print(string.format(\"invalid ip address `%s`: %s\", ip_src, err))\n        else\n            local ip_string = xdb.ip_to_string(ip_bytes);\n            print(string.format(\"parse_ip(%s)->%s ? %s\", ip_src, ip_string, tostring(ip_src==ip_string)))\n        end\n    end\nend\n\nfunction test_print_const()\n    print(\"ipv4: \", xdb.IPv4);\n    print(\"ipv6: \", xdb.IPv6);\n    print(\"header_buffer: \", xdb.header_buffer);\n    print(\"v_index_buffer: \", xdb.v_index_buffer);\n    print(\"content_buffer: \", xdb.content_buffer);\nend\n\n---- buffer loading test\nfunction test_load_header() \n    header, err = xdb.load_header(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load header: \", err)\n    else\n        print(string.format(\"xdb header buffer `%s` loaded\", tostring(header)))\n\n        local tpl = [[\n    header: {\n        version: %d\n        index_policy: %d\n        created_at: %d\n        start_index_ptr: %d\n        end_index_ptr: %d\n        ip_version: %d\n        runtime_ptr_bytes: %d\n    }]]\n\n        local t = header:to_table()\n        print(string.format(tpl,\n            t[\"version\"], t[\"index_policy\"], t[\"created_at\"], \n            t[\"start_index_ptr\"], t[\"end_index_ptr\"], t[\"ip_version\"], t[\"runtime_ptr_bytes\"])\n        )\n    end\nend\n\nfunction test_version_info()\n    local v4 = xdb.version_info(xdb.IPv4)\n    print(string.format(\"{id:%d, name: %s, bytes: %d, segment_index_size: %d}\", v4.id, v4.name, v4.bytes, v4.segment_index_size))\n    local v6 = xdb.version_info(xdb.IPv6)\n    print(string.format(\"{id:%d, name: %s, bytes: %d, segment_index_size: %d}\", v6.id, v6.name, v6.bytes, v6.segment_index_size))\n    local vx = xdb.version_info(3)\nend\n\nfunction test_load_vector_index()\n    v_index, err = xdb.load_vector_index(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load vector index: \", err)\n    else\n        print(string.format(\"xdb vector index buffer `%s` loaded, info={name=%s, type=%d, length=%d}\",\n                tostring(v_index), v_index:name(), v_index:type(), v_index:length()))\n        v_index:close()\n    end\nend\n\n\nfunction test_load_content()\n    c_buffer, err = xdb.load_content(\"../../data/ip2region_v4.xdb\")\n    if err ~= nil then\n        print(\"failed to load content: \", err)\n    else\n        print(string.format(\"xdb content buffer `%s` loaded, info={name=%s, type=%d, length=%d}\",\n                tostring(c_buffer), c_buffer:name(), c_buffer:type(), c_buffer:length()))\n        c_buffer:close();\n    end\nend\n\n\nfunction test_search()\n    -- ipv4\n    local ip_str = \"1.2.3.4\"\n    searcher, err = xdb.new_with_file_only(xdb.IPv4, \"../../data/ip2region_v4.xdb\")\n    print(string.format(\"searcher.tostring=%s\", tostring(searcher)))\n    local t_start = xdb.now()\n    region, err = searcher:search(ip_str)\n    local c_time = xdb.now() - t_start\n    print(string.format(\"search(%s): {region=%s, io_count: %d, took: %dμs, err=%s}\",\n            ip_str, region, searcher:get_io_count(), c_time, tostring(err)))\n    searcher:close()\n\n    -- IPv6\n    ip_str = \"240e:3b7:3276:33b0:958f:f34c:d04f:f6a\"\n    searcher, err = xdb.new_with_file_only(xdb.IPv6, \"../../data/ip2region_v6.xdb\")\n    print(string.format(\"searcher.tostring=%s\", tostring(searcher)))\n    t_start = xdb.now()\n    region, err = searcher:search(ip_str)\n    c_time = xdb.now() - t_start\n    print(string.format(\"search(%s): {region=%s, io_count: %d, took: %dμs, err=%s}\",\n            ip_str, region, searcher:get_io_count(), c_time, tostring(err)))\n    searcher:close()\nend\n\nlocal func_name = arg[1]\nif func_name == nil then\n    print(\"please specified the function to test\")\n    return\nend\n\nif (_G[func_name] == nil) then\n    print(string.format(\"undefined function `%s` to call\", func_name))\n    return\nend\n\nlocal s_time = xdb.now();\nprint(string.format(\"+---calling test function %s ...\", func_name))\n_G[func_name]()\nlocal cost_time = xdb.now() - s_time\nxdb.cleanup();\nprint(string.format(\"|---done, took: %.3fμs\", cost_time))"
  },
  {
    "path": "binding/lua_c/xdb_searcher.c",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/30\n\n\n#include \"stdio.h\"\n#include \"lua.h\"\n#include \"lauxlib.h\"\n#include \"../c/xdb_api.h\"\n\n#define XDB_BUFFER_METATABLE_NAME \"xdb_buffer_mt\"\n#define XDB_METATABLE_NAME \"xdb_metatable_name\"\n\n#define xdb_header_buffer 1\n#define xdb_vector_index_buffer 2\n#define xdb_content_buffer 3\n\n\n// --- xdb buffer interface impl\n\nstruct xdb_buffer_entry {\n    int type;   // buffer type\n    char *name; // buffer name\n    void *ptr;  // buffer ptr\n    void (*closer) (void *);\n};\ntypedef struct xdb_buffer_entry xdb_buffer_t;\n\nstatic int lua_xdb_buffer_name(lua_State *L) {\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    lua_pushstring(L, buffer->name);\n    return 1;\n}\n\nstatic int lua_xdb_buffer_type(lua_State *L) {\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    lua_pushinteger(L, buffer->type);\n    return 1;\n}\n\nstatic int lua_xdb_buffer_to_table(lua_State *L) {\n    xdb_buffer_t *buffer;\n    xdb_header_t *header;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    lua_newtable(L);\n    if (buffer->type == xdb_header_buffer) {\n        header = (xdb_header_t *) buffer->ptr;\n        lua_pushinteger(L, header->version);\n        lua_setfield(L, -2, \"version\");\n\n        lua_pushinteger(L, header->index_policy);\n        lua_setfield(L, -2, \"index_policy\");\n\n        lua_pushinteger(L, header->created_at);\n        lua_setfield(L, -2, \"created_at\");\n\n        lua_pushinteger(L, header->start_index_ptr);\n        lua_setfield(L, -2, \"start_index_ptr\");\n\n        lua_pushinteger(L, header->end_index_ptr);\n        lua_setfield(L, -2, \"end_index_ptr\");\n\n        lua_pushinteger(L, header->ip_version);\n        lua_setfield(L, -2, \"ip_version\");\n\n        lua_pushinteger(L, header->runtime_ptr_bytes);\n        lua_setfield(L, -2, \"runtime_ptr_bytes\");\n    } else {\n        // do nothing for now\n    }\n\n    return 1;\n}\n\nstatic int lua_xdb_buffer_length(lua_State *L) {\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    if (buffer->type == xdb_header_buffer) {\n        lua_pushinteger(L, ((xdb_header_t *) buffer->ptr)->length);\n    } else if (buffer->type == xdb_vector_index_buffer) {\n        lua_pushinteger(L, ((xdb_vector_index_t *) buffer->ptr)->length);\n    } else if (buffer->type == xdb_content_buffer) {\n        lua_pushinteger(L, ((xdb_content_t *) buffer->ptr)->length);\n    } else {\n        lua_pushinteger(L, -1);\n    }\n\n    return 1;\n}\n\nstatic int lua_xdb_buffer_tostring(lua_State *L) {\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    lua_pushfstring(L, \"xdb %s buffer object {name: %s, type: %d}\", buffer->name, buffer->name, buffer->type);\n    return 1;\n}\n\nstatic int lua_xdb_buffer_close(lua_State *L) {\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    buffer = (xdb_buffer_t *) luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n\n    // check and call the closer\n    if (buffer->closer != NULL) {\n        buffer->closer(buffer->ptr);\n        buffer->closer = NULL;\n    }\n\n    return 0;\n}\n\n// module method define, should be access via ':'\nstatic const struct luaL_Reg xdb_buffer_methods[] = {\n    {\"name\",        lua_xdb_buffer_name},\n    {\"type\",        lua_xdb_buffer_type},\n    {\"length\",      lua_xdb_buffer_length},\n    {\"to_table\",    lua_xdb_buffer_to_table},\n    {\"close\",       lua_xdb_buffer_close},\n    {\"__gc\",        lua_xdb_buffer_close},\n    {\"__tostring\",  lua_xdb_buffer_tostring},\n    {NULL, NULL}\n};\n\n// --- End of xdb buffer\n\n\n// --- xdb util function\n\nstatic int lua_xdb_load_header_from_file(lua_State *L) {\n    const char *db_path;\n    xdb_header_t *header;\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and the xdb file path expected\");\n    db_path = luaL_checkstring(L, 1);\n    header = xdb_load_header_from_file(db_path);\n    if (header == NULL) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"load header from `%s`\", db_path);\n        return 2;\n    }\n\n    // alloc the buffer.\n    buffer = (xdb_buffer_t *) lua_newuserdata(L, sizeof(xdb_buffer_t));\n    if (buffer == NULL) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"failed to alloc xdb buffer entry\");\n        return 2;\n    }\n\n    // init the buffer\n    buffer->type = xdb_header_buffer;\n    buffer->name = \"header\";\n    buffer->ptr = header;\n    buffer->closer = xdb_free_header;\n\n    // set the metatable of the header buffer object and push onto the stack\n    luaL_getmetatable(L, XDB_BUFFER_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_load_vector_index_from_file(lua_State *L) {\n    const char *db_path;\n    xdb_vector_index_t *v_index;\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and the xdb path expected\");\n    db_path = luaL_checkstring(L, 1);\n    v_index = xdb_load_vector_index_from_file(db_path);\n    if (v_index == NULL) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"load vector index from `%s`\", db_path);\n        return 2;\n    }\n\n    // alloc the buffer.\n    buffer = (xdb_buffer_t *) lua_newuserdata(L, sizeof(xdb_buffer_t));\n    if (buffer == NULL) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"failed to alloc xdb buffer entry\");\n        return 2;\n    }\n\n    // init the buffer\n    buffer->type = xdb_vector_index_buffer;\n    buffer->name = \"v_index\";\n    buffer->ptr = v_index;\n    buffer->closer = xdb_free_vector_index;\n\n    // set the metatable of the header buffer object and push onto the stack\n    luaL_getmetatable(L, XDB_BUFFER_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_load_content_from_file(lua_State *L) {\n    const char *db_path;\n    xdb_content_t *content;\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and xdb path expected\");\n    db_path = luaL_checkstring(L, 1);\n    content = xdb_load_content_from_file(db_path);\n    if (content == NULL) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"load xdb content from `%s`\", db_path);\n        return 2;\n    }\n\n    // alloc the buffer.\n    buffer = (xdb_buffer_t *) lua_newuserdata(L, sizeof(xdb_buffer_t));\n    if (buffer == NULL) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"failed to alloc xdb buffer entry\");\n        return 2;\n    }\n\n    // init the buffer\n    buffer->type = xdb_content_buffer;\n    buffer->name = \"content\";\n    buffer->ptr = content;\n    buffer->closer = xdb_free_content;\n\n    // set the metatable of the header buffer object and push onto the stack\n    luaL_getmetatable(L, XDB_BUFFER_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_verify_from_file(lua_State *L) {\n    const char *db_path;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and the xdb file path expected\");\n    db_path = luaL_checkstring(L, 1);\n    lua_pushboolean(L, xdb_verify_from_file(db_path) == 0 ? 1 : 0);\n    return 1;\n}\n\nstatic int lua_xdb_version_from_header(lua_State *L) {\n    xdb_version_t *version;\n    xdb_buffer_t *buffer;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and xdb header expected\");\n    // header buffer checking\n    buffer = luaL_checkudata(L, 1, XDB_BUFFER_METATABLE_NAME);\n    if (buffer->type != xdb_header_buffer) {\n        return luaL_error(L, \"invalid xdb header buffer\");\n    }\n\n    version = xdb_version_from_header((xdb_header_t *) buffer->ptr);\n    if (version == NULL) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"failed to detect version from header\");\n    } else {\n        lua_pushinteger(L, version->id);\n        lua_pushnil(L);\n    }\n\n    return 2;\n}\n\nstatic xdb_version_t *_get_version(lua_State *L, int arg) {\n    int vid = luaL_checkinteger(L, arg);\n    if (vid == xdb_ipv4_id) {\n        return XDB_IPv4;\n    } else if (vid == xdb_ipv6_id) {\n        return XDB_IPv6;\n    } else {\n        return NULL;\n    }\n}\n\nstatic int lua_xdb_version_info(lua_State *L) {\n    xdb_version_t *version;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and version id expected\");\n    // check the ip version\n    version = _get_version(L, 1);\n    if (version == NULL) {\n        return luaL_error(L, \"invalid verison id specified\");\n    }\n\n    lua_newtable(L);\n    lua_pushinteger(L, version->id);\n    lua_setfield(L, -2, \"id\");\n\n    lua_pushstring(L, version->name);\n    lua_setfield(L, -2, \"name\");\n\n    lua_pushinteger(L, version->bytes);\n    lua_setfield(L, -2, \"bytes\");\n\n    lua_pushinteger(L, version->segment_index_size);\n    lua_setfield(L, -2, \"segment_index_size\");\n    return 1;\n}\n\nstatic int lua_xdb_parse_ip(lua_State *L) {\n    const char *ip_str;\n    bytes_ip_t ip_bytes[19] = {'\\0'};\n    xdb_version_t *version;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and string ip expected, eg: 1.2.3.4 / 3000::\");\n    ip_str = luaL_checkstring(L, 1);\n\n    version = xdb_parse_ip(ip_str, ip_bytes + 2, sizeof(ip_bytes) - 2);\n    if (version == NULL) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"failed to parse the `%s`\", ip_str);\n        return 2;\n    }\n\n    // append the magic char for later analysis\n    // printf(\"ip:%s, version->id: %d\\n\", ip_str, version->id);\n    ip_bytes[0] = '&';\n    ip_bytes[1] = (bytes_ip_t) version->id;\n    lua_pushlstring(L, (string_ip_t *) ip_bytes, version->bytes + 2);\n    lua_pushnil(L);\n    return 2;\n}\n\nstatic int lua_xdb_ip_to_string(lua_State *L) {\n    int err, vid, bytes;\n    const string_ip_t *ip_bytes;\n    char ip_string[INET6_ADDRSTRLEN + 1] = {'\\0'};\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via '.' and bytes ip expected\");\n    ip_bytes = luaL_checkstring(L, 1);\n    if (strlen(ip_bytes) < 2) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"invalid binary ip bytes specified\");\n        return 2;\n    }\n\n    if (ip_bytes[0] != '&') {\n        lua_pushnil(L);\n        lua_pushstring(L, \"invalid binary ip bytes specified\");\n        return 2;\n    }\n\n    vid = ip_bytes[1] & 0xFF;\n    if (vid == xdb_ipv4_id) {\n        // IPv4\n        bytes = xdb_ipv4_bytes;\n    } else if (vid == xdb_ipv6_id) {\n        // IPv6\n        bytes = xdb_ipv6_bytes;\n    } else {\n        lua_pushnil(L);\n        lua_pushstring(L, \"invalid binary ip bytes specified\");\n        return 2;\n    }\n\n    err = xdb_ip_to_string(((bytes_ip_t *) ip_bytes) + 2, bytes, ip_string, sizeof(ip_string));\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"failed to conver the ip bytes to string\");\n        return 2;\n    }\n\n    lua_pushstring(L, ip_string);\n    lua_pushnil(L);\n    return 2;\n}\n\nstatic int _validate_bytes_ip(const string_ip_t *ip_bytes) {\n    if (strlen(ip_bytes) < 2) {\n        return 1;\n    }\n\n    if (ip_bytes[0] != '&') {\n        return 2;\n    }\n\n    int vid = ip_bytes[1] & 0xFF;\n    if (vid != xdb_ipv4_id && vid != xdb_ipv6_id) {\n        return 3;\n    }\n\n    return 0;\n}\n\nstatic int lua_xdb_ip_compare(lua_State *L) {\n    int err;\n    const string_ip_t *ip1_bytes, *ip2_bytes;\n\n    luaL_argcheck(L, lua_gettop(L) == 2, 1, \"call via '.' bytes ip1 and ip2 expected\");\n    ip1_bytes = luaL_checkstring(L, 1);\n    ip2_bytes = luaL_checkstring(L, 2);\n\n    // validate the ip1\n    err = _validate_bytes_ip(ip1_bytes);\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"failed to validate ip1 with errcode=%d\", err);\n        return 2;\n    }\n\n    // validate the ip2\n    err = _validate_bytes_ip(ip2_bytes);\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"failed to validate ip2 with errcode=%d\", err);\n        return 2;\n    }\n\n    if (ip1_bytes[1] != ip2_bytes[1]) {\n        lua_pushnil(L);\n        lua_pushstring(L, \"ip version of ip1 and ip2 are not the same\");\n        return 2;\n    }\n\n    err = xdb_ip_sub_compare(((bytes_ip_t *)ip1_bytes) + 2, (ip1_bytes[1] & 0xFF), ip2_bytes, 2);\n    lua_pushinteger(L, err);\n    lua_pushnil(L);\n    return 2;\n}\n\nstatic int lua_xdb_now(lua_State *L) {\n    lua_pushinteger(L, xdb_now());\n    return 1;\n}\n\n\n// --- End of xdb util api\n\n\n// --- xdb searcher api\n\nstatic int lua_xdb_new_with_file_only(lua_State *L) {\n    int err;\n    xdb_version_t *version;\n    xdb_searcher_t *searcher;\n    const char *db_path = NULL;\n\n    luaL_argcheck(L, lua_gettop(L) == 2, 1, \"call via '.' and ip version / xdb file path expected\");\n\n    // check the ip version\n    version = _get_version(L, 1);\n    if (version == NULL) {\n        return luaL_error(L, \"invalid verison id specified\");\n    }\n\n    // check the db path\n    db_path = luaL_checkstring(L, 2);\n\n    // alloc for the searcher\n    searcher = (xdb_searcher_t *) lua_newuserdata(L, sizeof(xdb_searcher_t));\n    if (searcher == NULL) {\n        return luaL_error(L, \"failed to alloc xdb searcher entry\");\n    }\n\n    // init the xdb searcher\n    err = xdb_new_with_file_only(version, searcher, db_path);\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"init xdb searcher on `%s`: errcode=%d\", db_path, err);\n        return 2;\n    }\n\n    // push the metatable onto the stack and\n    // set it as the metatable of the current searcher\n    luaL_getmetatable(L, XDB_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_new_with_vector_index(lua_State *L) {\n    xdb_version_t *version;\n    xdb_searcher_t *searcher;\n    xdb_buffer_t *xBuffer;\n    const char *db_path;\n    int err;\n\n    luaL_argcheck(L, lua_gettop(L) == 3, 1, \"call via '.', ip version / xdb file path / vector index buffer expected\");\n\n    // check the ip version\n    version = _get_version(L, 1);\n    if (version == NULL) {\n        return luaL_error(L, \"invalid verison id specified\");\n    }\n\n    // db_path checking\n    db_path = luaL_checkstring(L, 2);\n\n    // vector index buffer checking\n    xBuffer = luaL_checkudata(L, 3, XDB_BUFFER_METATABLE_NAME);\n    if (xBuffer->type != xdb_vector_index_buffer) {\n        return luaL_error(L, \"invalid vector index buffer\");\n    }\n\n    // alloc the searcher\n    searcher = (xdb_searcher_t *) lua_newuserdata(L, sizeof(xdb_searcher_t));\n    if (searcher == NULL) {\n        return luaL_error(L, \"failed to alloc xdb searcher entry\");\n    }\n\n    // init the xdb searcher\n    err = xdb_new_with_vector_index(version, searcher, db_path, (xdb_vector_index_t *) xBuffer->ptr);\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"init vector index cached xdb searcher on `%s` with errcode=%d\", db_path, err);\n        return 2;\n    }\n\n    // push the metatable onto the stack and\n    // set it as the metatable of the current searcher\n    luaL_getmetatable(L, XDB_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_new_with_buffer(lua_State *L) {\n    xdb_version_t *version;\n    xdb_searcher_t *searcher;\n    xdb_buffer_t *xBuffer;\n    int err;\n\n    luaL_argcheck(L, lua_gettop(L) == 2, 1, \"call via '.' and ip version / xdb content buffer expected\");\n\n    // check the ip version\n    version = _get_version(L, 1);\n    if (version == NULL) {\n        return luaL_error(L, \"invalid verison id specified\");\n    }\n\n    // content buffer checking\n    xBuffer = (xdb_buffer_t *) luaL_checkudata(L, 2, XDB_BUFFER_METATABLE_NAME);\n    if (xBuffer->type != xdb_content_buffer) {\n        return luaL_error(L, \"invalid xdb content buffer\");\n    }\n\n    // alloc the searcher\n    searcher = (xdb_searcher_t *) lua_newuserdata(L, sizeof(xdb_searcher_t));\n    if (searcher == NULL) {\n        return luaL_error(L, \"failed to alloc xdb searcher entry\");\n    }\n\n    // init the xdb searcher\n    err = xdb_new_with_buffer(version, searcher, (xdb_content_t *) xBuffer->ptr);\n    if (err != 0) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"init content cached xdb searcher with errcode=%d\", err);\n        return 2;\n    }\n\n    // push the metatable onto the stack and\n    // set it as the metatable of the current searcher\n    luaL_getmetatable(L, XDB_METATABLE_NAME);\n    lua_setmetatable(L, -2);\n    lua_pushnil(L);\n\n    return 2;\n}\n\nstatic int lua_xdb_close(lua_State *L) {\n    xdb_searcher_t *searcher;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':'\");\n    searcher = (xdb_searcher_t *) luaL_checkudata(L, 1, XDB_METATABLE_NAME);\n    if (searcher == NULL) {\n        return luaL_error(L, \"broken xdb searcher instance\");\n    }\n\n    xdb_close(searcher);\n    return 0;\n}\n\nstatic int lua_xdb_search(lua_State *L) {\n    int err, vid, ip_len;\n    const char *ip_string;\n    bytes_ip_t ip_buffer[INET6_ADDRSTRLEN] = {'\\0'};\n    const bytes_ip_t *ip_bytes;\n\n    xdb_version_t *version;\n    xdb_region_buffer_t region;\n    xdb_searcher_t *searcher;\n\n    luaL_argcheck(L, lua_gettop(L) == 2, 2, \"call via ':' and string ip address expected\");\n\n    // get the searcher\n    searcher  = (xdb_searcher_t *) luaL_checkudata(L, 1, XDB_METATABLE_NAME);\n    ip_string = luaL_checkstring(L, 2);\n\n    // ip string type checking\n    if (strlen(ip_string) < 2) {\n        lua_pushnil(L);\n        lua_pushfstring(L, \"invalid ip address `%s`\", ip_string);\n        return 2;\n    }\n\n    if (ip_string[0] == '&') {\n        vid = ip_string[1] & 0xFF;\n        if (vid == xdb_ipv4_id) {\n            ip_len = xdb_ipv4_bytes;\n        } else if (vid == xdb_ipv6_id) {\n            ip_len = xdb_ipv6_bytes;\n        } else {\n            lua_pushnil(L);\n            lua_pushstring(L, \"invalid binary ip bytes specified\");\n            return 2;\n        }\n\n        ip_bytes = (bytes_ip_t *)ip_string + 2;\n        // printf(\"ip_len: %d, vid: %d\\n\", ip_len, vid);\n    } else {\n        version = xdb_parse_ip(ip_string, ip_buffer, sizeof(ip_buffer));\n        if (version == NULL) {\n            lua_pushnil(L);\n            lua_pushfstring(L, \"failed to parse string ip `%s`\", ip_string);\n            return 2;\n        }\n\n        ip_len   = version->bytes;\n        ip_bytes = ip_buffer;\n    }\n\n    // init the region buffer\n    err = xdb_region_buffer_init(&region, NULL, 0);\n    if (err != 0) {\n        return luaL_error(L, \"failed to init the region buffer with errcode=%d\", err);\n    }\n\n    // do the search\n    err = xdb_search(searcher, ip_bytes, ip_len, &region);\n    if (err != 0) {\n        lua_pushinteger(L, err);\n        lua_pushfstring(L, \"err=%d\", err);\n    } else {\n        lua_pushstring(L, region.value);\n        lua_pushnil(L);\n    }\n\n    // clean up the region buffer\n    xdb_region_buffer_free(&region);\n    return 2;\n}\n\nstatic int lua_xdb_get_io_count(lua_State *L) {\n    xdb_searcher_t *searcher;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':' or xdb searcher was broken\");\n    searcher = (xdb_searcher_t *) luaL_checkudata(L, 1, XDB_METATABLE_NAME);\n    lua_pushinteger(L, xdb_get_io_count(searcher));\n    return 1;\n}\n\nstatic int lua_xdb_tostring(lua_State *L) {\n    xdb_searcher_t *searcher;\n\n    luaL_argcheck(L, lua_gettop(L) == 1, 1, \"call via ':' or xdb searcher was broken\");\n    searcher = (xdb_searcher_t *) luaL_checkudata(L, 1, XDB_METATABLE_NAME);\n    lua_pushfstring(L, \"xdb %s searcher object\", xdb_get_version(searcher)->name);\n    return 1;\n}\n\n// cleanup the current module\nstatic int lua_xdb_cleanup(lua_State *L) {\n     xdb_clean_winsock();\n    return 0;\n}\n\n// module method define, should be access via ':'\nstatic const struct luaL_Reg xdb_searcher_methods[] = {\n    {\"search\",      lua_xdb_search},\n    {\"get_io_count\",lua_xdb_get_io_count},\n    {\"close\",       lua_xdb_close},\n    {\"__gc\",        lua_xdb_close},\n    {\"__tostring\",  lua_xdb_tostring},\n    {NULL, NULL},\n};\n\n// module function define, should be access via '.'\nstatic const struct luaL_Reg xdb_searcher_functions[] = {\n    {\"new_with_file_only\",    lua_xdb_new_with_file_only},\n    {\"new_with_vector_index\", lua_xdb_new_with_vector_index},\n    {\"new_with_buffer\",       lua_xdb_new_with_buffer},\n    {\"load_header\",           lua_xdb_load_header_from_file},\n    {\"load_vector_index\",     lua_xdb_load_vector_index_from_file},\n    {\"load_content\",          lua_xdb_load_content_from_file},\n    {\"verify\",                lua_xdb_verify_from_file},\n    {\"version_from_header\",   lua_xdb_version_from_header},\n    {\"version_info\",          lua_xdb_version_info},\n    {\"cleanup\",               lua_xdb_cleanup},\n    {\"parse_ip\",              lua_xdb_parse_ip},\n    {\"ip_to_string\",          lua_xdb_ip_to_string},\n    {\"ip_compare\",            lua_xdb_ip_compare},\n    {\"now\",                   lua_xdb_now},\n    {NULL, NULL}\n};\n\n// module register function\nint luaopen_xdb_searcher(lua_State *L)\n{\n    int err = xdb_init_winsock();\n    if (err != 0) {\n        luaL_error(L, \"failed to init the winsock with errno=%d\\n\", err);\n        return 1;\n    }\n    \n    // create a metatable for xdb buffer object\n    luaL_newmetatable(L, XDB_BUFFER_METATABLE_NAME);\n    lua_pushvalue(L, -1);\n    lua_setfield(L, -2, \"__index\");\n#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501\n    // lua 5.1\n    luaL_register(L, NULL, xdb_buffer_methods);\n#elif defined(LUA_VERSION_NUM) && LUA_VERSION_NUM >= 502\n    // lua version 5.2, 5.3, 5.4 ...\n    luaL_setfuncs(L, xdb_buffer_methods, 0);\n#endif\n\n    // create a metatable for xdb searcher object\n    luaL_newmetatable(L, XDB_METATABLE_NAME);\n    lua_pushvalue(L, -1);\n    lua_setfield(L, -2, \"__index\");\n#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501\n    // lua 5.1\n    luaL_register(L, NULL, xdb_searcher_methods);\n    luaL_register(L, NULL, xdb_searcher_functions);\n#elif defined(LUA_VERSION_NUM) && LUA_VERSION_NUM >= 502\n    // lua version 5.2, 5.3, 5.4 ...\n    luaL_setfuncs(L, xdb_searcher_methods, 0);\n    luaL_setfuncs(L, xdb_searcher_functions, 0);\n#endif\n\n    // register the constants attributes\n    lua_pushinteger(L, xdb_ipv4_id);\n    lua_setfield(L, -2, \"IPv4\");\n    lua_pushinteger(L, xdb_ipv6_id);\n    lua_setfield(L, -2, \"IPv6\");\n    lua_pushinteger(L, xdb_header_buffer);\n    lua_setfield(L, -2, \"header_buffer\");\n    lua_pushinteger(L, xdb_vector_index_buffer);\n    lua_setfield(L, -2, \"v_index_buffer\");\n    lua_pushinteger(L, xdb_content_buffer);\n    lua_setfield(L, -2, \"content_buffer\");\n\n    return 1;\n}"
  },
  {
    "path": "binding/nginx/Dockerfile",
    "content": "ARG NGINX_VERSION=1.29.6\nFROM nginx:${NGINX_VERSION} AS build\nARG NGINX_VERSION\n\n# prepare the build environment\nRUN apt-get update && \\\n    apt-get install -y build-essential libpcre2-dev zlib1g-dev libssl-dev git\n\nWORKDIR /usr/src\nRUN curl -LO https://github.com/nginx/nginx/releases/download/release-${NGINX_VERSION}/nginx-${NGINX_VERSION}.tar.gz && \\\n    tar -zxf nginx-$NGINX_VERSION.tar.gz\n\nCOPY . /usr/src/ip2region\n\nWORKDIR /usr/src/ip2region/binding/c\nRUN make xdb_searcher_lib\n\n# parameters for building dynamic modules\nWORKDIR /usr/src\nRUN nginx -V 2>&1 | grep 'configure arguments' | sed 's/ --/ \\\\\\n  --/g' | sed \"s/pie'/pie' \\\\\\/g\" | grep -v 'configure arguments' >> /tmp/conf_arg\n\nRUN echo \\\n    '  --add-dynamic-module=$(pwd)/../ip2region/binding/nginx \\\\\\n' \\\n    ' --with-cc-opt=\"-I $(pwd)/../ip2region/binding/c/build/include\" \\\\\\n' \\\n    ' --with-ld-opt=\"-L $(pwd)/../ip2region/binding/c/build/lib\"' >> /tmp/conf_arg\nRUN cat  /tmp/conf_arg\n\nWORKDIR /usr/src/nginx-$NGINX_VERSION\nRUN eval \"./configure $(cat /tmp/conf_arg)\"\n\nRUN make modules && \\\n    cp objs/ngx_http_ip2region_module.so /etc/nginx/modules\n\n# for buildx export\nFROM scratch AS export_so\nARG NGINX_VERSION\nCOPY --from=build /etc/nginx/modules/ngx_http_ip2region_module.so /ngx_http_ip2region_module.so\n"
  },
  {
    "path": "binding/nginx/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# nginx-ip2region\n\n## build\n\n```shell\n$ mkdir -p workspace\n$ cd workspace\n$ wget https://nginx.org/download/nginx-1.23.6.tar.gz\n$ tar -zxf nginx-1.23.6.tar.gz && rm -rf nginx-1.23.6.tar.gz\n$ git clone https://github.com/lionsoul2014/ip2region.git\n$ cd ip2region/binding/c\n$ make xdb_searcher_lib\n$ cd ../../../nginx-1.23.6\n$ ./configure                                                            \\\n    --add-module=$(PWD)/../ip2region/binding/nginx                       \\\n    --with-cc-opt=\"-I $(PWD)/../ip2region/binding/c/build/include\"       \\\n    --with-ld-opt=\"-L $(PWD)/../ip2region/binding/c/build/lib\"\n$ make\n$ make install\n```\n\n## nginx conf\n\n> Syntax:  `ip2region_db xdb_file_path [cache_policy Optional]`;\n> Context: http\n\ncache_policy: `file` or `vectorIndex` or `content`, default: `content`\n\nEdit `nginx.conf` add `ip2region_db` directive\n\n```nginx\n...\nhttp {\n\n    log_format main escape=json '{'\n                                '\"remote_addr\": \"$remote_addr\", '\n                                '\"region\": \"$ip2region\", '\n                                '\"http_x_forwarded_for\": \"$http_x_forwarded_for\"'\n                                '}';\n\n    access_log logs/access.log main;\n\n    # set xdb file path\n    ip2region_db ip2region.xdb;\n    # ip2region_db ip2region.xdb vectorIndex;\n    # ip2region_db ip2region.xdb file;\n    # ip2region_db ip2region.xdb content;\n\n    server {\n        listen 80;\n        server_name localhost;\n\n        location / {\n            root html;\n            index index.html index.htm;\n        }\n    }\n}\n\n```\n\nCopy `ip2region_v4.xdb` to `nginx/config` folder (rename name it to ip2region.xdb), then restart nginx, the `region` data stored in `ip2region` variable\n\nnginx access log sample\n\n```log\n{\"remote_addr\": \"127.0.0.1\", \"region\": \"Reserved|Reserved|Reserved|0|0\", \"http_x_forwarded_for\": \"\"}\n{\"remote_addr\": \"127.0.0.1\", \"region\": \"Reserved|Reserved|Reserved|0|0\", \"http_x_forwarded_for\": \"\"}\n```\n\nAdditionally, you can build the nginx dynamic module using the Dockerfile in the current directory.\n\n> * The [buildx](https://github.com/docker/buildx) plugin is required to enable export functionality.\n\n```shell\n\ndocker build -t export_so -o type=tar,dest=./so.tar .\n# The final result is a dynamic module named ngx_http_ip2region_module.so.\ntar xf so.tar && rm so.tar\n\n```\n\nusage of dynamic modules\n\n```\n\n# nginx.conf\nload_module /etc/nginx/my-modules/ngx_http_ip2region_module.so;\n\nhttp {\n    # ...\n    ip2region_db /etc/nginx/conf.d/ip2region_v4.xdb content;\n    ip2region_db6 /etc/nginx/conf.d/ip2region_v6.xdb content;\n    # ...\n}\n```\n\n"
  },
  {
    "path": "binding/nginx/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# nginx-ip2region\n\n## build\n\n```shell\n$ mkdir -p workspace\n$ cd workspace\n$ wget https://nginx.org/download/nginx-1.23.6.tar.gz\n$ tar -zxf nginx-1.23.6.tar.gz && rm -rf nginx-1.23.6.tar.gz\n$ git clone https://github.com/lionsoul2014/ip2region.git\n$ cd ip2region/binding/c\n$ make xdb_searcher_lib\n$ cd ../../../nginx-1.23.6\n$ ./configure                                                            \\\n    --add-module=$(PWD)/../ip2region/binding/nginx                       \\\n    --with-cc-opt=\"-I $(PWD)/../ip2region/binding/c/build/include\"       \\\n    --with-ld-opt=\"-L$(PWD)/../ip2region/binding/c/build/lib\"\n$ make\n$ make install\n```\n\n## nginx conf\n\n> Syntax:  `ip2region_db xdb_file_path [cache_policy Optional]`;\n> Context: http\n\ncache_policy: `file` or `vectorIndex` or `content`, default: `content`\n\nEdit `nginx.conf` add `ip2region_db` directive\n\n```nginx\n...\nhttp {\n\n    log_format main escape=json '{'\n                                '\"remote_addr\": \"$remote_addr\", '\n                                '\"region\": \"$ip2region\", '\n                                '\"http_x_forwarded_for\": \"$http_x_forwarded_for\"'\n                                '}';\n\n    access_log logs/access.log main;\n\n    # set xdb file path\n    ip2region_db ip2region.xdb;\n    # ip2region_db ip2region.xdb vectorIndex;\n    # ip2region_db ip2region.xdb file;\n    # ip2region_db ip2region.xdb content;\n\n    server {\n        listen 80;\n        server_name localhost;\n\n        location / {\n            root html;\n            index index.html index.htm;\n        }\n    }\n}\n\n```\n\nCopy `ip2region_v4.xdb` to `nginx/config` folder (rename name it to ip2region.xdb), then restart nginx, the `region` data stored in `ip2region` variable\n\nnginx access log sample\n\n```log\n{\"remote_addr\": \"127.0.0.1\", \"region\": \"Reserved|Reserved|Reserved|0|0\", \"http_x_forwarded_for\": \"\"}\n{\"remote_addr\": \"127.0.0.1\", \"region\": \"Reserved|Reserved|Reserved|0|0\", \"http_x_forwarded_for\": \"\"}\n```\n\n另外，也可以使用当前路径的 Dockerfile 构建 nginx 动态模块\n\n> *需要安装 [buildx](https://github.com/docker/buildx) 插件以支持导出*\n\n```shell\n\ndocker build -t export_so -o type=tar,dest=./so.tar .\n# 最终得到一个叫 ngx_http_ip2region_module.so 的动态模块\ntar xf so.tar && rm so.tar\n\n```\n\n动态模块使用方式\n\n```\n\n# nginx.conf\nload_module /etc/nginx/my-modules/ngx_http_ip2region_module.so;\n\nhttp {\n    # ...\n    ip2region_db /etc/nginx/conf.d/ip2region_v4.xdb content;\n    ip2region_db6 /etc/nginx/conf.d/ip2region_v6.xdb content;\n    # ...\n}\n```\n\n"
  },
  {
    "path": "binding/nginx/config",
    "content": "ngx_addon_name=ngx_http_ip2region_module\n\nNGX_HTTP_IP2REGION_SRCS=\"                                                           \\\n                $ngx_addon_dir/src/ngx_http_ip2region_module.c                      \\\n                \"\n\nNGX_HTTP_IP2REGION_DEPS=\"                                                           \\\n                \"\n\nif test -n \"$ngx_module_link\"; then\n    ngx_module_type=HTTP\n    ngx_module_name=$ngx_addon_name\n    ngx_module_deps=\"$NGX_HTTP_IP2REGION_DEPS\"\n    ngx_module_srcs=\"$NGX_HTTP_IP2REGION_SRCS\"\n    ngx_module_libs=\"-lxdb\"\n\n    . auto/module\nelse\n    HTTP_MODULES=\"$HTTP_MODULES $ngx_addon_name\"\n    NGX_ADDON_DEPS=\"$NGX_ADDON_DEPS $NGX_HTTP_IP2REGION_DEPS\"\n    NGX_ADDON_SRCS=\"$NGX_ADDON_SRCS $NGX_HTTP_IP2REGION_SRCS\"\nfi"
  },
  {
    "path": "binding/nginx/src/ngx_http_ip2region_module.c",
    "content": "/*\n * Created by Wu Jian Ping on - 2023/03/30.\n */\n\n#include \"ngx_http_ip2region_module.h\"\n\nstatic ngx_int_t ngx_http_ip2region_is_absolute_path(char *name);\nstatic char *ngx_http_ip2region_init_searcher(ngx_conf_t *cf, char *db_name,\n    char *cache_policy, xdb_version_t *expected_version, const char *directive_name);\n\nstatic ngx_int_t ngx_http_ip2region_add_variables(ngx_conf_t *cf);\n\nstatic void *ngx_http_ip2region_create_conf(ngx_conf_t *cf);\nstatic void ngx_http_ip2region_cleanup(void *data);\nstatic char *ngx_http_ip2region_init(ngx_conf_t *cf,\n    ngx_command_t *cmd, void *conf);\n\nstatic ngx_int_t ngx_http_ip2region_variable(ngx_http_request_t *r,\n    ngx_http_variable_value_t *v, uintptr_t data);\n\nstatic ngx_http_module_t ngx_http_ip2region_ctx = {\n    ngx_http_ip2region_add_variables,                                          /* pre configuration */\n    NULL,                                                                      /* post configuration */\n    ngx_http_ip2region_create_conf,                                            /* create main configuration */\n    NULL,                                                                      /* init main configuration */\n    NULL,                                                                      /* create server configuration */\n    NULL,                                                                      /* merge server configuration */\n    NULL,                                                                      /* create location configuration */\n    NULL                                                                       /* merge location configuration */\n};\n\n\nstatic char *ngx_http_ip2region_init(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);\nstatic char *ngx_http_ip2region_init_v6(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);\n\nstatic ngx_command_t ngx_http_ip2region_commands[] = {\n    { ngx_string(\"ip2region_db\"),\n      NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE12,\n      ngx_http_ip2region_init,\n      NGX_HTTP_MAIN_CONF_OFFSET,\n      0,\n      NULL },\n\n    { ngx_string(\"ip2region_db6\"),\n      NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE12,\n      ngx_http_ip2region_init_v6,\n      NGX_HTTP_MAIN_CONF_OFFSET,\n      0,\n      NULL },\n\n    ngx_null_command\n};\n\n\n/* ngx_module_t is required, otherwise failed at complie time */\nngx_module_t ngx_http_ip2region_module = {\n    NGX_MODULE_V1,\n    &ngx_http_ip2region_ctx,                                                   /* module context */\n    ngx_http_ip2region_commands,                                               /* module directives */\n    NGX_HTTP_MODULE,                                                           /* module type */\n    NULL,                                                                      /* init master */\n    NULL,                                                                      /* init module */\n    NULL,                                                                      /* init process */\n    NULL,                                                                      /* init thread */\n    NULL,                                                                      /* exit thread */\n    NULL,                                                                      /* exit process */\n    NULL,                                                                      /* exit master */\n    NGX_MODULE_V1_PADDING\n};\n\n\nstatic ngx_http_variable_t ngx_http_ip2region_vars[] = {\n    { ngx_string(\"ip2region\"), NULL,\n      ngx_http_ip2region_variable,\n      0, 0, 0 },\n\n    ngx_http_null_variable\n};\n\n\n// 用于初始化带版本校验的搜索器的辅助函数\nstatic char *\nngx_http_ip2region_init_searcher(ngx_conf_t *cf, char *db_name,\n    char *cache_policy, xdb_version_t *expected_version, const char *directive_name)\n{\n    ip2region_searcher_t       *ip2region_searcher;\n    int                         err;\n    char                       *db_path;\n    size_t                      len;\n\n    if(ngx_http_ip2region_is_absolute_path(db_name) == NGX_OK) {\n        db_path = db_name;\n    } else { // relative path to conf directory\n        len = ngx_cycle->conf_prefix.len + strlen(db_name) + 1;\n        db_path = malloc(len);\n        if (db_path == NULL) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to allocate memory for db_path\");\n            return NGX_CONF_ERROR;\n        }\n        memset(db_path, '\\0', len);\n        memcpy(db_path, ngx_cycle->conf_prefix.data, ngx_cycle->conf_prefix.len);\n        strcat(db_path, db_name);\n    }\n\n    ip2region_searcher = ngx_palloc(cf->pool, sizeof(ip2region_searcher_t));\n\n    if(ip2region_searcher == NULL) {\n        if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n            free(db_path);\n        }\n        return NGX_CONF_ERROR;\n    }\n\n    ip2region_searcher->v_index = NULL;\n    ip2region_searcher->c_buffer = NULL;\n\n    // 检查XDB文件的版本信息以确定IP类型\n    xdb_header_t *header = xdb_load_header_from_file(db_path);\n    if (header == NULL) {\n        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                           \"failed to load xdb header from: %s\", db_path);\n        if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n            free(db_path);\n        }\n        return NGX_CONF_ERROR;\n    }\n\n    xdb_version_t *xdb_version = xdb_version_from_header(header);\n    if (xdb_version == NULL) {\n        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                           \"failed to determine xdb version from header: %s\", db_path);\n        xdb_free_header((void *)header);\n        if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n            free(db_path);\n        }\n        return NGX_CONF_ERROR;\n    }\n\n    // 验证XDB文件版本是否匹配\n    if (xdb_version->id != expected_version->id) {\n        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                           \"%s expects %s xdb file, but got %s: %s\",\n                           directive_name, expected_version->name, xdb_version->name, db_path);\n        xdb_free_header((void *)header);\n        if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n            free(db_path);\n        }\n        return NGX_CONF_ERROR;\n    }\n\n    if (strcmp(cache_policy, \"file\") == 0) {\n        err = xdb_new_with_file_only(xdb_version, &ip2region_searcher->searcher, db_path);\n        xdb_free_header((void *)header);\n        if (err != 0) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to create searcher: %s\", db_path);\n            if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n                free(db_path);\n            }\n            return NGX_CONF_ERROR;\n        }\n    } else if (strcmp(cache_policy, \"vectorIndex\") == 0) {\n        ip2region_searcher->v_index = xdb_load_vector_index_from_file(db_path);\n        if (ip2region_searcher->v_index == NULL) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to load vector index from: %s\", db_path);\n            xdb_free_header((void *)header);\n            if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n                free(db_path);\n            }\n            return NGX_CONF_ERROR;\n        }\n\n        err = xdb_new_with_vector_index(xdb_version, &ip2region_searcher->searcher, db_path, ip2region_searcher->v_index);\n        xdb_free_header((void *)header);\n        if (err != 0) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to create vector index cached searcher: %s\", db_path);\n            xdb_free_vector_index((void *)ip2region_searcher->v_index);\n            if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n                free(db_path);\n            }\n            return NGX_CONF_ERROR;\n        }\n    } else if (strcmp(cache_policy, \"content\") == 0) {\n        ip2region_searcher->c_buffer = xdb_load_content_from_file(db_path);\n        if (ip2region_searcher->c_buffer == NULL) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to load xdb content: %s\", db_path);\n            xdb_free_header((void *)header);\n            if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n                free(db_path);\n            }\n            return NGX_CONF_ERROR;\n        }\n\n        err = xdb_new_with_buffer(xdb_version, &ip2region_searcher->searcher, ip2region_searcher->c_buffer);\n        xdb_free_header((void *)header);\n        if (err != 0) {\n            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                               \"failed to create content cached searcher: %s\", db_path);\n            xdb_free_content((void *)ip2region_searcher->c_buffer);\n            if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n                free(db_path);\n            }\n            return NGX_CONF_ERROR;\n        }\n    } else {\n        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,\n                           \"invalid cache policy, options: file/vectorIndex/content\");\n        xdb_free_header((void *)header);\n        if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n            free(db_path);\n        }\n        return NGX_CONF_ERROR;\n    }\n\n    if(ngx_http_ip2region_is_absolute_path(db_name) != NGX_OK) {\n        free(db_path);\n    }\n\n    return (char *)ip2region_searcher;\n}\n\n\nstatic char *\nngx_http_ip2region_init(ngx_conf_t *cf, ngx_command_t *cmd,\n    void *conf)\n{\n    ngx_http_ip2region_conf_t  *ip2region_cf;\n    char                       *db_name, *cache_policy;\n    ngx_str_t                  *value;\n    char                       *result;\n\n    ip2region_cf = conf;\n\n    if (ip2region_cf->v4_searcher) {\n        return \"ip2region_db is duplicate\";\n    }\n\n    value = cf->args->elts;\n    db_name = (char *)value[1].data;\n\n    // default cache_policy: content\n    if(cf->args->nelts == 2) {\n        cache_policy = \"content\";\n    } else {\n        cache_policy = (char *)value[2].data;\n    }\n\n    result = ngx_http_ip2region_init_searcher(cf, db_name, cache_policy,\n                                               xdb_version_v4(), \"ip2region_db\");\n    if (result == NGX_CONF_ERROR) {\n        return NGX_CONF_ERROR;\n    }\n\n    ip2region_cf->v4_searcher = (ip2region_searcher_t *)result;\n\n    return NGX_CONF_OK;\n}\n\nstatic char *\nngx_http_ip2region_init_v6(ngx_conf_t *cf, ngx_command_t *cmd,\n    void *conf)\n{\n    ngx_http_ip2region_conf_t  *ip2region_cf;\n    char                       *db_name, *cache_policy;\n    ngx_str_t                  *value;\n    char                       *result;\n\n    ip2region_cf = conf;\n\n    if (ip2region_cf->v6_searcher) {\n        return \"ip2region_db6 is duplicate\";\n    }\n\n    value = cf->args->elts;\n    db_name = (char *)value[1].data;\n\n    // default cache_policy: content\n    if(cf->args->nelts == 2) {\n        cache_policy = \"content\";\n    } else {\n        cache_policy = (char *)value[2].data;\n    }\n\n    result = ngx_http_ip2region_init_searcher(cf, db_name, cache_policy,\n                                               xdb_version_v6(), \"ip2region_db6\");\n    if (result == NGX_CONF_ERROR) {\n        return NGX_CONF_ERROR;\n    }\n\n    ip2region_cf->v6_searcher = (ip2region_searcher_t *)result;\n\n    return NGX_CONF_OK;\n}\n\n\nstatic void *\nngx_http_ip2region_create_conf(ngx_conf_t *cf)\n{\n    ngx_pool_cleanup_t         *cln;\n    ngx_http_ip2region_conf_t  *conf;\n\n    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_ip2region_conf_t));\n    if (conf == NULL) {\n        return NULL;\n    }\n\n    cln = ngx_pool_cleanup_add(cf->pool, 0);\n    if (cln == NULL) {\n        return NULL;\n    }\n\n    cln->handler = ngx_http_ip2region_cleanup;\n    cln->data = conf;\n\n    return conf;\n}\n\n\nstatic ngx_int_t\nngx_http_ip2region_add_variables(ngx_conf_t *cf)\n{\n    ngx_http_variable_t  *var;\n    ngx_http_variable_t  *v;\n\n    for (v = ngx_http_ip2region_vars; v->name.len; v++) {\n        var = ngx_http_add_variable(cf, &v->name, v->flags);\n        if (var == NULL) {\n            return NGX_ERROR;\n        }\n\n        var->get_handler = v->get_handler;\n        var->data = v->data;\n    }\n    return NGX_OK;\n}\n\n\nstatic ngx_int_t\nngx_http_ip2region_variable(ngx_http_request_t *r,\n    ngx_http_variable_value_t *v, uintptr_t data)\n{\n    ngx_http_ip2region_conf_t  *ip2region_conf;\n    struct sockaddr_in         *sin;\n    char                        region_buffer[512] = {'\\0'};\n    xdb_region_buffer_t         region;\n    int                         err = 1;\n    unsigned int                ip;\n    xdb_searcher_t             *searcher_ptr = NULL;\n\n#if (NGX_HAVE_INET6)\n    u_char                      *p;\n    in_addr_t                    addr;\n    struct sockaddr_in6         *sin6;\n#endif\n\n    ip2region_conf = ngx_http_get_module_main_conf(r, ngx_http_ip2region_module);\n\n    if (ip2region_conf->v4_searcher == NULL && ip2region_conf->v6_searcher == NULL) {\n        v->not_found = 1;\n        return NGX_OK;\n    }\n\n    // 初始化 region buffer\n    err = xdb_region_buffer_init(&region, region_buffer, sizeof(region_buffer));\n    if (err != 0) {\n        v->not_found = 1;\n        return NGX_OK;\n    }\n\n    switch (r->connection->sockaddr->sa_family) {\n        case AF_INET:\n            sin = (struct sockaddr_in *) r->connection->sockaddr;\n\n            // 检查是否有IPv4 searcher\n            if (ip2region_conf->v4_searcher == NULL) {\n                ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,\n                              \"no IPv4 searcher available for IPv4 address\");\n                break;\n            }\n\n            searcher_ptr = &ip2region_conf->v4_searcher->searcher;\n\n            // 正确转换IP地址字节序，按ip2region期望的格式\n            ip = ntohl(sin->sin_addr.s_addr); // 将网络字节序转换为主机字节序\n            // 按照xdb_parse_v4_ip中的格式重新组织字节\n            {\n                bytes_ip_t ip_bytes[4];\n                ip_bytes[0] = (ip >> 24) & 0xFF;\n                ip_bytes[1] = (ip >> 16) & 0xFF;\n                ip_bytes[2] = (ip >> 8) & 0xFF;\n                ip_bytes[3] = ip & 0xFF;\n                err = xdb_search(searcher_ptr, ip_bytes, 4, &region);\n            }\n            if (err == 0) {\n                v->data = (unsigned char *)region.value;\n                v->len = strlen(region.value);\n                xdb_region_buffer_free(&region);\n                return NGX_OK;\n            } else {\n                ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,\n                              \"ip2region search failed for IPv4 address\");\n            }\n            break;\n\n#if (NGX_HAVE_INET6)\n\n        case AF_INET6:\n            sin6 = (struct sockaddr_in6 *) r->connection->sockaddr;\n            p = sin6->sin6_addr.s6_addr;\n\n            if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) {\n                // 处理IPv4映射的IPv6地址\n                if (ip2region_conf->v4_searcher != NULL) {\n                    searcher_ptr = &ip2region_conf->v4_searcher->searcher;\n                    addr = p[12] << 24;\n                    addr += p[13] << 16;\n                    addr += p[14] << 8;\n                    addr += p[15];\n                    // 按照xdb_parse_v4_ip中的格式重新组织字节\n                    {\n                        bytes_ip_t ip_bytes[4];\n                        ip_bytes[0] = (addr >> 24) & 0xFF;\n                        ip_bytes[1] = (addr >> 16) & 0xFF;\n                        ip_bytes[2] = (addr >> 8) & 0xFF;\n                        ip_bytes[3] = addr & 0xFF;\n                        err = xdb_search(searcher_ptr, ip_bytes, 4, &region);\n                    }\n                    if (err == 0) {\n                        v->data = (unsigned char *)region.value;\n                        v->len = strlen(region.value);\n                        xdb_region_buffer_free(&region);\n                        return NGX_OK;\n                    } else {\n                        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,\n                                      \"ip2region search failed for IPv4-mapped IPv6 address\");\n                    }\n                }\n            } else {\n                // 处理纯IPv6地址\n                if (ip2region_conf->v6_searcher != NULL) {\n                    searcher_ptr = &ip2region_conf->v6_searcher->searcher;\n                    bytes_ip_t ip6_bytes[16];\n                    if (p != NULL) {\n                        memcpy(ip6_bytes, p, 16);\n                        err = xdb_search(searcher_ptr, ip6_bytes, 16, &region);\n                        if (err == 0) {\n                            v->data = (unsigned char *)region.value;\n                            v->len = strlen(region.value);\n                            xdb_region_buffer_free(&region);\n                            return NGX_OK;\n                        } else {\n                            ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,\n                                          \"ip2region search failed for IPv6 address\");\n                        }\n                    }\n                } else {\n                    ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,\n                                  \"no IPv6 searcher available for IPv6 address\");\n                }\n            }\n            break;\n\n#endif\n\n    }\n    // 如果搜索失败，释放 region buffer\n    xdb_region_buffer_free(&region);\n\n    ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,\n                  \"ip2region: no region found for IP address\");\n    v->not_found = 1;\n    return NGX_OK;\n}\n\n\nstatic void\nngx_http_ip2region_cleanup(void *data)\n{\n    ngx_http_ip2region_conf_t *ip2region_conf = data;\n\n    // 清理 IPv4 searcher\n    if(ip2region_conf->v4_searcher != NULL) {\n        xdb_close(&ip2region_conf->v4_searcher->searcher);\n\n        // check and free the vector index\n        if (ip2region_conf->v4_searcher->v_index != NULL) {\n            xdb_free_vector_index((void *)ip2region_conf->v4_searcher->v_index);\n            ip2region_conf->v4_searcher->v_index = NULL;\n        }\n\n        // check and free the content buffer\n        if (ip2region_conf->v4_searcher->c_buffer != NULL) {\n            xdb_free_content((void *)ip2region_conf->v4_searcher->c_buffer);\n            ip2region_conf->v4_searcher->c_buffer = NULL;\n        }\n\n        ip2region_conf->v4_searcher = NULL;\n    }\n\n    // 清理 IPv6 searcher\n    if(ip2region_conf->v6_searcher != NULL) {\n        xdb_close(&ip2region_conf->v6_searcher->searcher);\n\n        // check and free the vector index\n        if (ip2region_conf->v6_searcher->v_index != NULL) {\n            xdb_free_vector_index((void *)ip2region_conf->v6_searcher->v_index);\n            ip2region_conf->v6_searcher->v_index = NULL;\n        }\n\n        // check and free the content buffer\n        if (ip2region_conf->v6_searcher->c_buffer != NULL) {\n            xdb_free_content((void *)ip2region_conf->v6_searcher->c_buffer);\n            ip2region_conf->v6_searcher->c_buffer = NULL;\n        }\n\n        ip2region_conf->v6_searcher = NULL;\n    }\n}\n\n\nstatic ngx_int_t\nngx_http_ip2region_is_absolute_path(char *name)\n{\n#if (NGX_WIN32)\n    u_char  c0, c1;\n\n    c0 = name[0];\n\n    if (strlen(name) < 2) {\n        if (c0 == '/') {\n            return 2;\n        }\n\n        return NGX_DECLINED;\n    }\n\n    c1 = name[1];\n\n    if (c1 == ':') {\n        c0 |= 0x20;\n\n        if ((c0 >= 'a' && c0 <= 'z')) {\n            return NGX_OK;\n        }\n\n        return NGX_DECLINED;\n    }\n\n    if (c1 == '/') {\n        return NGX_OK;\n    }\n\n    if (c0 == '/') {\n        return 2;\n    }\n\n    return NGX_DECLINED;\n\n#else\n\n    if (name[0] == '/') {\n        return NGX_OK;\n    }\n\n    return NGX_DECLINED;\n\n#endif\n}\n"
  },
  {
    "path": "binding/nginx/src/ngx_http_ip2region_module.h",
    "content": "/*\n * Created by Wu Jian Ping on - 2023/03/30.\n */\n\n#ifndef __NGX_HTTP_IP2REGION_MODULE_H_INCLUDED__\n#define __NGX_HTTP_IP2REGION_MODULE_H_INCLUDED__\n\n#include <ngx_config.h>\n#include <ngx_core.h>\n#include <ngx_http.h>\n#include <xdb_api.h>\n\n#if (NGX_HAVE_INET6)\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <arpa/inet.h>\n#endif\n\ntypedef struct {\n    xdb_searcher_t       searcher;\n    xdb_vector_index_t  *v_index;\n    xdb_content_t       *c_buffer;\n} ip2region_searcher_t;\n\ntypedef struct {\n    ip2region_searcher_t *v4_searcher;  // IPv4 searcher for ip2region_db\n    ip2region_searcher_t *v6_searcher;  // IPv6 searcher for ip2region_db6\n} ngx_http_ip2region_conf_t;\n\n#endif\n"
  },
  {
    "path": "binding/nginx/t/http_ip2region.t",
    "content": "use lib 'lib';\nuse Test::Nginx::Socket; # 'no_plan';\n\nrepeat_each(2);\n\nplan tests => repeat_each() * 124;\n\nno_long_string();\n#no_diff;\n\nrun_tests();\n\n__DATA__\n\n=== TEST 1: set request header at client side\n--- config\n    location /foo {\n        echo $http_x_foo;\n    }\n--- request\n    GET /foo\n--- more_headers\nX-Foo: blah\n--- response_headers\n! X-Foo\n--- response_body\nblah\n"
  },
  {
    "path": "binding/nodejs/README.md",
    "content": "# :cn: [中文简体]\n\n# ip2region nodejs 查询客户端\n\n请使用最新的 IPv6 兼容的 javascript binding：[javascript binding](../javascript/)\n\n---\n\n# :globe_with_meridians: [English]\n\n# ip2region nodejs query client\n\nPlease use the latest IPv6-compliant JavaScript binding: [javascript binding](../javascript/)\n"
  },
  {
    "path": "binding/php/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region PHP Query Client\n\n# Usage\n\n### About Query API\n\nThe prototype of the Query API is as follows:\n\n```php\n// Query via string IP\n// @throw Exception\nsearch($ip_string) string\n\n// Query via binary IP returned by Util.parseIP\n// @throw Exception\nsearchByBytes($ip_bytes) string\n```\n\nIf the query fails, an exception will be thrown; if the query is successful, the `region` information string will be returned; if the IP being queried cannot be found, an empty string `\"\"` will be returned.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. The usage is as follows:\n\n```php\nuse \\ip2region\\xdb\\{IPv4, IPv6};\n\n// For IPv4: Set xdb path to the v4 xdb file, specify IP version as IPv4\n$dbFile  = \"../../data/ip2region_v4.xdb\";  // or your ipv4 xdb path\n$version = IPv4::default();\n\n// For IPv6: Set xdb path to the v6 xdb file, specify IP version as IPv6\n$dbFile  = \"../../data/ip2region_v6.xdb\";  // or your ipv6 xdb path\n$version = IPv6::default();\n\n// The IP version of the xdb specified by dbPath must be consistent with the version specified, otherwise an error will occur during query execution\n// Note: The following demonstration directly uses $dbFile and $version variables\n```\n\n### XDB File Verification\n\nIt is recommended that you proactively verify the applicability of the xdb file, as some new features in the future may cause the current Searcher version to be incompatible with the xdb file you are using. Verification can avoid unpredictable errors during runtime. You do not need to verify every time; for example, verify when the service starts or manually call the command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```php\nuse \\ip2region\\xdb\\Util;\n\n$err = Util::verify($dbFile);\nif ($err != null) {\n    // Applicability verification failed!!!\n    // The current query client implementation is not suitable for querying the xdb file specified by dbPath.\n    // You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation compatible with dbPath.\n    printf(\"failed to verify xdb file `%s`: %s\\n\", $dbFile, $err);\n    return;\n}\n\n// Verification passed, the current Searcher can be safely used for query operations on the xdb pointed to by dbPath\n```\n\n### File-Based Query\n\n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n// 1. Create a Searcher object using the $version and $dbFile mentioned above\ntry {\n    $searcher = Searcher::newWithFileOnly($version, $dbFile);\n} catch (Exception $e) {\n    printf(\"failed to create searcher with '%s': %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 2. Query, both IPv4 or IPv6 addresses are supported\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"\"240e:3b7:3272:d8d0:db09:c067:8d59:539e; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 3. Close resources\n$searcher->close();\n\n// Note: For concurrent use, each thread or coroutine needs to create an independent searcher object.\n```\n\n### Caching `VectorIndex`\n\nIf your PHP environment supports it, you can pre-load the vectorIndex cache and make it a global variable. Using the global vectorIndex every time you create a Searcher can reduce a fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n\n// 1. Load VectorIndex cache from $dbFile and cache the following vIndex variable in memory.\n$vIndex = Util::loadVectorIndexFromFile($dbFile);\nif ($vIndex === null) {\n    printf(\"failed to load vector index from '%s'\\n\", $dbFile);\n    return;\n}\n\n// 2. Use the global vIndex to create a query object with VectorIndex cache.\ntry {\n    $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex);\n} catch (Exception $e) {\n    printf(\"failed to create vectorIndex searcher with '%s': %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 3. Query, both IPv4 or IPv6 are supported\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 4. Close resources\n$searcher->close();\n\n// Note: For concurrent use, each thread or coroutine needs to create an independent searcher object, but they all share the same read-only global vectorIndex.\n```\n\n### Caching the Entire `xdb` File\n\nIf your PHP environment supports it, you can pre-load the entire `xdb` file into memory. This allows for fully memory-based queries, similar to the previous memory search.\n\n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n// 1. Load the entire xdb from $dbFile into memory.\n$cBuff = Util::loadContentFromFile($dbFile);\nif ($cBuff === null) {\n    printf(\"failed to load content buffer from '%s'\\n\", $dbFile);\n    return;\n}\n\n// 2. Use the global cBuff to create a query object that is fully based on memory.\ntry {\n    $searcher = Searcher::newWithBuffer($version, $cBuff);\n} catch (Exception $e) {\n    printf(\"failed to create buffer cached searcher: %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 3. Query, both IPv4 or IPv6 are supported\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 4. Close resources\n// This searcher object can be safely used for concurrency; close it when the entire service is shut down\n// $searcher->close();\n\n// Note: For concurrent use, the searcher object created with the entire xdb cache can be safely used for concurrency.\n```\n\n# Query Testing\n\nRun query tests via the `search_test.php` script:\n\n```bash\n➜  php git:(fr_php_ipv6) ✗ php search_test.php \nphp search_test.php [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: using the default data/ip2region_v4.xdb for IPv4 query testing:\n\n```bash\n➜  php git:(fr_php_ipv6) ✗ php search_test.php --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb file: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 0.12695 ms}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, ioCount: 3, took: 0.07397 ms}\n```\n\nFor example: using the default data/ip2region_v6.xdb for IPv6 query testing:\n\n```bash\n➜  php git:(master) ✗ php ./search_test.php --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb file: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , ioCount: 1, took: 0.08887 ms}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 0.10303 ms}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 0.04614 ms}\n```\n\nEnter an IP to perform a query test. You can also set `cache-policy` to file/vectorIndex/content respectively to test the efficiency of the three different cache implementations.\n\n# Bench Testing\n\nRun automatic bench testing via the `bench_test.php` script. On one hand, this ensures that there are no errors in the `xdb` file; on the other hand, it tests average query performance through a large number of queries:\n\n```bash\n➜  php git:(fr_php_ipv6) ✗ php bench_test.php\nphp bench_test.php [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\nFor example: perform an IPv4 bench test using the default data/ip2region_v4.xdb and data/ipv4_source.txt files:\n\n```bash\nphp bench_test.php --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\nFor example: perform an IPv6 bench test using the default data/ip2region_v6.xdb and data/ipv6_source.txt files:\n\n```bash\nphp bench_test.php --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\nYou can test the performance of the three different cache implementations (file/vectorIndex/content) by setting the `cache-policy` parameter.\n@Note: Please ensure that the src file used for benching is the same source file used to generate the corresponding xdb file.\n\n### Third-party Repository Support\n\n1. Composer supported [zoujingli/ip2region](https://github.com/zoujingli/ip2region) - IPv6 supported.\n"
  },
  {
    "path": "binding/php/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region php 查询客户端\n\n# 使用方式\n\n### 关于查询 API\n查询 API 的原型如下：\n```php\n// 通过字符串 IP 进行查询\n// @throw Exception\nsearch($ip_string) string\n\n// 通过 Util.parseIP 返回的二进制 IP 进行查询\n// @throw Exception\nsearchByBytes($ip_bytes) string\n```\n如果查询失败则会抛出异常，如果查询成功则会返回字符串的 `region` 信息，如果查询的 IP 找不到则会返回空字符串 `\"\"`。\n\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```php\nuse \\ip2region\\xdb\\{IPv4, IPv6};\n\n// 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 IPv4\n$dbFile  = \"../../data/ip2region_v4.xdb\";  // 或者你的 ipv4 xdb 的路径\n$version = IPv4::default();\n\n// 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 IPv6\n$dbFile  = \"../../data/ip2region_v6.xdb\";  // 或者你的 ipv6 xdb 路径\n$version = IPv6::default();\n\n// dbPath 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n// 备注：以下演示直接使用 $dbFile 和 $version 变量\n```\n\n### XDB 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```php\nuse \\ip2region\\xdb\\Util;\n\n$err = Util::verify($dbFile);\nif ($err != null) {\n    // 适用性验证失败！！！\n    // 当前查询客户端实现不适用于 dbPath 指定的 xdb 文件的查询.\n    // 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 dbPath 的 Searcher 实现。\n    printf(\"failed to verify xdb file `%s`: %s\\n\", $dbFile, $err);\n    return;\n}\n\n// 验证通过，当前使用的 Searcher 可以安全的用于对 dbPath 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n// 1, 使用上述的 $version 和 $dbFile 创建 Searcher 对象\ntry {\n    $searcher = Searcher::newWithFileOnly($version, $dbFile);\n} catch (Exception $e) {\n    printf(\"failed to create searcher with '%s': %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 2, 查询，IPv4 或者 IPv6 的地址都支持\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"\"240e:3b7:3272:d8d0:db09:c067:8d59:539e; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 3，关闭资源\n$searcher->close();\n\n// 备注：并发使用，每个线程或者协程需要创建一个独立的 searcher 对象。\n```\n\n### 缓存 `VectorIndex` 索引\n\n如果你的 php 母环境支持，可以预先加载 vectorIndex 缓存，然后做成全局变量，每次创建 Searcher 的时候使用全局的 vectorIndex，可以减少一次固定的 IO 操作从而加速查询，减少 io 压力。 \n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n\n// 1、从 $dbFile 加载 VectorIndex 缓存，把下述的 vIndex 变量缓存到内存里面。\n$vIndex = Util::loadVectorIndexFromFile($dbFile);\nif ($vIndex === null) {\n    printf(\"failed to load vector index from '%s'\\n\", $dbFile);\n    return;\n}\n\n// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。\ntry {\n    $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex);\n} catch (Exception $e) {\n    printf(\"failed to create vectorIndex searcher with '%s': %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 3、查询，IPv4 或者 IPv6 都支持\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 4, 关闭资源\n$searcher->close();\n\n// 备注：并发使用，每个线程或者协程需要创建一个独立的 searcher 对象，但是都共享统一的只读全局 vectorIndex。。\n```\n\n### 缓存整个 `xdb` 文件\n\n如果你的 PHP 母环境支持，可以预先加载整个 `xdb` 的数据到内存，这样可以实现完全基于内存的查询，类似之前的 memory search 查询。\n```php\n// require or autoload the xdb\\Searcher\nrequire 'xdb\\Searcher.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\n\n// 1、从 $dbFile 加载整个 xdb 到内存。\n$cBuff = Util::loadContentFromFile($dbFile);\nif ($cBuff === null) {\n    printf(\"failed to load content buffer from '%s'\\n\", $dbFile);\n    return;\n}\n\n// 2、使用全局的 cBuff 创建带完全基于内存的查询对象。\ntry {\n    $searcher = Searcher::newWithBuffer($version, $cBuff);\n} catch (Exception $e) {\n    printf(\"failed to create buffer cached searcher: %s\\n\", $dbFile, $e->getMessage());\n    return;\n}\n\n// 3、查询，IPv4 或者 IPv6 都支持\ntry {\n    $ip = '1.2.3.4';\n    // $ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"; // IPv6\n    $sTime  = Util::now();\n    $region = $searcher->search($ip);\n    $costMs = Util::now() - $sTime;\n    printf(\"{region: %s, took: %.5f ms}\\n\", $region, $costMs);\n} catch (Exception $e) {\n    printf(\"failed to search(%s): %s\", $ip, $e->getMessage());\n}\n\n// 4，关闭资源\n// 该 searcher 对象可以安全的用于并发，等整个服务都关闭的时候再关闭 searcher\n// $searcher->close();\n\n// 备注：并发使用，用整个 xdb 缓存创建的 searcher 对象可以安全用于并发。\n```\n\n# 查询测试\n\n通过 `search_test.php` 脚本来进行查询测试：\n```bash\n➜  php git:(fr_php_ipv6) ✗ php search_test.php \nphp search_test.php [command options]\noptions: \n --db string             ip2region binary xdb file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 进行 IPv4 的查询测试：\n```bash\n➜  php git:(fr_php_ipv6) ✗ php search_test.php --db=../../data/ip2region_v4.xdb\nip2region xdb searcher test program\nsource xdb file: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 0.12695 ms}\nip2region>> 120.229.45.2\n{region: 中国|广东省|深圳市|移动|CN, ioCount: 3, took: 0.07397 ms}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 进行 IPv6 的查询测试：\n```bash\n➜  php git:(master) ✗ php ./search_test.php --db=../../data/ip2region_v6.xdb\nip2region xdb searcher test program\nsource xdb file: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , ioCount: 1, took: 0.08887 ms}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 0.10303 ms}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 0.04614 ms}\n```\n\n输入 ip 即可进行查询测试。也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效率。\n\n# bench 测试\n\n通过 `bench_test.php` 脚本来进行自动 bench 测试，一方面确保 `xdb` 文件没有错误，另一方面通过大量的查询测试平均查询性能：\n```bash\n➜  php git:(fr_php_ipv6) ✗ php bench_test.php\nphp bench_test.php [command options]\noptions: \n --db string             ip2region binary xdb file path\n --src string            source ip text file path\n --cache-policy string   cache policy: file/vectorIndex/content\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 文件进行 IPv4 的 bench 测试：\n```bash\nphp bench_test.php --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 文件进行 IPv6 的 bench 测试：\n```bash\nphp bench_test.php --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\n可以通过设置 `cache-policy` 参数来分别测试 file/vectorIndex/content 三种不同的缓存实现的的性能。\n@Note：请注意 bench 使用的 src 文件需要是生成对应的 xdb 文件的相同的源文件。\n\n\n### 第三方仓库支持\n1. composer 支持的 [zoujingli/ip2region](https://github.com/zoujingli/ip2region) - 已提供 IPv6 支持。\n"
  },
  {
    "path": "binding/php/batch_test.php",
    "content": "<?php\n// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/22\n\nrequire dirname(__FILE__) . '/xdb/Searcher.class.php';\n\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\{IPv4, IPv6};\nuse \\ip2region\\xdb\\Searcher;\n\nfunction printHelp($argv) {\n    printf(\"php %s [command options]\\n\", $argv[0]);\n    printf(\"options: \\n\");\n    printf(\" --db string             ip2region binary xdb file path\\n\");\n    printf(\" --src string            source ip text file path\\n\");\n    printf(\" --cache-policy string   cache policy: file/vectorIndex/content\\n\");\n}\n\nif($argc < 2) {\n    printHelp($argv);\n    return;\n}\n\n$dbFile = \"\";\n$srcFile = \"\";\n$cachePolicy = 'vectorIndex';\narray_shift($argv);\nforeach ($argv as $r) {\n    if (strlen($r) < 5) {\n        continue;\n    }\n\n    if (strpos($r, '--') != 0) {\n        continue;\n    }\n\n    $sIdx = strpos($r, \"=\");\n    if ($sIdx < 0) {\n        printf(\"missing = for args pair %s\\n\", $r);\n        return;\n    }\n\n    $key = substr($r, 2, $sIdx - 2);\n    $val = substr($r, $sIdx + 1);\n    if ($key == 'db') {\n        $dbFile = $val;\n    } else if ($key == 'src') {\n        $srcFile = $val;\n    } else if ($key == 'cache-policy') {\n        $cachePolicy = $val;\n    } else {\n        printf(\"undefined option `%s`\\n\", $r);\n        return;\n    }\n}\n\nif (strlen($dbFile) < 1 || strlen($srcFile) < 1) {\n    printHelp($argv);\n    return;\n}\n\n// printf(\"debug: dbFile: %s, cachePolicy: %s\\n\", $dbFile, $cachePolicy);\n$handle = fopen($dbFile, 'r');\nif ($handle === false) {\n    printf(\"failed to open the xdb file `{$dbFile}`\\n\");\n    return;\n}\n\n// load header\n$header = Util::loadHeader($handle);\nif ($header == null) {\n    printf(\"failed to load the header\\n\");\n    return;\n}\n\n// get the version number from the xdb header\ntry {\n    $version = Util::versionFromHeader($header);\n} catch (Exception $e) {\n    printf(\"failed to detect version from header: {$e->getMessage()}\\n\");\n    return;\n}\n\n// create the xdb searcher by the cache-policy\nswitch ( $cachePolicy ) {\n    case 'file':\n        try {\n            $searcher = Searcher::newWithFileOnly($version, $dbFile);\n        } catch (Exception $e) {\n            printf(\"failed to create searcher with '%s': %s\\n\", $dbFile, $e);\n            return;\n        }\n        break;\n    case 'vectorIndex':\n        $vIndex = Util::loadVectorIndex($handle);\n        if ($vIndex == null) {\n            printf(\"failed to load vector index from '%s'\\n\", $dbFile);\n            return;\n        }\n\n        try {\n            $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex);\n        } catch (Exception $e) {\n            printf(\"failed to create vector index cached searcher with '%s': %s\\n\", $dbFile, $e);\n            return;\n        }\n        break;\n    case 'content':\n        $cBuff = Util::loadContent($handle);\n        if ($cBuff == null) {\n            printf(\"failed to load xdb content from '%s'\\n\", $dbFile);\n            return;\n        }\n\n        try {\n            $searcher = Searcher::newWithBuffer($version, $cBuff);\n        } catch (Exception $e) {\n            printf(\"failed to create content cached searcher: %s\", $e);\n            return;\n        }\n        break;\n    default:\n        printf(\"undefined cache-policy `%s`\\n\", $cachePolicy);\n        return;\n}\n\n\n// do the bench test\n$handle = fopen($srcFile, \"r\");\nif ($handle === false) {\n    printf(\"failed to open source text file `%s`\\n\", $srcFile);\n    return null;\n}\n\n$count = 0;\n$qx_count = 0;\nwhile (!feof($handle)) {\n    $line = trim(fgets($handle, 1024));\n    if (strlen($line) < 1) {\n        continue;\n    }\n\n    //@Note extract the ip address from line with special chars\n    // if (preg_match('/^([0-9\\.]+)/', $line, $m) != 1) {\n    //     continue;\n    // }\n\n    // $line = $m[1];\n    // print(\"ip: {$m[1]}\\n\");\n\n    try {\n        $ipBytes = Util::parseIP($line);\n    } catch (Exception $e) {\n        printf(\"failed to parse ip `%s`: %s\\n\", $line, $e->getMessage());\n        continue;\n    }\n\n    $count++;\n    $region = $searcher->searchByBytes($ipBytes);\n    $ss = explode('|', $region);\n    echo $line, \",\", str_replace('|', ',', $region), \"\\n\";\n}\n\nfclose($handle);\necho \"Done, with {$count} IPs\\n\";\n"
  },
  {
    "path": "binding/php/bench_test.php",
    "content": "<?php\n// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/22\n\nrequire dirname(__FILE__) . '/xdb/Searcher.class.php';\n\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\{IPv4, IPv6};\nuse \\ip2region\\xdb\\Searcher;\n\nfunction printHelp($argv) {\n    printf(\"php %s [command options]\\n\", $argv[0]);\n    printf(\"options: \\n\");\n    printf(\" --db string             ip2region binary xdb file path\\n\");\n    printf(\" --src string            source ip text file path\\n\");\n    printf(\" --cache-policy string   cache policy: file/vectorIndex/content\\n\");\n}\n\nif($argc < 2) {\n    printHelp($argv);\n    return;\n}\n\n$dbFile = \"\";\n$srcFile = \"\";\n$cachePolicy = 'vectorIndex';\narray_shift($argv);\nforeach ($argv as $r) {\n    if (strlen($r) < 5) {\n        continue;\n    }\n\n    if (strpos($r, '--') != 0) {\n        continue;\n    }\n\n    $sIdx = strpos($r, \"=\");\n    if ($sIdx < 0) {\n        printf(\"missing = for args pair %s\\n\", $r);\n        return;\n    }\n\n    $key = substr($r, 2, $sIdx - 2);\n    $val = substr($r, $sIdx + 1);\n    if ($key == 'db') {\n        $dbFile = $val;\n    } else if ($key == 'src') {\n        $srcFile = $val;\n    } else if ($key == 'cache-policy') {\n        $cachePolicy = $val;\n    } else {\n        printf(\"undefined option `%s`\\n\", $r);\n        return;\n    }\n}\n\nif (strlen($dbFile) < 1 || strlen($srcFile) < 1) {\n    printHelp($argv);\n    return;\n}\n\n// printf(\"debug: dbFile: %s, cachePolicy: %s\\n\", $dbFile, $cachePolicy);\n$handle = fopen($dbFile, 'r');\nif ($handle === false) {\n    printf(\"failed to open the xdb file `{$dbFile}`\\n\");\n    return;\n}\n\n// verify the xdb file\n// @Note: do NOT call it every time you create a searcher since this will slow\n// down the search response.\n// @see the Util.verify function for details.\n$err = Util::verify($handle);\nif ($err != null) {\n    printf(\"failed to verify xdb file `%s`: %s\\n\", $dbFile, $err);\n    return;\n}\n\n// load header\n$header = Util::loadHeader($handle);\nif ($header == null) {\n    printf(\"failed to load the header\\n\");\n    return;\n}\n\n// get the version number from the xdb header\ntry {\n    $version = Util::versionFromHeader($header);\n} catch (Exception $e) {\n    printf(\"failed to detect version from header: {$e->getMessage()}\\n\");\n    return;\n}\n\n// create the xdb searcher by the cache-policy\nswitch ( $cachePolicy ) {\n    case 'file':\n        try {\n            $searcher = Searcher::newWithFileOnly($version, $dbFile);\n        } catch (Exception $e) {\n            printf(\"failed to create searcher with '%s': %s\\n\", $dbFile, $e);\n            return;\n        }\n        break;\n    case 'vectorIndex':\n        $vIndex = Util::loadVectorIndex($handle);\n        if ($vIndex == null) {\n            printf(\"failed to load vector index from '%s'\\n\", $dbFile);\n            return;\n        }\n\n        try {\n            $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex);\n        } catch (Exception $e) {\n            printf(\"failed to create vector index cached searcher with '%s': %s\\n\", $dbFile, $e);\n            return;\n        }\n        break;\n    case 'content':\n        $cBuff = Util::loadContent($handle);\n        if ($cBuff == null) {\n            printf(\"failed to load xdb content from '%s'\\n\", $dbFile);\n            return;\n        }\n\n        try {\n            $searcher = Searcher::newWithBuffer($version, $cBuff);\n        } catch (Exception $e) {\n            printf(\"failed to create content cached searcher: %s\", $e);\n            return;\n        }\n        break;\n    default:\n        printf(\"undefined cache-policy `%s`\\n\", $cachePolicy);\n        return;\n}\n\n\n// do the bench test\n$handle = fopen($srcFile, \"r\");\nif ($handle === false) {\n    printf(\"failed to open source text file `%s`\\n\", $srcFile);\n    return null;\n}\n\n$count = 0;\n$costs = 0;\n$sTime = Util::now();\nwhile (!feof($handle)) {\n    $line = trim(fgets($handle, 1024));\n    if (strlen($line) < 1) {\n        continue;\n    }\n\n    // ignore the comment\n    if ($line[0] == '#') {\n        continue;\n    }\n\n    $ps = explode('|', $line, 3);\n    if (count($ps) != 3) {\n        printf(\"invalid ip segment line `${line}`\\n\");\n        return;\n    }\n\n    $sip = Util::parseIP($ps[0]);\n    if ($sip === null) {\n        printf(\"invalid start ip `%s`\\n\", $ps[0]);\n        return;\n    }\n\n    $eip = Util::parseIP($ps[1]);\n    if ($eip === null) {\n        printf(\"invalid end ip `%s`\\n\", $ps[1]);\n        return;\n    }\n\n    if (Util::ipCompare($sip, $eip) > 0) {\n        printf(\n            \"start ip(%s) should not be greater than end ip(%s)\\n\", \n            Util::ipToString($ps[0]), Util::ipToString($ps[1])\n        );\n        return;\n    }\n\n    foreach ([$sip, $eip] as $ip) {\n        try {\n            $cTime = Util::now();\n            $region = $searcher->searchByBytes($ip);\n            $costs += Util::now() - $cTime;\n        } catch (Exception $e) {\n            printf(\"failed to search ip `%s`: %s\\n\", Util::ipToString($ip), $e->getMessage());\n            return;\n        }\n\n        if ($region == null) {\n            printf(\"failed to search ip `%s`: empty region info\\n\", Util::ipToString($ip));\n            return;\n        }\n\n        // check the region info\n        if ($region != $ps[2]) {\n            printf(\"failed search(%s) with (%s != %s)\\n\", Util::ipToString($ip), $region, $ps[2]);\n            return;\n        }\n\n        $count++;\n    }\n}\n\n// close the searcher at last\nfclose($handle);\n$searcher->close();\nprintf(\"Bench finished, {cachePolicy: %s, total: %d, took: %ds, cost: %.3f ms/op}\\n\",\n    $cachePolicy, $count, (Util::now() - $sTime)/1000, $count == 0 ? 0 : $costs/$count);\n"
  },
  {
    "path": "binding/php/search_test.php",
    "content": "<?php\n// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/21\n\nrequire dirname(__FILE__) . '/xdb/Searcher.class.php';\n\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\{IPv4, IPv6};\nuse \\ip2region\\xdb\\Searcher;\n\nfunction printHelp($argv) {\n    printf(\"php %s [command options]\\n\", $argv[0]);\n    printf(\"options: \\n\");\n    printf(\" --db string             ip2region binary xdb file path\\n\");\n    printf(\" --cache-policy string   cache policy: file/vectorIndex/content\\n\");\n}\n\nif($argc < 2) {\n    printHelp($argv);\n    return;\n}\n\n$dbFile = \"\";\n$cachePolicy = 'vectorIndex';\narray_shift($argv);\nforeach ($argv as $r) {\n    if (strlen($r) < 5) {\n        continue;\n    }\n\n    if (strpos($r, '--') != 0) {\n        continue;\n    }\n\n    $sIdx = strpos($r, \"=\");\n    if ($sIdx < 0) {\n        printf(\"missing = for args pair %s\\n\", $r);\n        return;\n    }\n\n    $key = substr($r, 2, $sIdx - 2);\n    $val = substr($r, $sIdx + 1);\n    if ($key == 'db') {\n        $dbFile = $val;\n    } else if ($key == 'cache-policy') {\n        $cachePolicy = $val;\n    } else {\n        printf(\"undefined option `%s`\\n\", $r);\n        return;\n    }\n}\n\nif (strlen($dbFile) < 1) {\n    printHelp($argv);\n    return;\n}\n\n// printf(\"debug: dbFile: %s, cachePolicy: %s\\n\", $dbFile, $cachePolicy);\n$handle = fopen($dbFile, 'r');\nif ($handle === false) {\n    printf(\"failed to open the xdb file `{$dbFile}`\\n\");\n    return;\n}\n\n// verify the xdb file\n// @Note: do NOT call it every time you create a searcher since this will slow\n// down the search response.\n// @see the Util.verify function for details.\n$err = Util::verify($handle);\nif ($err != null) {\n    printf(\"failed to verify xdb file `%s`: %s\\n\", $dbFile, $err);\n    return;\n}\n\n// load header\n$header = Util::loadHeader($handle);\nif ($header == null) {\n    printf(\"failed to load the header\\n\");\n    return;\n}\n\n// get the version number from the xdb header\ntry {\n    $version = Util::versionFromHeader($header);\n} catch (Exception $e) {\n    printf(\"failed to detect version from header: {$e->getMessage()}\\n\");\n    return;\n}\n\n// create the xdb searcher by the cache-policy\nswitch ( $cachePolicy ) {\ncase 'file':\n    try {\n        $searcher = Searcher::newWithFileOnly($version, $dbFile);\n    } catch (Exception $e) {\n        printf(\"failed to create searcher with '%s': %s\\n\", $dbFile, $e);\n        return;\n    }\n    break;\ncase 'vectorIndex':\n    $vIndex = Util::loadVectorIndex($handle);\n    if ($vIndex == null) {\n        printf(\"failed to load vector index from '%s'\\n\", $dbFile);\n        return;\n    }\n\n    try {\n        $searcher = Searcher::newWithVectorIndex($version, $dbFile, $vIndex);\n    } catch (Exception $e) {\n        printf(\"failed to create vector index cached searcher with '%s': %s\\n\", $dbFile, $e);\n        return;\n    }\n    break;\ncase 'content':\n    $cBuff = Util::loadContent($handle);\n    if ($cBuff == null) {\n        printf(\"failed to load xdb content from '%s'\\n\", $dbFile);\n        return;\n    }\n\n    try {\n        $searcher = Searcher::newWithBuffer($version, $cBuff);\n    } catch (Exception $e) {\n        printf(\"failed to create content cached searcher: %s\", $e);\n        return;\n    }\n    break;\ndefault:\n    printf(\"undefined cache-policy `%s`\\n\", $cachePolicy);\n    return;\n}\n\nprintf(<<<EOF\nip2region xdb searcher test program\nsource xdb file: {$dbFile} ({$version->name}, ${cachePolicy})\ntype 'quit' to exit\\n\nEOF);\nwhile ( true ) {\n    echo \"ip2region>> \";\n    $line = trim(fgets(STDIN));\n    if (strlen($line) < 2) {\n        continue;\n    }\n\n    if ($line == 'quit') {\n        break;\n    }\n\n    $cost = -1;\n    try {\n        $sTime = Util::now();\n        $region = $searcher->search($line);\n        $cost = Util::now() - $sTime;\n    } catch (Exception $e) {\n        printf(\"search call failed: %s\\n\", $e->getMessage());\n        continue;\n    }\n\n    printf(\n        \"{region: %s, ioCount: %d, took: %.5f ms}\\n\",\n        $region, $searcher->getIOCount(), $cost\n    );\n}\n\n// close the searcher at last\n$searcher->close();\nprintf(\"searcher test program exited, thanks for trying\\n\");"
  },
  {
    "path": "binding/php/xdb/Searcher.class.php",
    "content": "<?php\n// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/21\n\nnamespace ip2region\\xdb;\nuse \\Exception;\n\n// global constants\nconst Structure_20     = 2;\nconst Structure_30     = 3;\nconst IPv4VersionNo    = 4;\nconst IPv6VersionNo    = 6;\nconst HeaderInfoLength = 256;\nconst VectorIndexRows  = 256;\nconst VectorIndexCols  = 256;\nconst VectorIndexSize  = 8;\n\n\n// Util class\nclass Util {\n    // parse the specified IP address and return its bytes.\n    // returns: NULL for failed or the packed bytes\n    public static function parseIP($ipString) {\n        $flag = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;\n        if (!filter_var($ipString, FILTER_VALIDATE_IP, $flag)) {\n            return null;\n        }\n\n        return inet_pton($ipString);\n    }\n\n    // IP bytes to string\n    public static function ipToString($ipBytes) {\n        $l = strlen($ipBytes);\n        return ($l == 4 || $l == 16) ? inet_ntop($ipBytes) : '<invalid-ip-bytes>';\n    }\n\n    // compare two ip bytes (packed string return by parsedIP)\n    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2\n    public static function ipSubCompare($ip1, $buff, $offset) {\n        // $r = substr_compare($ip1, $buff, $offset, strlen($ip1));\n        // @Note: substr_compare is not working, use the substr + strcmp instead\n        $r = strcmp($ip1, substr($buff, $offset, strlen($ip1)));\n        if ($r < 0) {\n            return -1;\n        } else if ($r > 0) {\n            return 1;\n        } else {\n            return 0;\n        }\n    }\n\n    // returns: -1 if ip1 < ip2, 0 if ip1 == ip2 or 1 if ip1 > ip2\n    public static function ipCompare($ip1, $ip2) {\n        $r = strcmp($ip1, $ip2);\n        if ($r < 0) {\n            return -1;\n        } else if ($r > 0) {\n            return 1;\n        } else {\n            return 0;\n        }\n    }\n\n    // version parse\n    public static function versionFromName($ver_name) {\n        $name = strtoupper($ver_name);\n        if ($name == \"V4\" || $name == \"IPv4\") {\n            return IPv4::default();\n        } else if ($name == \"V6\" || $name == \"IPv6\") {\n            return IPv6::default();\n        } else {\n            throw new Exception(\"invalid verstion name `{$ver_name}`\");\n        }\n    }\n\n    // version parse from header\n    public static function versionFromHeader($header) {\n        // Old structure 2.0 with IPv4 supports ONLY\n        if ($header['version'] == Structure_20) {\n            return IPv4::default();\n        }\n\n        // structure 3.0 after IPv6 supporting\n        if ($header['version'] != Structure_30) {\n            throw new Exception(\"invalid xdb structure version `{$header['version']}`\");\n        }\n\n        if ($header['ipVersion'] == IPv4VersionNo) {\n            return IPv4::default();\n        } else if ($header['ipVersion'] == IPv6VersionNo) {\n            return IPv6::default();\n        } else {\n            throw new Exception(\"invalid ip version number `{$header['ipVersion']}`\");\n        }\n    }\n\n    // binary string chars implode with space\n    public static function bytesToString($buff, $offset, $length) {\n        $sb = [];\n        for ($i = 0; $i < $length; $i++) {\n            $sb[] = ord($buff[$offset+$i]) & 0xFF;\n        }\n        return '['.implode(' ', $sb).']';\n    }\n\n    // decode a 4bytes long with Little endian byte order from a byte buffer\n    public static function le_getUint32($b, $idx) {\n        $val = (ord($b[$idx])) | (ord($b[$idx+1]) << 8)\n            | (ord($b[$idx+2]) << 16) | (ord($b[$idx+3]) << 24);\n\n        // convert signed int to unsigned int if on 32 bit operating system\n        if ($val < 0 && PHP_INT_SIZE == 4) {\n            $val = sprintf(\"%u\", $val);\n        }\n\n        return $val;\n    }\n\n    // read a 2bytes int with litten endian byte order from a byte buffer\n    public static function le_getUint16($b, $idx) {\n        return ((ord($b[$idx])) | (ord($b[$idx+1]) << 8));\n    }\n\n    // Verify if the current Searcher could be used to search the specified xdb file.\n    // Why do we need this check ?\n    // The future features of the xdb impl may cause the current searcher not able to work properly.\n    //\n    // @Note: You Just need to check this ONCE when the service starts\n    // Or use another process (eg, A command) to check once Just to confirm the suitability.\n    // returns: null for everything is ok or the error string.\n    public static function verify($handle) {\n        // load the header\n        $header = self::loadHeader($handle);\n        if ($header == null) {\n            return 'failed to load the header';\n        }\n\n        // get the runtime ptr bytes\n        $runtimePtrBytes = 0;\n        if ($header['version'] == Structure_20) {\n            $runtimePtrBytes = 4;\n        } else if ($header['version'] == Structure_30) {\n            $runtimePtrBytes = $header['runtimePtrBytes'];\n        } else {\n            return \"invalid structure version `{$header['version']}`\";\n        }\n\n        // 1, confirm the xdb file size\n        // to ensure that the maximum file pointer does not overflow\n        $stat = fstat($handle);\n        if ($stat == false) {\n            return 'failed to stat the xdb file';\n        }\n\n        $maxFilePtr = (1 << ($runtimePtrBytes * 8)) - 1;\n        // print_r([$stat['size'], $maxFilePtr]);\n        if ($stat['size'] > $maxFilePtr) {\n             return \"xdb file exceeds the maximum supported bytes: {$maxFilePtr}\";\n        }\n\n        return null;\n    }\n\n    public static function verifyFromFile($dbFile) {\n        $handle = fopen($dbFile, 'r');\n        if ($handle === false) {\n            return null;\n        }\n\n        $r = self::verify($handle);\n        fclose($handle);\n        return $r;\n    }\n\n    // load header info from a specified file handle\n    public static function loadHeader($handle) {\n        if (fseek($handle, 0) == -1) {\n            return null;\n        }\n\n        $buff = fread($handle, HeaderInfoLength);\n        if ($buff === false) {\n            return null;\n        }\n\n        // read bytes length checking\n        if (strlen($buff) != HeaderInfoLength) {\n            return null;\n        }\n\n        // return the decoded header info\n        return array(\n            'version'         => self::le_getUint16($buff, 0),\n            'indexPolicy'     => self::le_getUint16($buff, 2),\n            'createdAt'       => self::le_getUint32($buff, 4),\n            'startIndexPtr'   => self::le_getUint32($buff, 8),\n            'endIndexPtr'     => self::le_getUint32($buff, 12),\n            'ipVersion'       => self::le_getUint16($buff, 16),\n            'runtimePtrBytes' => self::le_getUint16($buff, 18)\n        );\n    }\n\n    // load header info from the specified xdb file path\n    public static function loadHeaderFromFile($dbFile) {\n        $handle = fopen($dbFile, 'r');\n        if ($handle === false) {\n            return null;\n        }\n\n        $header = self::loadHeader($handle);\n        fclose($handle);\n        return $header;\n    }\n\n    // load vector index from a file handle\n    public static function loadVectorIndex($handle) {\n        if (fseek($handle, HeaderInfoLength) == -1) {\n            return null;\n        }\n\n        $rLen = VectorIndexRows * VectorIndexCols * VectorIndexSize;\n        $buff = fread($handle, $rLen);\n        if ($buff === false) {\n            return null;\n        }\n\n        if (strlen($buff) != $rLen) {\n            return null;\n        }\n\n        return $buff;\n    }\n\n    // load vector index from a specified xdb file path\n    public static function loadVectorIndexFromFile($dbFile) {\n        $handle = fopen($dbFile, 'r');\n        if ($handle === false) {\n            return null;\n        }\n\n        $vIndex = self::loadVectorIndex($handle);\n        fclose($handle);\n        return $vIndex;\n    }\n\n    // load the xdb content from a file handle\n    public static function loadContent($handle) {\n        if (fseek($handle, 0, SEEK_END) == -1) {\n            return null;\n        }\n\n        $size = ftell($handle);\n        if ($size === false) {\n            return null;\n        }\n\n        // seek to the head for reading\n        if (fseek($handle, 0) == -1) {\n            return null;\n        }\n\n        $buff = fread($handle, $size);\n        if ($buff === false) {\n            return null;\n        }\n\n        // read length checking\n        if (strlen($buff) != $size) {\n            return null;\n        }\n\n        return $buff;\n    }\n\n    // load the xdb content from a file path\n    public static function loadContentFromFile($dbFile) {\n        $str = file_get_contents($dbFile, false);\n        if ($str === false) {\n            return null;\n        } else {\n            return $str;\n        }\n    }\n\n    public static function now() {\n        return (microtime(true) * 1000);\n    }\n}\n\n// IPv4 version class\nclass IPv4 {\n    public $id;\n    public $name;\n    public $bytes;\n    public $segmentIndexSize;\n    \n    private static $C = null;\n    public static function default() {\n        if (self::$C == null) {\n            // 14 = 4 + 4 + 2 + 4\n            self::$C = new self(IPv4VersionNo, 'IPv4', 4, 14);\n        }\n        return self::$C;\n    }\n\n    public function __construct($id, $name, $bytes, $segmentIndexSize) {\n        $this->id = $id;\n        $this->name = $name;\n        $this->bytes = $bytes;\n        $this->segmentIndexSize = $segmentIndexSize;\n    }\n\n    // compare the two ip bytes with the current version\n    public function ipSubCompare($ip1, $buff, $offset) {\n        // ip1: Little endian byte order encoded long from searcher.\n        // ip2: Little endian byte order read from xdb index.\n        $len  = strlen($ip1);\n        $eIdx = $offset + $len;\n        for ($i = 0, $j = $eIdx - 1; $i < $len; $i++, $j--) {\n            $i1 = ord($ip1[$i]) & 0xFF;\n            $i2 = ord($buff[$j]) & 0xFF;\n            // printf(\"i:%d, j:%d, i1:%d, i2:%d\\n\", $i, $j, $i1, $i2);\n            if ($i1 > $i2) {\n                return 1;\n            } else if ($i1 < $i2) {\n                return -1;\n            }\n        }\n\n        return 0;\n    }\n\n    public function __toString() {\n        return sprintf(\n            \"{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}\", \n            $this->id, $this->name, $this->bytes, $this->segmentIndexSize\n        );\n    }\n}\n\nclass IPv6 {\n    public $id;\n    public $name;\n    public $bytes;\n    public $segmentIndexSize;\n\n    private static $C = null;\n    public static function default() {\n        if (self::$C == null) {\n            // 38 = 16 + 16 + 2 + 4\n            self::$C = new self(IPv6VersionNo, 'IPv6', 16, 38);\n        }\n\n        return self::$C;\n    }\n\n    public function __construct($id, $name, $bytes, $segmentIndexSize) {\n        $this->id = $id;\n        $this->name = $name;\n        $this->bytes = $bytes;\n        $this->segmentIndexSize = $segmentIndexSize;\n    }\n\n    public function ipSubCompare($ip, $buff, $offset) {\n        // return Util::ipCompare($ip, substr($buff, $offset, strlen($ip)));\n        return Util::ipSubCompare($ip, $buff, $offset);\n    }\n\n    public function __toString() {\n        return sprintf(\n            \"{id:%d, name:%s, bytes:%d, segmentIndexSize:%d}\", \n            $this->id, $this->name, $this->bytes, $this->segmentIndexSize\n        );\n    }\n}\n\n// Xdb searcher implementation\nclass Searcher {\n    // ip version\n    private $version;\n\n    // xdb file handle\n    private $handle  = null;\n    private $ioCount = 0;\n\n    // vector index in binary string.\n    // string decode will be faster than the map based Array.\n    private $vectorIndex = null;\n\n    // xdb content buffer\n    private $contentBuff = null;\n\n    // ---\n    // static function to create searcher\n\n    /**\n     * @throws Exception\n     */\n    public static function newWithFileOnly($version, $dbFile) {\n        return new self($version, $dbFile, null, null);\n    }\n\n    /**\n     * @throws Exception\n     */\n    public static function newWithVectorIndex($version, $dbFile, $vIndex) {\n        return new self($version, $dbFile, $vIndex, null);\n    }\n\n    /**\n     * @throws Exception\n     */\n    public static function newWithBuffer($version, $cBuff) {\n        return new self($version, null, null, $cBuff);\n    }\n\n    // --- End of static creator\n\n    /**\n     * initialize the xdb searcher\n     * @throws Exception\n     */\n    function __construct($version, $dbFile, $vectorIndex=null, $cBuff=null) {\n        $this->version = $version;\n        // check the content buffer first\n        if ($cBuff != null) {\n            $this->vectorIndex = null;\n            $this->contentBuff = $cBuff;\n        } else {\n            // open the xdb binary file\n            $this->handle = fopen($dbFile, \"r\");\n            if ($this->handle === false) {\n                throw new Exception(\"failed to open xdb file '%s'\", $dbFile);\n            }\n\n            $this->vectorIndex = $vectorIndex;\n        }\n    }\n\n    public function close() {\n        if ($this->handle != null) {\n            fclose($this->handle);\n        }\n    }\n\n    public function getIPVersion() {\n        return $this->version;\n    }\n\n    public function getIOCount() {\n        return $this->ioCount;\n    }\n\n    /**\n     * find the region info for the specified ip address.\n     * @Note: the ip address couldO ONLY be a human-readable IP address string,\n     * DO not use the packed binary string returned by #parseIP\n     * \n     * @throws Exception\n     */\n    public function search($ip) {\n        $ipBytes = Util::parseIP($ip);\n        if ($ipBytes == null) {\n            throw new Exception(\"invalid ip address `{$ip}`\");\n        }\n\n        return $this->searchByBytes($ipBytes);\n    }\n\n    /**\n     * find the region info for the specified binary ip bytes returned by #parseIP.\n     * \n     * @throws Exception\n     */\n    public function searchByBytes($ipBytes) {\n        // ip version check\n        if (strlen($ipBytes) != $this->version->bytes) {\n            throw new Exception(\"invalid ip address ({$this->version->name} expected)\");\n        }\n\n        // reset the global counter\n        $this->ioCount = 0;\n\n        // locate the segment index block based on the vector index\n        $il0 = ord($ipBytes[0]) & 0xFF;\n        $il1 = ord($ipBytes[1]) & 0xFF;\n        $idx = $il0 * VectorIndexCols * VectorIndexSize + $il1 * VectorIndexSize;\n        if ($this->vectorIndex != null) {\n            $sPtr = Util::le_getUint32($this->vectorIndex, $idx);\n            $ePtr = Util::le_getUint32($this->vectorIndex, $idx + 4);\n        } else if ($this->contentBuff != null) {\n            $sPtr = Util::le_getUint32($this->contentBuff, HeaderInfoLength + $idx);\n            $ePtr = Util::le_getUint32($this->contentBuff, HeaderInfoLength + $idx + 4);\n        } else {\n            // read the vector index block\n            $buff = $this->read(HeaderInfoLength + $idx, 8);\n            $sPtr = Util::le_getUint32($buff, 0);\n            $ePtr = Util::le_getUint32($buff, 4);\n        }\n\n        // printf(\"sPtr: %d, ePtr: %d\\n\", $sPtr, $ePtr);\n        // @Note: ptr validate, zero ptr means source data missing\n        // so we could just stop here and return an empty string.\n        if ($sPtr == 0 || $ePtr == 0) {\n            return \"\";\n        }\n\n        [$bytes, $dBytes] = [strlen($ipBytes), strlen($ipBytes) << 1];\n\n        // binary search the segment index to get the region info\n        $idxSize = $this->version->segmentIndexSize;\n        [$dataLen, $dataPtr, $l, $h] = [0, 0, 0, ($ePtr - $sPtr) / $idxSize];\n        while ($l <= $h) {\n            $m = ($l + $h) >> 1;\n            $p = $sPtr + $m * $idxSize;\n\n            // read the segment index\n            $buff = $this->read($p, $idxSize);\n\n            // compare the segment index\n            if ($this->version->ipSubCompare($ipBytes, $buff, 0) < 0) {\n                $h = $m - 1;\n            } else if ($this->version->ipSubCompare($ipBytes, $buff, $bytes) > 0) {\n                $l = $m + 1;\n            } else {\n                $dataLen = Util::le_getUint16($buff, $dBytes);\n                $dataPtr = Util::le_getUint32($buff, $dBytes + 2);\n                break;\n            }\n        }\n\n        // empty match interception.\n        // printf(\"dataLen: %d, dataPtr: %d\\n\", $dataLen, $dataPtr);\n        if ($dataLen == 0) {\n            return \"\";\n        }\n\n        // load and return the region data\n        return $this->read($dataPtr, $dataLen);\n    }\n\n    // read specified bytes from the specified index\n    private function read($offset, $len) {\n        // check the in-memory buffer first\n        if ($this->contentBuff != null) {\n            return substr($this->contentBuff, $offset, $len);\n        }\n\n        // read from the file\n        $r = fseek($this->handle, $offset);\n        if ($r == -1) {\n            throw new Exception(\"failed to fseek to {$offset}\");\n        }\n\n        $this->ioCount++;\n        $buff = fread($this->handle, $len);\n        if ($buff === false) {\n            throw new Exception(\"failed to fread from {$len}\");\n        }\n\n        if (strlen($buff) != $len) {\n            throw new Exception(\"incomplete read: read bytes should be {$len}\");\n        }\n\n        return $buff;\n    }\n\n}\n"
  },
  {
    "path": "binding/php/xdb/util_test.php",
    "content": "<?php\n// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/06/22\n\nrequire 'Searcher.class.php';\nuse \\ip2region\\xdb\\Util;\nuse \\ip2region\\xdb\\Searcher;\nuse \\ip2region\\xdb\\IPv4;\nuse \\ip2region\\xdb\\IPv6;\n\n// check and get the function to run\nif($argc < 2) {\n    printf(\"please specified the function name\\n\");\n    return;\n} else {\n    $func_name = trim($argv[1]);\n}\n\n$basePath = dirname(dirname(dirname(dirname(__FILE__))));\n\nfunction testLoadHeader() {\n    global $basePath;\n    $header = Util::loadHeaderFromFile(\"{$basePath}/data/ip2region_v4.xdb\");\n    if ($header == null) {\n        printf(\"failed to load header from file\\n\");\n        return;\n    }\n\n    printf(\"header loaded: \");\n    print_r($header);\n}\n\nfunction testLoadVectorIndex() {\n    global $basePath;\n    $vIndex = Util::loadVectorIndexFromFile(\"{$basePath}/data/ip2region_v4.xdb\");\n    if ($vIndex == null) {\n        printf(\"failed to load vector index from file\\n\");\n        return;\n    }\n\n    printf(\"vector index loaded: length=%d\\n\", strlen($vIndex));\n}\n\nfunction testLoadContent() {\n    global $basePath;\n    $cBuff = Util::loadContentFromFile(\"{$basePath}/data/ip2region_v4.xdb\");\n    if ($cBuff == null) {\n        printf(\"failed to load content from file\\n\");\n        return;\n    }\n\n    printf(\"content loaded, length=%d\\n\", strlen($cBuff));\n}\n\nfunction testParseIP() {\n    $ips = [\n        // IPv4\n        \"1.0.0.1\", \n        \"192.168.1.100\",\n        \"121.35.184.170\",\n\n        \"xx.xx.1.100\",\n\n        // IPv6\n        \"3000::\",\n        \"240e:87c:71a:639a:3dff:ffff:ffff:ffff\",\n        \"240e:87c:71a:c877:900::\"\n    ];\n\n    foreach ($ips as $ip) {\n        $bytes = Util::parseIP($ip);\n        if ($bytes == NULL) {\n            printf(\"invalid ip address: `%s`\\n\", $ip);\n            continue;\n        }\n\n        // for ($i = 0; $i < strlen($bytes); $i++) {\n        //     printf(\"%d: []: %s, ord: %d\\n\", $i, $bytes[$i], ord($bytes[$i]));\n        // }\n\n        printf(\"bytes: %s (%s), address: %s\\n\", bin2hex($bytes), gettype($bytes), Util::ipToString($bytes));\n    }\n}\n\nfunction testIPCompare() {\n    $ipPairs = [\n        [\"1.0.0.0\", \"1.0.0.1\"],\n        [\"192.168.1.101\", \"192.168.1.90\"],\n        [\"219.133.111.87\", \"114.114.114.114\"],\n        [\"1.0.4.0\", \"1.0.1.0\"],\n        [\"1.0.4.0\", \"1.0.3.255\"],\n        [\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"],\n        [\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\"],\n        [\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\"]\n    ];\n\n    foreach ($ipPairs as $ips) {\n        $ip1 = Util::parseIP($ips[0]);\n        $ip2 = Util::parseIP($ips[1]);\n        printf(\"ipCompare(%s, %s): %d\\n\", Util::ipToString($ip1), Util::ipToString($ip2), Util::ipSubCompare($ip1, $ip2, 0));\n    }\n}\n\nfunction testAttributes() {\n    printf(\"IPv4VersioNo: %d\\n\", \\ip2region\\xdb\\IPv4VersionNo);\n    printf(\"IPv6VersioNo: %d\\n\", \\ip2region\\xdb\\IPv6VersionNo);\n    printf(\"IPv4 Object: %s\\n\", IPv4::default());\n    printf(\"IPv6 Object: %s\\n\", IPv6::default());\n}\n\n\nif (!function_exists($func_name)) {\n    printf(\"function {$func_name} not found\\n\");\n} else {\n    printf(\"calling {$func_name} ... \\n\");\n    $now = Util::now();\n    $func_name();\n    $cost = Util::now() - $now;\n    printf(\"done, cost: %0.5f ms\\n\", $cost);\n}"
  },
  {
    "path": "binding/python/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\n\n# PyPI configuration file\n.pypirc\n"
  },
  {
    "path": "binding/python/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n==========================================================================\nThe following license applies to the ip2region library\n--------------------------------------------------------------------------\nCopyright (c) 2015 Lionsoul<chenxin619315@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "binding/python/MANIFEST.in",
    "content": "﻿include LICENSE\ninclude ReadMe.md\ninclude search_test.py\ninclude bench_test.py\nrecursive-include tests\n"
  },
  {
    "path": "binding/python/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region python query client\n\n# Version Compatibility\n\nThis implementation is compatible with Python `>=` **`3.7`**\n\n# Usage\n\n### Install `py-ip2region`\n\n```bash\npip3 install py-ip2region\n```\n\n### About Query API\n\nThe prototype of the Query API is:\n\n```python\n# Query via string IP or binary IP (bytes type) parsed by util.parse_ip\nsearch(ip: str | bytes)\n```\n\nAn exception will be thrown if the query fails. If successful, the `region` information in string format will be returned. If the specified IP cannot be found, an empty string `\"\"` will be returned.\n\n### About IPv4 and IPv6\n\nThis xdb query client implementation supports both IPv4 and IPv6 queries. Use it as follows:\n\n```python\nimport ip2region.util as util\n\n# For IPv4: Set xdb path to the v4 xdb file, and specify the IP version as util.IPv4\ndb_path = \"../../data/ip2region_v4.xdb\"  # Or your ipv4 xdb path\nversion = util.IPv4\n\n# For IPv6: Set xdb path to the v6 xdb file, and specify the IP version as util.IPv6\ndb_path = \"../../data/ip2region_v6.xdb\"  # Or your ipv6 xdb path\nversion = util.IPv6\n\n# The IP version of the xdb specified by db_path must match the version specified by version, otherwise an error will occur during query execution\n# Note: The following demonstrations directly use the db_path and version variables\n```\n\n### File Verification\n\nIt is recommended that you proactively verify the applicability of the xdb file. Future new features may cause the current Searcher version to be incompatible with the xdb file you are using; verification can prevent unpredictable errors during runtime. You do not need to verify every time. For example, perform verification when the service starts or manually call the command to confirm version matching. Do not run verification every time a Searcher is created, as this will affect query response speed, especially in high-concurrency scenarios.\n\n```python\nimport ip2region.util as util\n\ntry:\n    util.verify_from_file(db_path)\nexcept Exception e:\n    # Applicability verification failed!!!\n    # The current query client implementation is not applicable for the xdb file specified by db_path.\n    # You should stop the service and use a suitable xdb file or upgrade to a Searcher implementation suitable for db_path.\n    print(f\"binding is not applicable for xdb file '{db_path}': {str(e)}\")\n    return\n\n# Verification passed, the current Searcher can be safely used for query operations on the xdb pointed to by db_path\n```\n\n### File-Only Query\n\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1. Use the version and db_path mentioned above to create a file-only query object\ntry:\n    searcher = xdb.new_with_file_only(version, db_path)\nexcept Exception as e:\n    print(f\"failed to new_with_file_only: {str(e)}\")\n    return\n\n\n# 2. Query, it supports both IPv4 and IPv6 addresses\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  // IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: {searcher.get_io_count()}}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\")\n\n# 3. Close resources\nsearcher.close()\n\n# Note: Each thread needs to create an independent Searcher object\n```\n\n### Caching `VectorIndex`\n\nWe can pre-load the `VectorIndex` data from the `xdb` file and cache it globally. Using the global VectorIndex cache every time a Searcher object is created can reduce a fixed IO operation, thereby accelerating queries and reducing IO pressure.\n\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1. Pre-load VectorIndex cache from db_path and use this data as a global variable for subsequent repeated use.\ntry:\n    v_index = util.load_vector_index_from_file(db_path)\nexcept Exception as e:\n    print(f\"failed to load vector index from {db_path}: {str(e)}\")\n    return\n\n# 2. Use the global v_index to create a query object with VectorIndex cache.\ntry:\n    searcher = xdb.new_with_vector_index(version, db_path, v_index)\nexcept Exception as e:\n    print(f\"failed to new_with_vector_index: {str(e)}\")\n    return\n\n\n# 3. Query; the interface is the same for both IPv4 and IPv6 addresses\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  // IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: {searcher.get_io_count()}}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\");\n\n# 4. Close resources\nsearcher.close()\n\n# Note: Each thread needs to create an independent Searcher object, but they all share the global read-only v_index cache.\n```\n\n### Caching the Entire `xdb` File\n\nWe can also pre-load the data of the entire xdb file into memory and create a query object based on this data to achieve fully memory-based queries, similar to the previous memory search.\n\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1. Load the entire xdb into memory from db_path.\ntry:\n    c_buffer = util.load_content_from_file(db_path)\nexcept Exception as e:\n    print(f\"failed to load content from {db_path}: {str(e)}\")\n    return\n\n# 2. Use the c_buffer mentioned above to create a fully memory-based query object.\ntry:\n    searcher = xdb.new_with_buffer(version, c_buffer)\nexcept Exception e:\n    print(f\"failed to new_with_buffer: {str(e)}\")\n    return\n\n# 3. Query; the interface is the same for both IPv4 and IPv6 addresses\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  # IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: 0}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\")\n        \n# 4. Close resources - This searcher object can be safely used for concurrency; close the searcher only when the entire service is going to shut down\n# searcher.close()\n\n# Note: For concurrent use, query objects created with the entire xdb data cache can be safely used for concurrency, meaning you can make this searcher object a global object for cross-thread access.\n```\n\n# Query Test\n\nYou can test queries via the `python3 search_test.py` command:\n\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py \nusage: python search_test.py [command option]\n\nip2region search test script\n\noptions:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\nFor example: Use the default data/ip2region_v4.xdb file for IPv4 query testing:\n\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py --db=../../data/ip2region_v4.xdb                       \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 188 μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 2, took: 143 μs}\n```\n\nFor example: Use the default data/ip2region_v6.xdb file for IPv6 query testing:\n\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py --db=../../data/ip2region_v6.xdb \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , ioCount: 1, took: 166 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 197 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 240 μs}\n```\n\nInput an IP to perform a query test. You can also set `cache-policy` to file/vectorIndex/content respectively to test the effects of the three different cache implementations.\n\n# bench Test\n\nBench testing can be performed via the `python3 bench_test.py` command, which ensures the `xdb` file is error-free and allows for performance evaluation:\n\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 bench_test.py                                                                                         \nusage: python bench_test.py [command option]\n\nip2region bench test script\n\noptions:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --src SRC             source ip text file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\nFor example: Perform IPv4 bench testing via the default data/ip2region_v4.xdb and data/ipv4_source.txt files:\n\n```bash\npython3 bench_test.py --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\nFor example: Perform IPv6 bench testing via the default data/ip2region_v6.xdb and data/ipv6_source.txt files:\n\n```bash\npython3 bench_test.py --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\nYou can test the effects of the three different cache implementations by setting `cache-policy` to file/vectorIndex/content respectively.\n@Note: Ensure the src file used for bench is the same source file used to generate the corresponding xdb file.\n"
  },
  {
    "path": "binding/python/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region python 查询客户端\n\n# 版本兼容\n该实现兼容 Python `>=` **`3.7`**\n\n# 使用方式\n\n### 安装 `py-ip2region`\n```bash\npip3 install py-ip2region\n```\n\n### 关于查询 API\n查询 API 的原型为：\n```python \n# 通过字符串 IP 或者 util.parse_ip 解析得到的二进制 IP (bytes类型) 进行查询\nsearch(ip: str | bytes)\n```\n如果查询出错会抛异常，查询成功则会返回字符的 `region` 信息，如果指定的 IP 查询不到则会返回空字符串 `\"\"`。\n\n### 关于 IPv4 和 IPv6\n该 xdb 查询客户端实现同时支持对 IPv4 和 IPv6 的查询，使用方式如下：\n```python\nimport ip2region.util as util\n\n# 如果是 IPv4: 设置 xdb 路径为 v4 的 xdb 文件，IP版本指定为 util.IPv4\ndb_path = \"../../data/ip2region_v4.xdb\"  # 或者你的 ipv4 xdb 的路径\nversion = util.IPv4\n\n# 如果是 IPv6: 设置 xdb 路径为 v6 的 xdb 文件，IP版本指定为 util.IPv6\ndb_path = \"../../data/ip2region_v6.xdb\"  # 或者你的 ipv6 xdb 路径\nversion = util.IPv6\n\n# db_path 指定的 xdb 的 IP 版本必须和 version 指定的一致，不然查询执行的时候会报错\n# 备注：以下演示直接使用 db_path 和 version 变量\n```\n\n### 文件验证\n建议您主动去验证 xdb 文件的适用性，因为后期的一些新功能可能会导致目前的 Searcher 版本无法适用你使用的 xdb 文件，验证可以避免运行过程中的一些不可预测的错误。 你不需要每次都去验证，例如在服务启动的时候，或者手动调用命令验证确认版本匹配即可，不要在每次创建的 Searcher 的时候运行验证，这样会影响查询的响应速度，尤其是高并发的使用场景。\n```python\nimport ip2region.util as util\n\ntry:\n    util.verify_from_file(db_path)\nexcept Exception e:\n    # 适用性验证失败！！！\n    # 当前查询客户端实现不适用于 db_path 指定的 xdb 文件的查询.\n    # 应该停止启动服务，使用合适的 xdb 文件或者升级到适合 db_path 的 Searcher 实现。\n    print(f\"binding is not applicable for xdb file '{db_path}': {str(e)}\")\n    return\n\n# 验证通过，当前使用的 Searcher 可以安全的用于对 db_path 指向的 xdb 的查询操作\n```\n\n### 完全基于文件的查询\n\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1，使用上述的 version 和 db_path 创建完全基于文件的查询对象\ntry:\n    searcher = xdb.new_with_file_only(version, db_path)\nexcept Exception as e:\n    print(f\"failed to new_with_file_only: {str(e)}\")\n    return\n\n\n# 2、查询，IPv4 或者 IPv6 的地址都是同一个接口\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  // IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: {searcher.get_io_count()}}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\")\n\n# 3、关闭资源\nsearcher.close()\n\n# 备注：每个线程需要单独创建一个独立的 Searcher 对象\n```\n\n### 缓存 `VectorIndex` 索引\n\n我们可以提前从 `xdb` 文件中加载出来 `VectorIndex` 数据，然后全局缓存，每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作，从而加速查询，减少 IO 压力。\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1、从 db_path 中预先加载 VectorIndex 缓存，并且把这个得到的数据作为全局变量，后续反复使用。\ntry:\n    v_index = util.load_vector_index_from_file(db_path)\nexcept Exception as e:\n    print(f\"failed to load vector index from {db_path}: {str(e)}\")\n    return\n\n# 2、使用全局的 v_index 创建带 VectorIndex 缓存的查询对象。\ntry:\n    searcher = xdb.new_with_vector_index(version, db_path, v_index)\nexcept Exception as e:\n    print(f\"failed to new_with_vector_index: {str(e))}\")\n    return\n\n\n# 3、查询，IPv4 或者 IPv6 的地址都是同一个接口\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  // IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: {searcher.get_io_count()}}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\");\n\n# 4、关闭资源\nsearcher.close()\n\n# 备注：每个线程需要单独创建一个独立的 Searcher 对象，但是都共享全局的只读 v_index 缓存。\n```\n\n### 缓存整个 `xdb` 文件\n\n我们也可以预先加载整个 xdb 文件的数据到内存，然后基于这个数据创建查询对象来实现完全基于内存的查询，类似之前的 memory search。\n```python\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\n# 1、从 db_path 加载整个 xdb 到内存。\ntry:\n    c_buffer = util.load_content_from_file(db_path)\nexcept Exception as e:\n    print(f\"failed to load content from {db_path}: {str(e)}\")\n    return\n\n# 2、使用上述的 c_buff 创建一个完全基于内存的查询对象。\ntry:\n    searcher = xdb.new_with_buffer(version, c_buffer)\nexcept Exception e:\n    print(f\"failed to new_with_buffer: {str(e)}\")\n    return\n\n# 3、查询，IPv4 或者 IPv6 的地址都是同一个接口\nip = \"1.2.3.4\"\n# ip = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"  # IPv6\ntry:\n    region = searcher.search(ip)\n    print(f\"search({ip}): {{region: {region}, io_count: 0}}\")\nexcept Exception as e:\n    print(f\"failed to search: {str(e)}\")\n        \n# 4、关闭资源 - 该 searcher 对象可以安全用于并发，等整个服务关闭的时候再关闭 searcher\n# searcher.close()\n\n# 备注：并发使用，用整个 xdb 数据缓存创建的查询对象可以安全的用于并发，也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。\n```\n\n\n# 查询测试\n\n可以通过 `python3 search_test.py` 命令来测试查询：\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py \nusage: python search_test.py [command option]\n\nip2region search test script\n\noptions:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\n例如：使用默认的 data/ip2region_v4.xdb 文件进行 IPv4 的查询测试：\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py --db=../../data/ip2region_v4.xdb                       \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v4.xdb (IPv4, vectorIndex)\ntype 'quit' to exit\nip2region>> 1.2.3.4\n{region: Australia|Queensland|Brisbane|0|AU, ioCount: 5, took: 188 μs}\nip2region>> 113.118.113.77\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 2, took: 143 μs}\n```\n\n例如：使用默认的 data/ip2region_v6.xdb 文件进行 IPv6 的查询测试：\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 search_test.py --db=../../data/ip2region_v6.xdb \nip2region xdb searcher test program\nsource xdb: ../../data/ip2region_v6.xdb (IPv6, vectorIndex)\ntype 'quit' to exit\nip2region>> ::\n{region: , ioCount: 1, took: 166 μs}\nip2region>> 240e:3b7:3272:d8d0:db09:c067:8d59:539e\n{region: 中国|广东省|深圳市|电信|CN, ioCount: 8, took: 197 μs}\nip2region>> 2604:a840:3::a04d\n{region: United States|California|San Jose|xTom|US, ioCount: 13, took: 240 μs}\n```\n\n输入 ip 即可进行查询测试，也可以分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。\n\n\n# bench 测试\n\n可以通过 `python3 bench_test.py` 命令来进行 bench 测试，一方面确保 `xdb` 文件没有错误，一方面可以评估查询性能：\n```bash\n➜  python git:(fr_python_ipv6) ✗ python3 bench_test.py                                                                                         \nusage: python bench_test.py [command option]\n\nip2region bench test script\n\noptions:\n  -h, --help            show this help message and exit\n  --db DB               ip2region binary xdb file path\n  --src SRC             source ip text file path\n  --cache-policy CACHE_POLICY\n                        cache policy: file/vectorIndex/content, default: vectorIndex\n```\n\n例如：通过默认的 data/ip2region_v4.xdb 和 data/ipv4_source.txt 文件进行 IPv4 的 bench 测试：\n```bash\npython3 bench_test.py --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt\n```\n\n例如：通过默认的 data/ip2region_v6.xdb 和 data/ipv6_source.txt 文件进行 IPv6 的 bench 测试：\n```bash\npython3 bench_test.py --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt\n```\n\n可以通过分别设置 `cache-policy` 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。\n@Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。\n"
  },
  {
    "path": "binding/python/bench_test.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# xdb searcher bench test on 2025/10/30\n# Author Leon<chenxin619315@gmail.com>\n\nimport io\nimport sys\nimport argparse\nimport time\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\ndef create_searcher(db_path, cache_policy):\n    # open the source xdb file\n    handle = io.open(db_path, \"rb\")\n\n    # verify the xdb file\n    # @Note: do NOT call it every time you create a searcher since this will slow\n    # down the search response.\n    # @see the verify function for details.\n    util.verify(handle)\n\n    # get the ip version from header\n    header = util.load_header(handle)\n    version = util.version_from_header(header)\n    if version is None:\n        handle.close()\n        raise Exception(\"failed to get version from header\")\n\n    searcher = None\n    if cache_policy == \"file\":\n        searcher = xdb.new_with_file_only(version, db_path)\n    elif cache_policy == \"vectorIndex\":\n        v_index = util.load_vector_index(handle)\n        searcher = xdb.new_with_vector_index(version, db_path, v_index)\n    elif cache_policy == \"content\":\n        c_buffer = util.load_content(handle)\n        searcher = xdb.new_with_buffer(version, c_buffer)\n    else:\n        raise ValueError(\"invalid cache_policy `{}`\".format(cache_policy))\n\n    handle.close()\n    return searcher\n\n\ndef run_bench_test(db_path: str, src_path: str, cache_policy: str):\n    # create the searcher\n    searcher = None\n    try:\n        searcher = create_searcher(args.db, args.cache_policy)\n    except Exception as e:\n        print(\"failed to create searcher: {}\".format(str(e)))\n        return\n\n    print(\"searcher ->\", searcher)\n    # read the source lines and do the search test\n    count, total_secs = 0, 0\n    try:\n        handle = io.open(src_path, \"rb\")\n        while True:\n            line = handle.readline().decode(\"utf-8\").strip()\n            # ignore empty  or comment line\n            if len(line) < 1:\n                break\n\n            if line[0] == \"#\":\n                continue\n\n            # line splits\n            ps = line.split(\"|\", 2)\n            if len(ps) != 3:\n                raise Exception(f\"invalid ip segment line `{line}`\")\n\n            # ip parse and compare\n            start_time = time.time()\n            sip_bytes = util.parse_ip(ps[0])\n            eip_bytes = util.parse_ip(ps[1])\n            if util.ip_compare(sip_bytes, eip_bytes) > 0:\n                raise Exception(f\"start ip({ps[0]}) should not be greater than end ip({ps[1]})\")\n\n            # do the search test\n            for ip_bytes in [sip_bytes, eip_bytes]:\n                count  = count + 1\n                region = searcher.search(ip_bytes)\n                if region != ps[2]:\n                    raise Exception(f\"failed to search({util.ip_to_string(ip_bytes)}) with {region} != {ps[2]}\")\n            \n            total_secs = total_secs + (time.time() - start_time)\n\n        # resource cleanup\n        handle.close()\n    except Exception as e:\n        print(f\"bench failed: {str(e)}\")\n        return\n\n    # print the stats info\n    each_us = total_secs * 1000_000 / count\n    print(f\"Bench finished, {{cachePolicy: {cache_policy}, total: {count}, took: {total_secs:.3f} s, cost: {each_us:.0f} μs/op}}\");\n\n    # resource cleanup\n    searcher.close()\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        add_help=True,\n        prog=\"python bench_test.py\",\n        description=\"ip2region bench test script\",\n        usage=\"%(prog)s [command option]\"\n    )\n\n    # check the args\n    parser.add_argument('--db', help='ip2region binary xdb file path')\n    parser.add_argument('--src', help='source ip text file path')\n    parser.add_argument('--cache-policy', help='cache policy: file/vectorIndex/content, default: vectorIndex', default=\"vectorIndex\")\n    args = parser.parse_args()\n    if (args.db is None) or (args.src is None):\n        parser.print_help()\n        sys.exit()\n\n    # run the search test\n    run_bench_test(args.db, args.src, args.cache_policy)"
  },
  {
    "path": "binding/python/ip2region/__init__.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# xdb package"
  },
  {
    "path": "binding/python/ip2region/searcher.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# xdb searcher on 2025/10/30\n# Author Leon<chenxin619315@gmail.com>\n\nimport io\nimport ip2region.util as util\nfrom typing import Union\n\nclass Searcher(object):\n    '''\n    xdb searcher class with Both IPv4 and IPv6 supported.\n    three kinds of cache policy: file / vectorIndex / content\n    '''\n    def __init__(self, version: util.Version, \n                 db_path: str, vector_index: bytes, c_buffer: bytes):\n        self.version = version\n        self.__db_path = db_path\n        self.__io_count = 0\n        if c_buffer != None:\n            self.__handle = None\n            self.vector_index = None\n            self.c_buffer = c_buffer\n        else:\n            self.__handle = io.open(db_path, \"rb\")\n            self.vector_index = vector_index\n            self.c_buffer = None\n\n    def get_ip_version(self):\n        return self.version\n\n    def get_io_count(self):\n        return self.__io_count\n\n    def search(self, ip: Union[bytes, str]):\n        # check and parse the string ip\n        ip_bytes = None\n        if isinstance(ip, str):\n            ip_bytes = util.parse_ip(ip)\n        elif isinstance(ip, bytes):\n            ip_bytes = ip\n        else:\n            raise ValueError(\"invalid ip address `{}`\".format(ip))\n\n        # ip version check\n        if len(ip_bytes) != self.version.byte_num:\n            raise ValueError(\"invalid ip address `{}` ({} expected)\".format(\n                util.ip_to_string(ip_bytes), self.version.name))\n\n        # reset the global io_count\n        self.__io_count = 0\n\n        # located the segment index block based on the vector index\n        s_ptr, e_ptr, i0, i1 = 0, 0, ip_bytes[0], ip_bytes[1]\n        idx = i0 * util.VectorIndexCols * util.VectorIndexSize + i1 * util.VectorIndexSize\n        if self.vector_index != None:\n            s_ptr = util.le_get_uint32(self.vector_index, idx)\n            e_ptr = util.le_get_uint32(self.vector_index, idx + 4)\n        elif self.c_buffer != None:\n            offset = util.HeaderInfoLength + idx\n            s_ptr = util.le_get_uint32(self.c_buffer, offset)\n            e_ptr = util.le_get_uint32(self.c_buffer, offset + 4)\n        else:\n            buff = self.read(util.HeaderInfoLength + idx, util.VectorIndexSize)\n            s_ptr = util.le_get_uint32(buff, 0)\n            e_ptr = util.le_get_uint32(buff, 4)\n        \n        # print(\"s_ptr: {}, e_ptr: {}\".format(s_ptr, e_ptr))\n        # @Note: ptr validate, zero ptr means source data missing\n        # so we could just stop here and return an empty string.\n        if s_ptr == 0 or e_ptr == 0:\n            return \"\"\n\n        # binary search the segment index block to get the region info\n        _bytes, _d_bytes = len(ip_bytes), len(ip_bytes) << 1\n        index_size = self.version.index_size\n        d_len, d_ptr, l, h = 0, 0, int(0), int((e_ptr - s_ptr) / index_size)\n        while l <= h:\n            m = (l + h) >> 1\n            p = int(s_ptr + m * index_size)\n\n            # read the segment index\n            buff = self.read(p, index_size)\n            if self.version.ip_sub_compare(ip_bytes, buff, 0) < 0:\n                h = m - 1\n            elif self.version.ip_sub_compare(ip_bytes, buff, _bytes) > 0:\n                l = m + 1\n            else:\n                d_len = util.le_get_uint16(buff, _d_bytes)\n                d_ptr = util.le_get_uint32(buff, _d_bytes + 2)\n                break\n\n        # print(\"d_len: {}, d_ptr: {}\".format(d_len, d_ptr))\n        # empty match interception.\n        # and this could be a case.\n        if d_len == 0:\n            return \"\"\n\n        # read and return the region info\n        return self.read(d_ptr, d_len).decode(\"utf-8\")\n\n    def read(self, offset: int, length: int):\n        # check the content buffer first\n        if self.c_buffer != None:\n            return self.c_buffer[offset:offset+length]\n        \n        # load the buffer from file\n        self.__handle.seek(offset)\n        self.__io_count += 1\n        return self.__handle.read(length)\n\n    def close(self):\n        if self.__handle != None:\n            self.__handle.close()\n\n    def __str__(self):\n        return '{{\"version\": {}, \"db_path\": \"{}\", \"v_index\": {}, \"c_buffer\": {}}}'.format(\n            self.version.name,\n            self.__db_path,\n            None if self.vector_index is None else len(self.vector_index),\n            None if self.c_buffer is None else len(self.c_buffer)\n        )\n\n\n# ---\n# functions to create Searcher with different cache policy\n\ndef new_with_file_only(version: util.Version, db_path: str):\n    return Searcher(version, db_path, None, None)\n\ndef new_with_vector_index(version: util.Version, db_path: str, vector_index: bytes):\n    return Searcher(version, db_path, vector_index, None)\n\ndef new_with_buffer(version: util.Version, c_buffer: bytes):\n    return Searcher(version, None, None, c_buffer)\n"
  },
  {
    "path": "binding/python/ip2region/util.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# xdb utils on 2025/10/29\n# Author Leon<chenxin619315@gmail.com>\n\nimport io\nimport os\nimport ipaddress\nfrom typing import Callable\n\n# global constants\nXdbStructure20 = 2\nXdbStructure30 = 3\nXdbIPv4Id = 4\nXdbIPv6Id = 6\n\nHeaderInfoLength = 256\nVectorIndexRows  = 256\nVectorIndexCols  = 256\nVectorIndexSize  = 8\n# cache of VectorIndexCols × VectorIndexRows × VectorIndexSize\nVectorIndexLength = 524288\n\nclass Header(object):\n    def __init__(self, buff: bytes):\n        self.version = le_get_uint16(buff, 0)\n        self.indexPolicy = le_get_uint16(buff, 2)\n        self.createdAt = le_get_uint32(buff, 4)\n        self.startIndexPtr = le_get_uint32(buff, 8)\n        self.endIndexPtr = le_get_uint32(buff, 12)\n\n        # since IPv6 supporting\n        self.ipVersion = le_get_uint16(buff, 16)\n        self.runtimePtrBytes = le_get_uint16(buff, 18)\n\n\n        # keep the raw data\n        self.buff = buff\n\n    def __str__(self):\n        return '''{{\n    \"version\": {},\n    \"indexPolicy\": {},\n    \"createdAt\": {},\n    \"startIndexPtr\": {},\n    \"endIndexPtr\": {},\n    \"ipVersion\": {},\n    \"runtimePtrBytes\": {}\n}}'''.format(\n        self.version, \n        self.indexPolicy, \n        self.createdAt, \n        self.startIndexPtr, \n        self.endIndexPtr, \n        self.ipVersion, \n        self.runtimePtrBytes\n    )\n\n\n# ---\n# ip parse and convert functions\n\ndef parse_ip(ip_string: str):\n    try:\n        return ipaddress.ip_address(ip_string).packed\n    except:\n        raise ValueError(\"invalid ip address `{}`\".format(ip_string))\n\ndef ip_to_string(ip_bytes: bytes):\n    if isinstance(ip_bytes, bytes):\n        return str(ipaddress.ip_address(ip_bytes))\n    else:\n        raise ValueError(\"invalid bytes ip `{}`\".format(ip_bytes))\n\ndef ip_compare(ip1: bytes, ip2: bytes):\n    if ip1 > ip2:\n        return 1\n    elif ip1 < ip2:\n        return -1\n    else:\n        return 0\n\ndef ip_sub_compare(ip1: bytes, buff: bytes, offset: int):\n    ip2 = buff[offset:offset+len(ip1)]\n    if ip1 > ip2:\n        return 1\n    elif ip1 < ip2:\n        return -1\n    else:\n        return 0\n\n\n# ---\n# ip version class and functions\n\nclass Version(object):\n    def __init__(self, id: int, name: str, byte_num: int, index_size: int, ip_compare: Callable[[bytes, bytes, int], int]):\n        self.id = id\n        self.name = name\n        self.byte_num = byte_num\n        self.index_size = index_size\n        self.ip_compare = ip_compare\n\n    def ip_compare(self, ip1: bytes, ip2: bytes):\n        return self.ip_compare(ip1, ip2, 0)\n\n    def ip_sub_compare(self, ip1: bytes, buff: bytes, offset: int):\n        return self.ip_compare(ip1, buff, offset)\n\n    def __str__(self):\n        return '{{\"id\": {}, \"name\": \"{}\", \"bytes\": {}, \"index_size\": {}}}'.format(\n            self.id, \n            self.name, \n            self.byte_num, \n            self.index_size\n        )\n\ndef _v4_sub_compare(ip1: bytes, buff: bytes, offset: int):\n    # ip1: Big endian byte order parsed from input\n    # ip2: Little endian byte order read from xdb index.\n    # @Note: to compatible with the old Litten endian index encode implementation.\n    j = offset + len(ip1) - 1\n    for i in range(len(ip1)):\n        i1 = ip1[i]\n        i2 = buff[j]\n        if i1 < i2:\n            return -1\n        \n        if i1 > i2:\n            return 1\n\n        # increase the j\n        j = j - 1\n\n    return 0\n\n\n# ---\n# IPv4 and IPv6 version constants\n# 14 = 4 + 4 + 2 + 4\nIPv4 = Version(XdbIPv4Id, \"IPv4\", 4, 14, _v4_sub_compare)\n# 38 = 16 + 16 + 2 + 4\nIPv6 = Version(XdbIPv6Id, \"IPv6\", 16, 38, ip_sub_compare)\n\ndef version_from_name(name: str):\n    u_name = name.upper()\n    if u_name == \"IPV4\" or u_name == \"V4\":\n        return IPv4\n    elif u_name == \"IPV6\" or u_name == \"V6\":\n        return IPv6\n    else:\n        return None\n\ndef version_from_header(header: bytes):\n    # old xdb 2.0 with IPv4 supports ONLY\n    if header.version < XdbStructure30:\n        return IPv4\n\n    # xdb 3.0 or later version\n    ip_version = header.ipVersion\n    if ip_version == XdbIPv4Id:\n        return IPv4\n    elif ip_version == XdbIPv6Id:\n        return IPv6\n    else:\n        return None\n\n\n# ---\n# buffer decode functions\n\ndef le_get_uint32(buff: bytes, offset: int):\n    '''\n    decode an unsinged 4-bytes int from a buffer started from offset\n    with little byte endian\n    '''\n    return (\n        ((buff[offset  ]) & 0x000000FF) |\n        ((buff[offset+1] <<  8) & 0x0000FF00) | \n        ((buff[offset+2] << 16) & 0x00FF0000) |\n        ((buff[offset+3] << 24) & 0xFF000000)\n    )\n\ndef le_get_uint16(buff: bytes, offset: int):\n    '''\n    decode an unsinged 2-bytes short from a buffer started from offset\n    with little byte endian\n    '''\n    return (\n        ((buff[offset  ]) & 0x000000FF) |\n        ((buff[offset+1] <<  8) & 0x0000FF00)\n    )\n\n\n# ---\n# xdb buffer load functions\n\ndef load_header(handle):\n    '''\n    load xdb header from a specified file handle\n    '''\n    handle.seek(0)\n    return Header(handle.read(HeaderInfoLength))\n\ndef load_header_from_file(db_file: str):\n    handle = io.open(db_file, \"rb\")\n    header = load_header(handle)\n    handle.close()\n    return header\n\ndef load_vector_index(handle):\n    '''\n    load xdb vector index from a specified file handle\n    '''\n    handle.seek(HeaderInfoLength)\n    return handle.read(VectorIndexLength)\n\ndef load_vector_index_from_file(db_file: str):\n    handle = io.open(db_file, \"rb\")\n    v_index = load_vector_index(handle)\n    handle.close()\n    return v_index\n\ndef load_content(handle):\n    '''\n    load the whole xdb content from a specified file handle\n    '''\n    handle.seek(0)\n    return handle.read()\n\ndef load_content_from_file(db_file: str):\n    handle = io.open(db_file, \"rb\")\n    c_buff = load_content(handle)\n    handle.close()\n    return c_buff\n\n# ---\n# Verify if the current Searcher could be used to search the specified xdb file.\n# Why do we need this check ?\n# The future features of the xdb impl may cause the current searcher not able to work properly.\n# \n# @Note: You Just need to check this ONCE when the service starts\n# Or use another process (eg, A command) to check once Just to confirm the suitability.\ndef verify(handle):\n    header = load_header(handle)\n\n    # get the runtime ptr bytes\n    runtime_ptr_bytes = 0\n    if header.version == XdbStructure20:\n        runtime_ptr_bytes = 4\n    elif header.version == XdbStructure30:\n        runtime_ptr_bytes = header.runtimePtrBytes\n    else:\n        # Higher versions of the structure are usually incompatible.\n        raise ValueError(\"invalid structure version {}\".format(header.version))\n\n    # 1, confirm the xdb file size\n    # to ensure that the maximum file pointer does not overflow\n    max_file_ptr = (1 << (runtime_ptr_bytes * 8)) - 1\n    __file_bytes = os.stat(handle.fileno()).st_size\n    # print(\"max_file_ptr: {}, file_bytes: {}\".format(max_file_ptr, __file_bytes))\n    if __file_bytes > max_file_ptr:\n        raise Exception(\"xdb file exceeds the maximum supported bytes: {}\".format(max_file_ptr))\n\ndef verify_from_file(db_file: str):\n    handle = io.open(db_file, \"rb\")\n    verify(handle)\n    handle.close()"
  },
  {
    "path": "binding/python/search_test.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# xdb searcher test on 2025/10/30\n# Author Leon<chenxin619315@gmail.com>\n\nimport io\nimport sys\nimport argparse\nimport time\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\ndef create_searcher(db_path, cache_policy):\n    # open the source xdb file\n    handle = io.open(db_path, \"rb\")\n\n    # verify the xdb file\n    # @Note: do NOT call it every time you create a searcher since this will slow\n    # down the search response.\n    # @see the verify function for details.\n    util.verify(handle)\n\n    # get the ip version from header\n    header = util.load_header(handle)\n    version = util.version_from_header(header)\n    if version is None:\n        handle.close()\n        raise Exception(\"failed to get version from header\")\n\n    searcher = None\n    if cache_policy == \"file\":\n        searcher = xdb.new_with_file_only(version, db_path)\n    elif cache_policy == \"vectorIndex\":\n        v_index = util.load_vector_index(handle)\n        searcher = xdb.new_with_vector_index(version, db_path, v_index)\n    elif cache_policy == \"content\":\n        c_buffer = util.load_content(handle)\n        searcher = xdb.new_with_buffer(version, c_buffer)\n    else:\n        raise ValueError(\"invalid cache_policy `{}`\".format(cache_policy))\n\n    handle.close()\n    return searcher\n\n\ndef run_search_test(db_path: str, cache_policy: str):\n    # create the searcher\n    searcher = None\n    try:\n        searcher = create_searcher(args.db, args.cache_policy)\n    except Exception as e:\n        print(\"failed to create searcher: {}\".format(str(e)))\n        return\n\n    # print the searcher for debug\n    # print(\"searcher -> \", searcher)\n    print('''ip2region xdb searcher test program\nsource xdb: {} ({}, {})\ntype 'quit' to exit'''.format(db_path, searcher.get_ip_version().name, cache_policy))\n\n    # get input ip address and do the search\n    while True:\n        ip_str = input(\"ip2region>> \").strip()\n\n        if len(ip_str) < 2:\n            continue\n        if ip_str == \"quit\":\n            break\n\n        s_time = time.time()\n        try:\n            ip_bytes = util.parse_ip(ip_str)\n        except Exception as e:\n            print(f\"invalid ip address `{ip_str}`\")\n            continue\n        \n        try:\n            region = searcher.search(ip_bytes)\n        except Exception as e:\n            print(\"failed to search({}): {}\".format(util.ip_to_string(ip_bytes), str(e)))\n            continue\n\n        took = (time.time() - s_time) * 1000_000\n        print(f\"{{region: {region}, ioCount: {searcher.get_io_count()}, took: {took:.0f} μs}}\");\n\n    # close the searcher\n    searcher.close()\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        add_help=True,\n        prog=\"python search_test.py\",\n        description=\"ip2region search test script\",\n        usage=\"%(prog)s [command option]\"\n    )\n\n    # check the args\n    parser.add_argument('--db', help='ip2region binary xdb file path')\n    parser.add_argument('--cache-policy', help='cache policy: file/vectorIndex/content, default: vectorIndex', default=\"vectorIndex\")\n    args = parser.parse_args()\n    if args.db is None:\n        parser.print_help()\n        sys.exit()\n\n    # run the search test\n    run_search_test(args.db, args.cache_policy)"
  },
  {
    "path": "binding/python/setup.py",
    "content": "﻿import setuptools\n\nsetuptools.setup(\n    name=\"py-ip2region\",\n    version=\"3.0.4\",\n    description=\"ip2region official python binding with both IPv4 and IPv6 supported\",\n    long_description=open(\"README.md\", encoding='utf-8').read(),\n    long_description_content_type='text/markdown',\n\n    url=\"https://github.com/lionsoul2014/ip2region\",\n    license=\"Apache-2.0 License\",\n    author_email=\"chenxin619315@gmail.com\",\n    author=\"lionsoul2014\",\n\n    packages=setuptools.find_packages(),\n    include_package_data=True,\n    install_requires=[],\n    classifiers=(\n        \"Programming Language :: Python :: 3\",\n        \"Operating System :: OS Independent\",\n    ),\n    keywords=(\n        \"ip2region\",\n        \"ip-address\",\n        \"ip-region\",\n        \"ip-location\",\n        \"ip-lookup\",\n        \"ip-search\",\n        \"ipv4-address\",\n        \"ipv4-region\",\n        \"ipv4-location\",\n        \"ipv4-lookup\",\n        \"ipv4-search\",\n        \"ipv6-address\",\n        \"ipv6-region\",\n        \"ipv6-location\",\n        \"ipv6-lookup\",\n        \"ipv6-search\"\n    )\n)\n"
  },
  {
    "path": "binding/python/util_test.py",
    "content": "﻿# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n\n# util test script on 2025/10/29\n# Author Leon<chenxin619315@gmail.com>\n\nimport os\nimport sys\nimport time\nimport ip2region.util as util\nimport ip2region.searcher as xdb\n\nscript_dir = os.path.dirname(__file__)\ndata_dir = os.path.join(script_dir, '../../data/')\nxdb_v4_path = os.path.join(data_dir, \"ip2region_v4.xdb\")\nxdb_v6_path = os.path.join(data_dir, \"ip2region_v6.xdb\")\n\n# print(script_dir, data_dir, xdb_v4_path, xdb_v6_path)\ndef test_version():\n    print(\"1, version contants: \")\n    print(\"IPv4 -> \", util.IPv4)\n    print(\"IPv6 -> \", util.IPv6)\n\n    # version from name\n    print(\"2, version from name: \")\n    for name in [\"v4\", \"IPv4\", \"v4x\", \"v6\", \"IPv6\", \"v6x\"]:\n        print(\"version_from_name({}) -> \".format(name), util.version_from_name(name))\n\n    # version from header\n    print(\"3, version from header: \")\n    v4_header = util.load_header_from_file(xdb_v4_path)\n    v6_header = util.load_header_from_file(xdb_v6_path)\n    print(\"version_from_header(v4_header) -> \", util.version_from_header(v4_header))\n    print(\"version_from_header(v6_header) -> \", util.version_from_header(v6_header))\n\ndef test_verify():\n    # v4 xdb verify\n    try:\n        util.verify_from_file(xdb_v4_path)\n    except Exception as e:\n        print(\"failed to verify the xdb file `{}`: {}\".format(xdb_v4_path, str(e)))\n    else:\n        print(\"xdb file `{}` verified\".format(xdb_v4_path))\n\n    # v6 xdb verify\n    try:\n        util.verify_from_file(xdb_v6_path)\n    except Exception as e:\n        print(\"failed to verify the xdb file `{}`: {}\".format(xdb_v6_path, str(e)))\n    else:\n        print(\"xdb file `{}` verified\".format(xdb_v6_path))\n\ndef test_load_header():\n    v4_header = util.load_header_from_file(xdb_v4_path)\n    v6_header = util.load_header_from_file(xdb_v6_path)\n    print(\"v4_header -> \", v4_header)\n    print(\"v6_header -> \", v6_header)\n\ndef test_load_vector_index():\n    v4_v_index = util.load_vector_index_from_file(xdb_v4_path)\n    v6_v_index = util.load_vector_index_from_file(xdb_v6_path)\n    print(\"v4_v_index.length={}\".format(len(v4_v_index)))\n    print(\"v6_v_index.length={}\".format(len(v6_v_index)))\n\ndef test_load_content():\n    v4_content = util.load_content_from_file(xdb_v4_path)\n    v6_content = util.load_content_from_file(xdb_v6_path)\n    print(\"v4_content.length={}\".format(len(v4_content)))\n    print(\"v6_content.length={}\".format(len(v6_content)))\n\ndef test_parse_ip():\n    ip_list = [\n        \"1.0.0.0\", \"58.251.30.115\", \"192.168.1.100\", \"126.255.32.255\", \"219.xx.xx.11\", \n        \"::\", \"::1\", \"fffe::\", \"2c0f:fff0::\", \"2c0f:fff0::1\", \"2a02:26f7:c409:4001::\",\n        \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"240e:982:e617:ffff:ffff:ffff:ffff:ffff\", \"::xx:ffff\"\n    ]\n    for ip in ip_list:\n        try :\n            ip_bytes  = util.parse_ip(ip)\n            ip_string = util.ip_to_string(ip_bytes)\n            print(\"parse_ip({}) -> {{addr:{}, equal:{}}}\".format(ip, ip_string, ip_string == ip))\n        except ValueError as e:\n            print(\"failed to parse ip `{}`: {}\".format(ip, e))\n\ndef test_ip_compare():\n    ip_list = [\n        [\"1.0.0.0\", \"1.0.0.1\", -1],\n        [\"192.168.1.101\", \"192.168.1.90\", 1],\n        [\"219.133.111.87\", \"114.114.114.114\", 1],\n        [\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\", -1],\n        [\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\", 1]\n    ]\n    \n    for ip_pair in ip_list:\n        ip1 = util.parse_ip(ip_pair[0])\n        ip2 = util.parse_ip(ip_pair[1])\n        cmp = util.ip_compare(ip1, ip2)\n        print(\"compare({}, {}) -> {} ? {}\".format(util.ip_to_string(ip1), util.ip_to_string(ip2), cmp, cmp == ip_pair[2]))\n\ndef _get_searcher_list(version: util.Version):\n    db_path  = xdb_v4_path if version.id == util.XdbIPv4Id else xdb_v6_path\n    return [\n        [\"new_with_file_only\", lambda: xdb.new_with_file_only(version, db_path)],\n        [\"new_with_vector_index\", lambda: xdb.new_with_vector_index(version, db_path, util.load_vector_index_from_file(db_path))],\n        [\"new_with_buffer\", lambda: xdb.new_with_buffer(version, util.load_content_from_file(db_path))]\n    ]\n\ndef test_ip_search():\n    # ipv4 search test\n    print(\"---ipv4 search test:\")\n    ip_str = \"120.229.45.92\"\n    s_list = _get_searcher_list(util.IPv4)\n    try:\n        b_region = None\n        for meta in s_list:\n            searcher = meta[1]()\n            region = searcher.search(ip_str)\n            if b_region != None:\n                assert b_region == region, f\"region and b_region is not the same\"\n            print(f\"{meta[0]}.search({ip_str}): {region}\")\n\n            # searcher close\n            searcher.close()\n    except Exception as e:\n        print(f\"failed to search({ip_str}): {str(e)}\")\n\n    # ipv6 search test\n    print(\"---ipv6 search test:\")\n    ip_str = \"240e:3b7:3272:d8d0:db09:c067:8d59:539e\"\n    s_list = _get_searcher_list(util.IPv6)\n    try:\n        b_region = None\n        for meta in s_list:\n            searcher = meta[1]()\n            region = searcher.search(ip_str)\n            if b_region != None:\n                assert b_region == region, f\"region and b_region is not the same\"\n            print(f\"{meta[0]}.search({ip_str}): {region}\")\n\n            # searcher close\n            searcher.close()\n    except Exception as e:\n        print(f\"failed to search({ip_str}): {str(e)}\")\n\nif __name__ == \"__main__\":\n    # check and call the specified function\n    if len(sys.argv) < 2:\n        sys.exit(\"please specified the function to test\")\n    \n    func = sys.argv[1]\n    all_ids = globals()\n    if func in all_ids and callable(all_ids[func]):\n        print(\"+---calling test function {} ...\".format(func))\n        s_time = time.time()\n        all_ids[func]()\n        c_time = time.time() - s_time\n        print(f\"|---Done, elapsed {c_time:.6f}s\")\n    else:\n        sys.exit(\"unable to call function {}\".format(func))\n"
  },
  {
    "path": "binding/rust/Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\"example\", \"ip2region\"]\n"
  },
  {
    "path": "binding/rust/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n## `ip2region rust` Query Client\n\n## Features\n\n* Supports queries using both `ip` strings and `u32`/`u28` numeric types\n* Supports IPv4 and IPv6\n* Supports three modes: No Cache, Vector Index Cache, and Full Data Cache\n\n## Cache Policy Comparison and Description\n\n| Cache Mode | IPv4 Memory Usage | IPv6 Memory Usage | IPv4 benchmark query time | IPv6 benchmark query time |\n| --- | --- | --- | --- | --- |\n| No Cache | 1-2MB | 1-2MB | 54 us | 122us |\n| vector index | 1-2MB | 1-2MB | 27 us | 100us |\n| Full Cache | 20 MB | 200 MB | 120 ns | 178 ns |\n\n* During the initialization of `ip2region::Searcher`, an IO operation occurs to read the `xdb` header information to initialize the `Searcher`. The header information mainly includes the IP version of the `xdb`. This operation does not affect the performance or time consumption of subsequent IP queries and occupies approximately 20 additional bytes of memory.\n* In No Cache mode and `vector index` cache mode, all `xdb` IO reads are performed on-demand (based on bytes offset and bytes length) for small amounts of information. Both are thread-safe, as verified by benchmark testing.\n* In Full Cache mode, the `xdb` file is read and loaded into memory at once. Testing shows the `IPv6 xdb` file occupies about 200MB of memory. If queries are infrequent, memory usage will gradually decrease.\n* In all cache modes, including during the initialization of `ip2region::Searcher`, the program is thread-safe. There are no globally modifiable intermediate variables. After `ip2region::Searcher` initialization is complete, calling the `search` function uses immutable references. Meanwhile, `ip2region::Searcher` can also be passed to different threads using `Arc`.\n\n## Usage\n\nCreate a new project using `cargo`, such as `cargo new ip-test`\n\nConfigure `[dependencies]` in `Cargo.toml` as follows:\n\n```toml\n[dependencies]\nip2region = { git = \"https://github.com/lionsoul2014/ip2region.git\", branch = \"master\" }\n\n```\n\n### Basic Usage Example\n\nWrite `main.rs`\n\n```rust\nuse ip2region::{CachePolicy, Searcher};\n\nfn main() {\n    for cache_policy in [\n        CachePolicy::NoCache,\n        CachePolicy::FullMemory,\n        CachePolicy::VectorIndex,\n    ] {\n        // Create an IPv4 searcher\n        let ipv4_seacher = Searcher::new(\"../ip2region/data/ip2region_v4.xdb\".to_owned(), cache_policy).unwrap();\n        for ip in [1_u32, 2, 3] {\n            let result = ipv4_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        for ip in [\"1.1.1.1\", \"2.2.2.2\"] {\n            let result = ipv4_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        // Create an IPv6 searcher\n        let ipv6_seacher = Searcher::new(\"../ip2region/data/ip2region_v6.xdb\".to_owned(), cache_policy).unwrap();\n        for ip in [\"2001::\", \"2001:4:112::\"] {\n            let result = ipv6_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        for ip in [1_u128, 2, 3<<125] {\n            let result = ipv6_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n    }\n}\n```\n\n## Cache policy benchmark\n\n```bash\n$ cd binding/rust/ip2region\n$ cargo test\n$ cargo bench\n\n// --snip---\nipv4_no_memory_bench    time:   [54.699 µs 57.401 µs 61.062 µs]\nFound 16 outliers among 100 measurements (16.00%)\n  10 (10.00%) high mild\n  6 (6.00%) high severe\n\nipv4_vector_index_cache_bench\n                        time:   [25.972 µs 26.151 µs 26.360 µs]\nFound 9 outliers among 100 measurements (9.00%)\n  1 (1.00%) low severe\n  6 (6.00%) high mild\n  2 (2.00%) high severe\n\nipv4_full_memory_cache_bench\n                        time:   [132.04 ns 139.48 ns 149.20 ns]\nFound 10 outliers among 100 measurements (10.00%)\n  4 (4.00%) high mild\n  6 (6.00%) high severe\n\nipv6_no_memory_bench    time:   [121.00 µs 122.14 µs 123.40 µs]\nFound 5 outliers among 100 measurements (5.00%)\n  2 (2.00%) high mild\n  3 (3.00%) high severe\n\nipv6_vector_index_cache_bench\n                        time:   [96.830 µs 100.23 µs 104.81 µs]\nFound 8 outliers among 100 measurements (8.00%)\n  2 (2.00%) high mild\n  6 (6.00%) high severe\n\nipv6_full_memory_cache_bench\n                        time:   [175.29 ns 178.82 ns 183.77 ns]\nFound 6 outliers among 100 measurements (6.00%)\n  2 (2.00%) high mild\n  4 (4.00%) high severe\n// --snip--\n```\n\n## Testing, Result Verification, and Benchmark\n\n```bash\n$ cd binding/rust/example\n$ cargo build -r\n```\n\nThe location of the built executable is `binding/rust/target/release/searcher`\n\nTesting IPv6 and IPv4 requires verifying query results against the contents of `ipv6_source.txt` and `ipv4_source.txt`.\n\n**The query results shown here represent data at the current time; subsequent results may differ due to updates and corrections in the IP region segments of `ip_source.txt` and `xdb` binary data.**\n\n#### Test IPv6\n\n```bash\n$ cd binding/rust\n$ cargo build -r\n$ ./target/release/searcher --xdb='../../data/ip2region_v6.xdb' query\nip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit\nip2region>> ::\nregion: Ok(\"\"), took: 79.651412ms\nip2region>> 240e:3b7:3273:51d0:cd38:8ae1:e3c0:b708\nregion: Ok(\"中国|广东省|深圳市|电信|CN\"), took: 7.575µs\nip2region>> 2001::\nregion: Ok(\"0|0|Reserved|Reserved|Reserved\"), took: 7.256µs\nip2region>> 2001:268:9a02:8888::\nregion: Ok(\"Japan|Aichi|Nagoya|KDDI CORPORATION|JP\"), took: 7.921µs\nip2region>> 2a02:26f7:b408:a6c2::\nregion: Ok(\"United States|Virginia|Emporia|Akamai Technologies, Inc.|US\"), took: 8.461µs\nip2region>> 2c99::\nregion: Ok(\"0|0|Reserved|Reserved|Reserved\"), took: 5.33µs\n```\n\n#### Test IPv4\n\n```bash\n$ cd binding/rust\n$ cargo build -r\n$  ./target/release/searcher --xdb='../../data/ip2region_v4.xdb' query\nip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit\nip2region>> 1.2.3.4\nregion: Ok(\"Australia|Queensland|Brisbane|0|AU\"), took: 6.07µs\nip2region>> 1.1.2.1\nregion: Ok(\"中国|福建省|福州市|0|CN\"), took: 5.653µs\nip2region>> 2.2.21.1\nregion: Ok(\"United States|Texas|0|Oracle Svenska AB|US\"), took: 4.556µs\n```\n\n#### Benchmark and Result Verification\n\nTest performance via the `searcher` program while comparing query results against `ip sources` files to check for errors.\n\n```bash\n$ cd binding/rust/example\n$ cargo build -r\n## Perform IPv4 bench test using data/ip2region_v4.xdb and data/ipv4_source.txt:\n$ RUST_LOG=debug ../target/release/searcher --xdb='../../../data/ip2region_v4.xdb' bench '../../../data/ipv4_source.txt'\n2025-09-24T07:02:07.840535Z DEBUG ip2region::searcher: Load xdb file with header header=Header { version: 3, index_policy: VectorIndex, create_time: 1757125456, start_index_ptr: 955933, end_index_ptr: 11042415, ip_version: V4, runtime_ptr_bytes: 4 }\n2025-09-24T07:02:07.840894Z DEBUG ip2region::searcher: Load vector index cache\n2025-09-24T07:02:07.840905Z DEBUG ip2region::searcher: Load full cache filepath=\"../../../data/ip2region_v4.xdb\"\n2025-09-24T07:02:08.409990Z  INFO searcher: Benchmark finished count=3404406 took=569.388667ms avg_took=167ns\n\n## Perform IPv6 bench test using data/ip2region_v6.xdb and data/ipv6_source.txt:\n$ RUST_LOG=debug ../target/release/searcher --xdb='../../../data/ip2region_v6.xdb' bench '../../../data/ipv6_source.txt'\n2025-09-24T07:01:48.991835Z DEBUG ip2region::searcher: Load xdb file with header header=Header { version: 3, index_policy: VectorIndex, create_time: 1756970508, start_index_ptr: 6585371, end_index_ptr: 647078145, ip_version: V6, runtime_ptr_bytes: 4 }\n2025-09-24T07:01:48.992557Z DEBUG ip2region::searcher: Load vector index cache\n2025-09-24T07:01:48.992563Z DEBUG ip2region::searcher: Load full cache filepath=\"../../../data/ip2region_v6.xdb\"\n2025-09-24T07:01:59.775879Z  INFO searcher: Benchmark finished count=38335905 took=10.784124584s avg_took=281ns\n```\n"
  },
  {
    "path": "binding/rust/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n## `ip2region rust` 查询客户端\n\n## Features\n- 支持`ip`字符串和`u32`/`u28` 数字两种类型的查询\n- 支持 IPv4 和 IPv6\n- 支持无缓存，Vector 索引缓存，全部数据缓存三种模式\n\n## 缓存策略对比与说明\n| 缓存模式     | IPv4 数据内存占用 | IPv6 数据内存占用 | IPv4 benchmark 查询耗时 | IPv6 benchmark 查询耗时 |\n| ------------ | ----------- | ----------- | ------------------- |---------------------|\n| 无缓存       | 1-2MB       | 1-2MB       | 54 us               | 122us               |\n| vector index | 1-2MB       | 1-2MB       | 27 us               | 100us               |\n| 全部缓存     | 20 MB       | 200 MB      | 120 ns              | 178 ns              |\n\n- 在 `ip2region::Searcher` 初始化的时候会产生一次 IO, 读取 `xdb` 的 header 信息以初始化 `Searcher`，header 信息主要包含了 `xdb` 的 IP 版本，该操作对后续 IP 的查询不产生性能，耗时影响，多占用约 20 Byte 的内存\n- 在无缓存模式与 `vector index` 缓存模式下，所有 `xdb` 的 IO 读取都是按需（按照 bytes offset, bytes length）读取少量信息, 都是线程安全的，可以 benchmark 测试验证\n- 在全部缓存模式下，`xdb` 文件会一次读取，加载到内存中，测试 `IPv6 xdb` 文件大约占用内存 200MB 左右，查询不频繁的话，占用内存会逐渐降低\n- 所有缓存模式下，包括初始化 `ip2region::Searcher` 过程当中，程序都是线程安全的，不存在某个全局可修改的中间变量，`ip2region::Searcher` 初始化完成以后，调用函数`search`都是使用不可变引用，同时 `ip2region::Searcher` 也可以通过 `Arc` 方式传递给不同线程使用\n\n## 使用方式\n\n使用`cargo`新建一个项目，比如`cargo new ip-test`\n\n配置`Cargo.toml`的`[dependencies]`如下\n\n```toml\n[dependencies]\nip2region = { git = \"https://github.com/lionsoul2014/ip2region.git\", branch = \"master\" }\n```\n\n### 基本使用示例\n\n编写`main.rs`\n\n```rust\nuse ip2region::{CachePolicy, Searcher};\n\nfn main() {\n    for cache_policy in [\n        CachePolicy::NoCache,\n        CachePolicy::FullMemory,\n        CachePolicy::VectorIndex,\n    ] {\n        let ipv4_seacher = Searcher::new(\"../ip2region/data/ip2region_v4.xdb\".to_owned(), cache_policy).unwrap();\n        for ip in [1_u32, 2, 3] {\n            let result = ipv4_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        for ip in [\"1.1.1.1\", \"2.2.2.2\"] {\n            let result = ipv4_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        let ipv6_seacher = Searcher::new(\"../ip2region/data/ip2region_v6.xdb\".to_owned(), cache_policy).unwrap();\n        for ip in [\"2001::\", \"2001:4:112::\"] {\n            let result = ipv6_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n\n        for ip in [1_u128, 2, 3<<125] {\n            let result = ipv6_seacher.search(ip).unwrap();\n            println!(\"CachePolicy: {cache_policy:?}, IP: {ip}, Result: {result}\");\n        }\n    }\n}\n```\n\n## Cache policy benchmark\n\n```bash\n$ cd binding/rust/ip2region\n$ cargo test\n$ cargo bench\n\n// --snip---\nipv4_no_memory_bench    time:   [54.699 µs 57.401 µs 61.062 µs]\nFound 16 outliers among 100 measurements (16.00%)\n  10 (10.00%) high mild\n  6 (6.00%) high severe\n\nipv4_vector_index_cache_bench\n                        time:   [25.972 µs 26.151 µs 26.360 µs]\nFound 9 outliers among 100 measurements (9.00%)\n  1 (1.00%) low severe\n  6 (6.00%) high mild\n  2 (2.00%) high severe\n\nipv4_full_memory_cache_bench\n                        time:   [132.04 ns 139.48 ns 149.20 ns]\nFound 10 outliers among 100 measurements (10.00%)\n  4 (4.00%) high mild\n  6 (6.00%) high severe\n\nipv6_no_memory_bench    time:   [121.00 µs 122.14 µs 123.40 µs]\nFound 5 outliers among 100 measurements (5.00%)\n  2 (2.00%) high mild\n  3 (3.00%) high severe\n\nipv6_vector_index_cache_bench\n                        time:   [96.830 µs 100.23 µs 104.81 µs]\nFound 8 outliers among 100 measurements (8.00%)\n  2 (2.00%) high mild\n  6 (6.00%) high severe\n\nipv6_full_memory_cache_bench\n                        time:   [175.29 ns 178.82 ns 183.77 ns]\nFound 6 outliers among 100 measurements (6.00%)\n  2 (2.00%) high mild\n  4 (4.00%) high severe\n// --snip--\n```\n\n## 测试与结果验证，benchmark\n```bash\n$ cd binding/rust/example\n$ cargo build -r\n```\n构建的执行程序位置 `binding/rust/target/release/searcher`\n\n测试 IPv6 以及 IPv4 需要结合 ipv6_source.txt 以及 ipv4_source.txt 的内容进行查询结果校验\n\n**此处展示的查询结果只表示当前时间数据的查询，后续查询结果可能会由于 ip_source.txt 以及 xdb 二进制数据的 IP region 段更新修正导致不同**\n\n#### 测试 IPv6\n\n```bash\n$ cd binding/rust\n$ cargo build -r\n$ ./target/release/searcher --xdb='../../data/ip2region_v6.xdb' query\nip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit\nip2region>> ::\nregion: Ok(\"\"), took: 79.651412ms\nip2region>> 240e:3b7:3273:51d0:cd38:8ae1:e3c0:b708\nregion: Ok(\"中国|广东省|深圳市|电信|CN\"), took: 7.575µs\nip2region>> 2001::\nregion: Ok(\"0|0|Reserved|Reserved|Reserved\"), took: 7.256µs\nip2region>> 2001:268:9a02:8888::\nregion: Ok(\"Japan|Aichi|Nagoya|KDDI CORPORATION|JP\"), took: 7.921µs\nip2region>> 2a02:26f7:b408:a6c2::\nregion: Ok(\"United States|Virginia|Emporia|Akamai Technologies, Inc.|US\"), took: 8.461µs\nip2region>> 2c99::\nregion: Ok(\"0|0|Reserved|Reserved|Reserved\"), took: 5.33µs\n```\n\n#### 测试 IPv4\n```bash\n$ cd binding/rust\n$ cargo build -r\n$  ./target/release/searcher --xdb='../../data/ip2region_v4.xdb' query\nip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit\nip2region>> 1.2.3.4\nregion: Ok(\"Australia|Queensland|Brisbane|0|AU\"), took: 6.07µs\nip2region>> 1.1.2.1\nregion: Ok(\"中国|福建省|福州市|0|CN\"), took: 5.653µs\nip2region>> 2.2.21.1\nregion: Ok(\"United States|Texas|0|Oracle Svenska AB|US\"), took: 4.556µs\n```\n\n#### Benchmark 与验证结果\n\n通过 searcher 程序来测试性能，同时依据 ip sources 文件对比查询结果，检测是否存在错误\n\n```bash\n$ cd binding/rust/example\n$ cargo build -r\n## 通过 data/ip2region_v4.xdb 和 data/ipv4_source.txt 进行 ipv4 的 bench 测试：\n$ RUST_LOG=debug ../target/release/searcher --xdb='../../../data/ip2region_v4.xdb' bench '../../../data/ipv4_source.txt'\n2025-09-24T07:02:07.840535Z DEBUG ip2region::searcher: Load xdb file with header header=Header { version: 3, index_policy: VectorIndex, create_time: 1757125456, start_index_ptr: 955933, end_index_ptr: 11042415, ip_version: V4, runtime_ptr_bytes: 4 }\n2025-09-24T07:02:07.840894Z DEBUG ip2region::searcher: Load vector index cache\n2025-09-24T07:02:07.840905Z DEBUG ip2region::searcher: Load full cache filepath=\"../../../data/ip2region_v4.xdb\"\n2025-09-24T07:02:08.409990Z  INFO searcher: Benchmark finished count=3404406 took=569.388667ms avg_took=167ns\n\n## 通过 data/ip2region_v6.xdb 和 data/ipv6_source.txt 进行 ipv6 的 bench 测试：\n$ RUST_LOG=debug ../target/release/searcher --xdb='../../../data/ip2region_v6.xdb' bench '../../../data/ipv6_source.txt'\n2025-09-24T07:01:48.991835Z DEBUG ip2region::searcher: Load xdb file with header header=Header { version: 3, index_policy: VectorIndex, create_time: 1756970508, start_index_ptr: 6585371, end_index_ptr: 647078145, ip_version: V6, runtime_ptr_bytes: 4 }\n2025-09-24T07:01:48.992557Z DEBUG ip2region::searcher: Load vector index cache\n2025-09-24T07:01:48.992563Z DEBUG ip2region::searcher: Load full cache filepath=\"../../../data/ip2region_v6.xdb\"\n2025-09-24T07:01:59.775879Z  INFO searcher: Benchmark finished count=38335905 took=10.784124584s avg_took=281ns\n```\n"
  },
  {
    "path": "binding/rust/example/Cargo.toml",
    "content": "[package]\nname = \"searcher\"\ndefault-run = \"searcher\"\nversion = \"0.2.0\"\nedition = \"2024\"\nrust-version = \"1.89.0\"\ndescription = \"Rust binding example for ip2region\"\nlicense = \"Apache-2.0\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nip2region = { path = \"../ip2region\" }\nclap = { version = \"4.5\", features = [\"derive\", \"env\"] }\ntracing-subscriber = \"0.3\"\ntracing = \"0.1\"\n"
  },
  {
    "path": "binding/rust/example/src/cmd.rs",
    "content": "use clap::{Parser, Subcommand, ValueEnum};\n\n/// Rust binding example for ip2region\n///\n/// e.g\n///\n/// ```\n///\n/// export XDB='../../../data/ip2region_v4.xdb'  ## or export XDB='../../../data/ip2region_v6.xdb'\n///\n/// export CHECK='../../../data/ipv4_source.txt'  ## or export CHECK='../../../data/ipv6_source.txt'\n///\n/// cd binding/rust/example\n///\n/// ./searcher --xdb=$XDB bench $CHECK\n///\n/// ./searcher --xdb=$XDB query\n///\n/// ```\n#[derive(Parser)]\npub struct Command {\n    /// xdb filepath, e.g. `../../../data/ip2region_v4.xdb` or `../../../data/ip2region_v6.xdb`\n    #[arg(long, env = \"XDB\")]\n    pub xdb: String,\n    #[arg(long, value_enum, default_value_t = CmdCachePolicy::FullMemory)]\n    pub cache_policy: CmdCachePolicy,\n    #[clap(subcommand)]\n    pub action: Action,\n}\n\n#[derive(Subcommand)]\npub enum Action {\n    /// Bench the ip search and output performance info\n    Bench { check_file: String },\n    /// Interactive input and output, querying one IP and get result at a time\n    Query,\n}\n\n#[derive(Debug, PartialEq, ValueEnum, Clone, Copy, Default)]\npub enum CmdCachePolicy {\n    #[default]\n    FullMemory,\n    NoCache,\n    VectorIndex,\n}\n"
  },
  {
    "path": "binding/rust/example/src/main.rs",
    "content": "use std::fs::File;\nuse std::io::Write;\nuse std::io::{BufRead, BufReader};\nuse std::net::IpAddr;\nuse std::str::FromStr;\nuse std::time::Instant;\n\nuse clap::Parser;\nuse ip2region::{CachePolicy, Searcher};\nuse tracing::info;\n\nuse crate::cmd::{Action, CmdCachePolicy, Command};\n\nmod cmd;\n\nmacro_rules! perform_check {\n    ($searcher:expr, $start_ip:expr, $end_ip:expr, $check:expr) => {{\n        let start_ip = $start_ip;\n        let end_ip = $end_ip;\n\n        let mid_ip = (start_ip >> 1) + (end_ip >> 1);\n\n        let mut checked = 0;\n        let checks = [\n            start_ip,\n            (start_ip >> 1) + (mid_ip >> 1),\n            mid_ip,\n            (mid_ip >> 1) + (end_ip >> 1),\n            end_ip,\n        ];\n        for ip in checks.iter() {\n            if *ip < start_ip || *ip > end_ip {\n                // IP not in start - end ip\n                // This happens when start ip equals end ip\n                continue;\n            }\n            let result = $searcher.search(*ip).unwrap();\n            assert_eq!(result.as_str(), $check);\n            checked += 1;\n        }\n        checked\n    }};\n}\n\nfn check(searcher: &Searcher, start_ip: IpAddr, end_ip: IpAddr, check: &str) -> usize {\n    match (start_ip, end_ip) {\n        (IpAddr::V4(original_start_ip), IpAddr::V4(original_end_ip)) => {\n            let start_ip = u32::from(original_start_ip);\n            let end_ip = u32::from(original_end_ip);\n            perform_check!(searcher, start_ip, end_ip, check)\n        }\n        (IpAddr::V6(original_start_ip), IpAddr::V6(original_end_ip)) => {\n            let start_ip = u128::from(original_start_ip);\n            let end_ip = u128::from(original_end_ip);\n            perform_check!(searcher, start_ip, end_ip, check)\n        }\n        _ => panic!(\"invalid start ip and end ip\"),\n    }\n}\n\nfn bench(searcher: &Searcher, check_filepath: &str) {\n    let file = File::open(check_filepath).unwrap();\n    let reader = BufReader::new(file);\n\n    let now = Instant::now();\n    let mut count = 0;\n\n    for line in reader.lines().map_while(Result::ok) {\n        let ip_test_line = line.splitn(3, '|').collect::<Vec<&str>>();\n        if ip_test_line.len() == 3 {\n            let start_ip = IpAddr::from_str(ip_test_line[0]).unwrap();\n            let end_ip = IpAddr::from_str(ip_test_line[1]).unwrap();\n            count += check(searcher, start_ip, end_ip, ip_test_line[2]);\n        }\n    }\n    info!(count, took=?now.elapsed(), avg_took=?(now.elapsed() / (count as u32)), \"Benchmark finished\");\n}\n\nfn query(searcher: &Searcher) {\n    println!(\"ip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit\");\n    loop {\n        print!(\"ip2region>> \");\n        std::io::stdout().flush().unwrap();\n        let mut line = String::new();\n        std::io::stdin().read_line(&mut line).unwrap();\n        if line.contains(\"quit\") {\n            break;\n        }\n        let line = line.trim();\n        let now = Instant::now();\n        let result = searcher.search(line);\n        let cost = now.elapsed();\n        println!(\"region: {result:?}, took: {cost:?}\",);\n    }\n}\n\nfn main() {\n    tracing_subscriber::fmt::init();\n\n    let cmd = Command::parse();\n    let cache_policy = match cmd.cache_policy {\n        CmdCachePolicy::FullMemory => CachePolicy::FullMemory,\n        CmdCachePolicy::VectorIndex => CachePolicy::VectorIndex,\n        CmdCachePolicy::NoCache => CachePolicy::NoCache,\n    };\n\n    let searcher = Searcher::new(cmd.xdb, cache_policy).unwrap();\n    match cmd.action {\n        Action::Bench { check_file } => bench(&searcher, &check_file),\n        Action::Query => query(&searcher),\n    }\n}\n"
  },
  {
    "path": "binding/rust/ip2region/Cargo.toml",
    "content": "[package]\nname = \"ip2region\"\nversion = \"0.2.1\"\nedition = \"2024\"\nrust-version = \"1.89.0\"\ndescription = \"The rust binding for ip2region\"\nlicense = \"Apache-2.0\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\ntracing = \"0.1\"\nthiserror = \"2\"\nmaker = { path = \"../../../maker/rust/maker\"}\n\n[dev-dependencies]\ncriterion = \"0.7\"\nrand = \"0.9\"\n\n[[bench]]\nname = \"search\"\nharness = false\n"
  },
  {
    "path": "binding/rust/ip2region/benches/search.rs",
    "content": "use std::net::Ipv6Addr;\nuse std::ops::Range;\nuse std::str::FromStr;\n\nuse criterion::{Criterion, criterion_group, criterion_main};\n\nuse ip2region::{CachePolicy, Searcher};\n\nmacro_rules! bench_search {\n    ($name:ident, $xdb:expr, $cache_policy:expr, $range:ident) => {\n        fn $name(c: &mut Criterion) {\n            let searcher = Searcher::new($xdb.to_owned(), $cache_policy).unwrap();\n            let range = $range();\n\n            c.bench_function(stringify!($name), |b| {\n                b.iter(|| {\n                    searcher.search(rand::random_range(range.clone())).unwrap();\n                })\n            });\n        }\n    };\n}\n\nfn ipv4_range() -> Range<u32> {\n    0..((1_u64 << 32) - 1) as u32\n}\n\n/// The range of IPv6 is too large, and the value range needs to be limited to\n/// make the benchmark test results closer to the production environment\nfn ipv6_range() -> Range<u128> {\n    let start = u128::from(Ipv6Addr::from_str(\"2000::\").unwrap());\n    let end = u128::from(Ipv6Addr::from_str(\"2004::\").unwrap());\n    start..end\n}\n\nconst IPV4_XDB: &str = \"../../../data/ip2region_v4.xdb\";\nconst IPV6_XDB: &str = \"../../../data/ip2region_v6.xdb\";\n\nbench_search!(\n    ipv4_no_memory_bench,\n    IPV4_XDB,\n    CachePolicy::NoCache,\n    ipv4_range\n);\nbench_search!(\n    ipv4_vector_index_cache_bench,\n    IPV4_XDB,\n    CachePolicy::VectorIndex,\n    ipv4_range\n);\nbench_search!(\n    ipv4_full_memory_cache_bench,\n    IPV4_XDB,\n    CachePolicy::FullMemory,\n    ipv4_range\n);\nbench_search!(\n    ipv6_no_memory_bench,\n    IPV6_XDB,\n    CachePolicy::NoCache,\n    ipv6_range\n);\nbench_search!(\n    ipv6_vector_index_cache_bench,\n    IPV6_XDB,\n    CachePolicy::VectorIndex,\n    ipv6_range\n);\nbench_search!(\n    ipv6_full_memory_cache_bench,\n    IPV6_XDB,\n    CachePolicy::FullMemory,\n    ipv6_range\n);\n\ncriterion_group!(\n    benches,\n    ipv4_no_memory_bench,\n    ipv4_vector_index_cache_bench,\n    ipv4_full_memory_cache_bench,\n    ipv6_no_memory_bench,\n    ipv6_vector_index_cache_bench,\n    ipv6_full_memory_cache_bench\n);\ncriterion_main!(benches);\n"
  },
  {
    "path": "binding/rust/ip2region/src/error.rs",
    "content": "#[derive(Debug, thiserror::Error)]\npub enum Ip2RegionError {\n    #[error(\"Io error: {0}\")]\n    IoError(#[from] std::io::Error),\n\n    #[error(\"From UTF-8 error: {0}\")]\n    Utf8Error(#[from] std::string::FromUtf8Error),\n\n    #[error(\"Parse invalid IP address\")]\n    ParseIpaddressFailed,\n\n    #[error(\"No matched Ipaddress\")]\n    NoMatchedIP,\n\n    #[error(\"Searcher load IPv4 data, couldn't search IPv6 data\")]\n    OnlyIPv4Version,\n\n    #[error(\"Searcher load IPv6 data, couldn't search IPv4 data\")]\n    OnlyIPv6Version,\n\n    #[error(\"Try from slice failed\")]\n    TryFromSliceFailed(#[from] std::array::TryFromSliceError),\n\n    #[error(\"Maker crate error: {0}\")]\n    MakerError(#[from] maker::MakerError),\n}\n\npub type Result<T> = std::result::Result<T, Ip2RegionError>;\n"
  },
  {
    "path": "binding/rust/ip2region/src/ip_value.rs",
    "content": "use std::borrow::Cow;\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr};\nuse std::str::FromStr;\n\nuse crate::error::{Ip2RegionError, Result};\n\npub trait IpValueExt {\n    fn to_ipaddr(self) -> Result<IpAddr>;\n}\n\nimpl IpValueExt for &str {\n    fn to_ipaddr(self) -> Result<IpAddr> {\n        IpAddr::from_str(self).map_err(|_| Ip2RegionError::ParseIpaddressFailed)\n    }\n}\n\nimpl IpValueExt for u32 {\n    fn to_ipaddr(self) -> Result<IpAddr> {\n        Ok(IpAddr::V4(Ipv4Addr::from(self)))\n    }\n}\n\nimpl IpValueExt for Ipv4Addr {\n    fn to_ipaddr(self) -> Result<IpAddr> {\n        Ok(IpAddr::V4(self))\n    }\n}\n\nimpl IpValueExt for Ipv6Addr {\n    fn to_ipaddr(self) -> Result<IpAddr> {\n        Ok(IpAddr::V6(self))\n    }\n}\n\nimpl IpValueExt for u128 {\n    fn to_ipaddr(self) -> Result<IpAddr> {\n        Ok(IpAddr::V6(Ipv6Addr::from(self)))\n    }\n}\n\npub trait CompareExt {\n    fn ip_lt(&self, other: Cow<'_, [u8]>) -> bool;\n    fn ip_gt(&self, other: Cow<'_, [u8]>) -> bool;\n}\n\nimpl CompareExt for IpAddr {\n    fn ip_lt(&self, other: Cow<'_, [u8]>) -> bool {\n        match self {\n            IpAddr::V4(ip) => ip.octets() < [other[3], other[2], other[1], other[0]],\n            IpAddr::V6(ip) => ip.octets() < other[0..16].try_into().unwrap(),\n        }\n    }\n\n    fn ip_gt(&self, other: Cow<'_, [u8]>) -> bool {\n        match self {\n            IpAddr::V4(ip) => ip.octets() > [other[3], other[2], other[1], other[0]],\n            IpAddr::V6(ip) => ip.octets() > other[0..16].try_into().unwrap(),\n        }\n    }\n}\n"
  },
  {
    "path": "binding/rust/ip2region/src/lib.rs",
    "content": "mod error;\nmod ip_value;\nmod searcher;\n\npub use ip_value::IpValueExt;\npub use searcher::{CachePolicy, Searcher};\n"
  },
  {
    "path": "binding/rust/ip2region/src/searcher.rs",
    "content": "use std::borrow::Cow;\nuse std::fmt::Display;\nuse std::fs::File;\nuse std::io::{Read, Seek, SeekFrom};\nuse std::net::IpAddr;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\nuse maker::{\n    HEADER_INFO_LENGTH, Header, IpVersion, VECTOR_INDEX_COLS, VECTOR_INDEX_LENGTH,\n    VECTOR_INDEX_SIZE,\n};\nuse tracing::{debug, trace, warn};\n\nuse crate::error::{Ip2RegionError, Result};\nuse crate::ip_value::{CompareExt, IpValueExt};\n\npub struct Searcher {\n    pub filepath: String,\n    pub cache_policy: CachePolicy,\n    pub header: Header,\n    vector_cache: OnceLock<Vec<u8>>,\n    full_cache: OnceLock<Vec<u8>>,\n}\n\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum CachePolicy {\n    NoCache,\n    VectorIndex,\n    FullMemory,\n}\n\nimpl Searcher {\n    pub fn new(filepath: String, cache_policy: CachePolicy) -> Result<Self> {\n        let mut file = File::open(Path::new(&filepath))?;\n        let mut buf = [0; HEADER_INFO_LENGTH];\n        file.read_exact(&mut buf)?;\n\n        let header = Header::try_from(&buf)?;\n        debug!(?header, \"Load xdb file with header\");\n\n        Ok(Self {\n            filepath,\n            cache_policy,\n            header,\n            vector_cache: OnceLock::new(),\n            full_cache: OnceLock::new(),\n        })\n    }\n\n    pub fn search<T>(&self, ip: T) -> Result<String>\n    where\n        T: IpValueExt + Display,\n    {\n        let ip = ip.to_ipaddr()?;\n\n        let (il0, il1) = match (ip, self.header.ip_version()) {\n            (IpAddr::V6(ip), IpVersion::V6) => (ip.octets()[0], ip.octets()[1]),\n            (IpAddr::V4(ip), IpVersion::V4) => (ip.octets()[0], ip.octets()[1]),\n            (_, IpVersion::V4) => return Err(Ip2RegionError::OnlyIPv4Version),\n            (_, IpVersion::V6) => return Err(Ip2RegionError::OnlyIPv6Version),\n        };\n\n        let start_point = VECTOR_INDEX_SIZE * ((il0 as usize) * VECTOR_INDEX_COLS + (il1 as usize));\n        let vector_index = self.vector_index()?;\n        let start_ptr =\n            u32::from_le_bytes(vector_index[start_point..start_point + 4].try_into()?) as usize;\n        let end_ptr =\n            u32::from_le_bytes(vector_index[start_point + 4..start_point + 8].try_into()?) as usize;\n\n        // @Note: ptr validate, zero ptr means source data missing\n        // so we could just stop here and return an empty string.\n        if start_ptr == 0 || end_ptr == 0 {\n            return Ok(String::new())\n        }\n\n        // Binary search the segment index to get the region\n        let segment_index_size = self.header.segment_index_size();\n        let ip_bytes_len = self.header.ip_bytes_len();\n        let ip_end_offset = ip_bytes_len * 2;\n\n        let mut left: usize = 0;\n        let mut right: usize = (end_ptr - start_ptr) / segment_index_size;\n\n        while left <= right {\n            let mid = (left + right) >> 1;\n            let offset = start_ptr + mid * segment_index_size;\n            let buffer_ip_value = self.read_buf(offset, segment_index_size)?;\n            if ip.ip_lt(Cow::Borrowed(&buffer_ip_value[0..ip_bytes_len])) {\n                let Some(m) = mid.checked_sub(1) else {\n                    break\n                };\n                right = m;\n            } else if ip.ip_gt(Cow::Borrowed(&buffer_ip_value[ip_bytes_len..ip_end_offset])) {\n                left = mid + 1;\n            } else {\n                let data_length = u16::from_le_bytes([\n                    buffer_ip_value[ip_end_offset],\n                    buffer_ip_value[ip_end_offset + 1],\n                ]);\n                let data_offset = u32::from_le_bytes(\n                    buffer_ip_value[ip_end_offset + 2..ip_end_offset + 6].try_into()?,\n                );\n                let result = String::from_utf8(\n                    self.read_buf(data_offset as usize, data_length as usize)?\n                        .to_vec(),\n                )?;\n                return Ok(result);\n            }\n        }\n        // From xdb 3.0 version no matched IP result change to empty string,\n        // so users should check string is empty.\n        //\n        // Err(Ip2RegionError::NoMatchedIP)\n        Ok(String::new())\n    }\n\n    pub fn vector_index(&self) -> Result<Cow<'_, [u8]>> {\n        if self.cache_policy.eq(&CachePolicy::NoCache) {\n            return self.read_buf(HEADER_INFO_LENGTH, VECTOR_INDEX_LENGTH);\n        }\n\n        match self.vector_cache.get() {\n            None => {\n                debug!(\"Load vector index cache\");\n                let data = self\n                    .read_buf(HEADER_INFO_LENGTH, VECTOR_INDEX_LENGTH)?\n                    .to_vec();\n                let _ = self\n                    .vector_cache\n                    .set(data)\n                    .inspect_err(|_| warn!(\"Vector index cache already initialized\"));\n\n                // Safety: vector cache checked and set for empty before\n                let cache = self.vector_cache.get().unwrap();\n                Ok(Cow::Borrowed(cache))\n            }\n            Some(cache) => Ok(Cow::Borrowed(cache)),\n        }\n    }\n\n    pub fn read_buf(&self, offset: usize, size: usize) -> Result<Cow<'_, [u8]>> {\n        trace!(offset, size = size, \"Read buffer\");\n        if self.cache_policy.ne(&CachePolicy::FullMemory) {\n            debug!(filepath=?self.filepath, offset=offset, size=size, \"Read buf without cache\");\n            let mut file = File::open(&self.filepath)?;\n            file.seek(SeekFrom::Start(offset as u64))?;\n\n            let mut buf = vec![0u8; size];\n            file.take(size as u64).read_exact(&mut buf)?;\n            return Ok(Cow::from(buf));\n        }\n\n        match self.full_cache.get() {\n            None => {\n                debug!(filepath=?self.filepath, \"Load full cache\");\n                let mut file = File::open(&self.filepath)?;\n                let mut buf = Vec::new();\n                file.read_to_end(&mut buf)?;\n                let _ = self\n                    .full_cache\n                    .set(buf)\n                    .inspect_err(|_| warn!(\"Full cache already initialized\"));\n\n                // Safety: FULL_CACHE checked and set for empty before\n                let cache = self.full_cache.get().unwrap();\n                Ok(Cow::from(&cache[offset..offset + size]))\n            }\n            Some(cache) => {\n                let data = Cow::from(&cache[offset..offset + size]);\n                Ok(data)\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::fs::File;\n    use std::io::{BufRead, BufReader};\n    use std::str::FromStr;\n\n    use super::*;\n\n    // Test ipv6 need after run command `git lfs pull`\n    const IPV4_XDB_PATH: &str = \"../../../data/ip2region_v4.xdb\";\n    const IPV4_CHECK_PATH: &str = \"../../../data/ipv4_source.txt\";\n    const IPV6_XDB_PATH: &str = \"../../../data/ip2region_v6.xdb\";\n    const IPV6_CHECK_PATH: &str = \"../../../data/ipv6_source.txt\";\n\n    ///test all types find correct\n    #[test]\n    fn test_multi_type_ip() {\n        for cache_policy in [\n            CachePolicy::NoCache,\n            CachePolicy::FullMemory,\n            CachePolicy::VectorIndex,\n        ] {\n            let searcher = Searcher::new(IPV4_XDB_PATH.to_owned(), cache_policy).unwrap();\n            searcher.search(\"1.0.1.0\").unwrap();\n            searcher.search(\"1.0.1.2\").unwrap();\n            searcher.search(0u32).unwrap();\n\n            let searcher = Searcher::new(IPV6_XDB_PATH.to_owned(), cache_policy).unwrap();\n            searcher.search(\"2c0f:fff1::\").unwrap();\n            searcher.search(\"2c0f:fff1::1\").unwrap();\n            searcher.search(111u128).unwrap();\n        }\n    }\n\n    fn match_ip_correct(xdb_filepath: &str, check_path: &str, cache_policy: CachePolicy) {\n        let searcher = Searcher::new(xdb_filepath.to_owned(), cache_policy).unwrap();\n\n        let file = File::open(check_path).unwrap();\n        let reader = BufReader::new(file);\n\n        for line in reader.lines().take(10_000) {\n            let line = line.unwrap();\n\n            if !line.contains(\"|\") {\n                continue;\n            }\n\n            let ip_test_line = line.splitn(3, \"|\").collect::<Vec<&str>>();\n            let start_ip = IpAddr::from_str(ip_test_line[0]).unwrap();\n            let end_ip = IpAddr::from_str(ip_test_line[1]).unwrap();\n            for _ in 0..3 {\n                let result = match (start_ip, end_ip) {\n                    (IpAddr::V4(start), IpAddr::V4(end)) => {\n                        let value = rand::random_range(u32::from(start)..u32::from(end) + 1);\n                        searcher.search(value).unwrap()\n                    }\n                    (IpAddr::V6(start), IpAddr::V6(end)) => {\n                        let value = rand::random_range(u128::from(start)..u128::from(end) + 1);\n                        searcher.search(value).unwrap()\n                    }\n                    _ => panic!(\"invalid ip address\"),\n                };\n                assert_eq!(result.as_str(), ip_test_line[2])\n            }\n        }\n    }\n\n    #[test]\n    fn test_match_ip_correct() {\n        for cache_policy in [\n            CachePolicy::NoCache,\n            CachePolicy::FullMemory,\n            CachePolicy::VectorIndex,\n        ] {\n            match_ip_correct(IPV4_XDB_PATH, IPV4_CHECK_PATH, cache_policy);\n            match_ip_correct(IPV6_XDB_PATH, IPV6_CHECK_PATH, cache_policy);\n        }\n    }\n}\n"
  },
  {
    "path": "binding/typescript/README.md",
    "content": "# :cn: [中文简体]\n\n# ip2region typescript xdb 查询客户端\n\n请使用最新的 IPv6 兼容的 javascript binding：[javascript binding](../javascript/)\n\n---\n\n# :globe_with_meridians: [English]\n\n# ip2region typescript query client\n\nPlease use the latest IPv6-compliant JavaScript binding: [javascript binding](../javascript/)\n"
  },
  {
    "path": "data/sample/github-issue-196.fix",
    "content": "39.144.0.0|39.144.0.255|中国|山东省|菏泽市|移动\n39.144.1.0|39.144.9.255|中国|0|0|移动\n39.144.10.0|39.144.12.255|中国|北京|北京|移动\n39.144.13.0|39.144.16.255|中国|广东|汕头|移动\n39.144.17.0|39.144.18.255|中国|重庆|重庆|移动\n39.144.19.0|39.144.19.255|中国|北京|北京|移动\n39.144.20.0|39.144.20.255|中国|重庆|重庆|移动\n39.144.21.0|39.144.29.255|中国|河南省|0|移动\n39.144.22.0|39.144.22.255|中国|河南|郑州|移动\n39.144.23.0|39.144.24.255|中国|河南|南阳|移动\n39.144.25.0|39.144.29.255|中国|河南|0|移动\n39.144.34.0|39.144.34.255|中国|安徽省|0|移动\n39.144.37.0|39.144.37.255|中国|安徽|蚌埠|移动\n39.144.38.0|39.144.38.255|中国|安徽省|合肥市|移动\n39.144.39.0|39.144.40.255|中国|上海|上海|移动\n39.144.41.0|39.144.42.255|中国|贵州|毕节|移动\n39.144.43.0|39.144.47.255|中国|上海|上海|移动\n39.144.48.0|39.144.48.255|中国|北京|北京|移动\n39.144.49.0|39.144.49.255|中国|河北|廊坊|移动\n39.144.50.0|39.144.50.255|中国|河北|石家庄|移动\n39.144.51.0|39.144.52.255|中国|北京|北京|移动\n39.144.53.0|39.144.53.255|中国|辽宁|朝阳|移动\n39.144.54.0|39.144.54.255|中国|辽宁|盘锦|移动\n39.144.55.0|39.144.55.255|中国|辽宁|大连|移动\n39.144.56.0|39.144.56.255|中国|辽宁|本溪|移动\n39.144.57.0|39.144.58.255|中国|辽宁|沈阳|移动\n39.144.64.0|39.144.64.255|中国|广西|0|移动\n39.144.66.0|39.144.66.255|中国|广西|桂林|移动\n39.144.67.0|39.144.68.255|中国|北京|北京|移动\n39.144.99.0|39.144.99.255|中国|山西|临汾|移动\n39.144.100.0|39.144.100.255|中国|吉林|长春|移动\n39.144.137.0|39.144.137.255|中国|四川省|成都市|移动\n39.144.138.0|39.144.138.255|中国|四川省|阿坝藏族羌族自治州|移动\n39.144.145.0|39.144.145.255|中国|云南省|昆明市|移动\n39.144.147.0|39.144.147.255|中国|云南省|0|移动\n39.144.151.0|39.144.151.255|中国|江苏|淮安|移动\n39.144.153.0|39.144.153.255|中国|江苏|苏州|移动\n39.144.154.0|39.144.154.127|中国|江苏省|无锡市|移动\n39.144.154.128|39.144.154.255|中国|江苏省|常州市|移动\n39.144.169.0|39.144.169.63|中国|江西省|宜春市|移动\n39.144.169.64|39.144.169.127|中国|江西省|吉安市|移动\n39.144.169.128|39.144.169.191|中国|江西省|赣州市|移动\n39.144.169.192|39.144.169.255|中国|江西省|南昌市|移动\n39.144.177.0|39.144.177.255|中国|河南省|郑州市|移动\n39.144.179.0|39.144.182.255|中国|河南|0|移动\n39.144.218.0|39.144.222.255|中国|重庆|重庆市|移动\n39.144.219.0|39.144.219.255|中国|重庆|重庆|移动\n"
  },
  {
    "path": "data/sample/github-issue-200.fix",
    "content": "112.224.0.0|112.224.63.255|中国|山东省|济南市|联通\n112.224.64.0|112.224.64.255|中国|山东省|青岛市|联通\n112.224.65.0|112.224.65.255|中国|山东省|0|联通\n112.224.66.0|112.224.66.255|中国|山东省|青岛市|联通\n112.224.67.0|112.224.67.255|中国|山东省|0|联通\n112.224.68.0|112.224.71.255|中国|山东省|青岛市|联通\n112.224.72.0|112.224.73.255|中国|山东省|0|联通\n112.224.74.0|112.224.127.255|中国|山东省|青岛市|联通\n112.224.128.0|112.224.128.255|中国|山东省|0|联通\n112.224.129.0|112.224.130.255|中国|山东省|泰安市|联通\n112.224.131.0|112.224.132.255|中国|山东省|0|联通\n112.224.133.0|112.224.136.255|中国|山东省|烟台市|联通\n112.224.137.0|112.224.138.255|中国|山东省|潍坊市|联通\n112.224.139.0|112.224.140.255|中国|山东省|烟台市|联通\n112.224.141.0|112.224.144.255|中国|山东省|0|联通\n112.224.145.0|112.224.146.255|中国|山东省|临沂市|联通\n112.224.147.0|112.224.148.255|中国|山东省|0|联通\n112.224.149.0|112.224.152.255|中国|河北省|0|联通\n112.224.153.0|112.224.153.255|中国|山东省|青岛市|联通\n112.224.154.0|112.224.154.255|中国|山东省|0|联通\n112.224.155.0|112.224.156.255|中国|山东省|青岛市|联通\n112.224.157.0|112.224.157.255|中国|山东省|济南市|联通\n112.224.158.0|112.224.159.255|中国|山东省|临沂市|联通\n112.224.160.0|112.224.163.255|中国|山东省|0|联通\n112.224.164.0|112.224.164.255|中国|山东省|青岛市|联通\n112.224.165.0|112.224.167.255|中国|山东省|0|联通\n112.224.168.0|112.224.255.255|中国|山东省|济南市|联通\n112.225.0.0|112.226.255.255|中国|山东省|青岛市|联通\n"
  },
  {
    "path": "data/sample/github-issue-243.fix",
    "content": "36.132.128.0|36.132.147.255|中国|黑龙江省|哈尔滨市|移动\n36.132.148.0|36.132.150.255|中国|黑龙江省|齐齐哈尔市|移动\n36.132.151.0|36.132.255.255|中国|黑龙江省|哈尔滨市|移动\n36.133.0.0|36.133.11.255|中国|广东省|广州市|移动\n36.133.12.0|36.133.23.255|中国|湖南省|长沙市|移动\n36.133.24.0|36.133.35.255|中国|江苏省|南京市|移动\n36.133.36.0|36.133.47.255|中国|河南省|郑州市|移动\n36.133.48.0|36.133.59.255|中国|北京|北京市|移动\n36.133.60.0|36.133.71.255|中国|四川省|成都市|移动\n36.133.72.0|36.133.83.255|中国|上海|上海市|移动\n36.133.84.0|36.133.95.255|中国|浙江省|0|移动\n36.133.96.0|36.133.107.255|中国|陕西省|西安市|移动\n36.133.108.0|36.133.119.255|中国|重庆|重庆市|移动\n36.133.120.0|36.133.131.255|中国|山东省|济南市|移动\n36.133.132.0|36.133.143.255|中国|江苏省|苏州市|移动\n36.133.144.0|36.133.151.255|中国|广东省|广州市|移动\n36.133.152.0|36.133.159.255|中国|湖南省|长沙市|移动\n36.133.160.0|36.133.175.255|中国|江苏省|南京市|移动\n36.133.176.0|36.133.191.255|中国|广东省|广州市|移动\n36.133.192.0|36.133.199.255|中国|北京|北京市|移动\n36.133.200.0|36.133.207.255|中国|四川省|成都市|移动\n36.133.208.0|36.133.223.255|中国|河南省|0|移动\n36.133.224.0|36.133.231.255|中国|广东省|广州市|移动\n36.133.232.0|36.133.239.255|中国|重庆|重庆市|移动\n36.133.240.0|36.133.247.255|中国|北京|北京市|移动\n36.133.248.0|36.133.255.255|中国|陕西省|西安市|移动\n36.134.0.0|36.134.15.255|中国|浙江省|杭州市|移动\n36.134.16.0|36.134.23.255|中国|江苏省|南京市|移动\n36.134.24.0|36.134.31.255|中国|重庆|重庆市|移动\n36.134.32.0|36.134.39.255|中国|上海|上海市|移动\n36.134.40.0|36.134.47.255|中国|山东省|0|移动\n36.134.48.0|36.134.63.255|中国|浙江省|杭州市|移动\n36.134.64.0|36.134.65.255|中国|天津|天津市|移动\n36.134.66.0|36.134.67.255|中国|吉林省|长春市|移动\n36.134.68.0|36.134.69.255|中国|辽宁省|0|移动\n36.134.70.0|36.134.71.255|中国|福建省|福州市|移动\n36.134.72.0|36.134.73.255|中国|甘肃省|兰州市|移动\n36.134.74.0|36.134.75.255|中国|云南省|昆明市|移动\n36.134.76.0|36.134.77.255|中国|山西省|太原市|移动\n36.134.78.0|36.134.79.255|中国|湖北省|武汉市|移动\n36.134.80.0|36.134.81.255|中国|江西省|南昌市|移动\n36.134.82.0|36.134.83.255|中国|0|0|移动\n36.134.84.0|36.134.85.255|中国|安徽省|合肥市|移动\n36.134.86.0|36.134.87.255|中国|广西|南宁市|移动\n36.134.88.0|36.134.89.255|中国|内蒙古|呼和浩特市|移动\n36.142.0.0|36.142.1.255|中国|四川省|成都市|移动\n36.142.2.0|36.142.31.255|中国|甘肃省|兰州市|移动\n36.142.32.0|36.142.127.255|中国|甘肃省|0|移动\n36.142.128.0|36.142.131.255|中国|甘肃省|白银市|移动\n36.142.132.0|36.142.135.255|中国|甘肃省|张掖市|移动\n36.142.136.0|36.142.139.255|中国|甘肃省|武威市|移动\n36.142.140.0|36.142.145.255|中国|甘肃省|定西市|移动\n36.142.146.0|36.142.153.255|中国|甘肃省|天水市|移动\n36.142.154.0|36.142.157.255|中国|甘肃省|平凉市|移动\n36.142.158.0|36.142.163.255|中国|甘肃省|庆阳市|移动\n36.142.164.0|36.142.165.255|中国|甘肃省|甘南市|移动\n36.142.166.0|36.142.171.255|中国|甘肃省|陇南市|移动\n36.142.172.0|36.142.175.255|中国|甘肃省|临夏市|移动\n36.142.176.0|36.142.187.255|中国|甘肃省|兰州市|移动\n36.142.188.0|36.142.189.255|中国|甘肃省|金昌市|移动\n36.142.190.0|36.142.191.255|中国|甘肃省|嘉峪关市|移动\n36.142.192.0|36.142.195.255|中国|甘肃省|酒泉市|移动\n36.142.196.0|36.142.197.255|中国|甘肃省|白银市|移动\n36.142.198.0|36.142.199.255|中国|甘肃省|张掖市|移动\n36.142.200.0|36.142.201.255|中国|甘肃省|武威市|移动\n36.142.202.0|36.142.203.255|中国|甘肃省|定西市|移动\n36.142.204.0|36.142.205.255|中国|甘肃省|天水市|移动\n36.142.206.0|36.142.207.255|中国|甘肃省|平凉市|移动\n36.142.208.0|36.142.209.255|中国|甘肃省|庆阳市|移动\n36.142.210.0|36.142.211.255|中国|甘肃省|甘南市|移动\n36.142.212.0|36.142.213.255|中国|甘肃省|陇南市|移动\n36.142.214.0|36.142.215.255|中国|甘肃省|临夏市|移动\n36.142.216.0|36.142.217.255|中国|甘肃省|兰州市|移动\n36.142.218.0|36.142.219.255|中国|甘肃省|金昌市|移动\n36.142.220.0|36.142.221.255|中国|甘肃省|嘉峪关市|移动\n36.142.222.0|36.142.223.255|中国|甘肃省|酒泉市|移动\n36.142.224.0|36.142.255.255|中国|甘肃省|0|移动\n36.143.0.0|36.143.0.255|中国|河北省|张家口市|移动\n36.143.1.0|36.143.2.255|中国|河北省|廊坊市|移动\n36.143.3.0|36.143.4.255|中国|河北省|邯郸市|移动\n36.143.5.0|36.143.12.255|中国|河北省|张家口市|移动\n36.143.13.0|36.143.14.255|中国|河北省|保定市|移动\n36.143.15.0|36.143.17.255|中国|河北省|石家庄市|移动\n36.143.18.0|36.143.22.255|中国|河北省|唐山市|移动\n36.143.23.0|36.143.29.255|中国|河北省|保定市|移动\n36.143.30.0|36.143.31.255|中国|河北省|石家庄市|移动\n36.143.32.0|36.143.35.255|中国|河北省|保定市|移动\n36.143.36.0|36.143.39.255|中国|河北省|唐山市|移动\n36.143.40.0|36.143.43.255|中国|河北省|邯郸市|移动\n36.143.44.0|36.143.47.255|中国|河北省|保定市|移动\n36.143.48.0|36.143.51.255|中国|河北省|张家口市|移动\n36.143.52.0|36.143.55.255|中国|河北省|0|移动\n36.143.56.0|36.143.59.255|中国|河北省|邯郸市|移动\n36.143.60.0|36.143.63.255|中国|河北省|廊坊市|移动\n36.143.64.0|36.143.67.255|中国|河北省|石家庄市|移动\n36.143.68.0|36.143.71.255|中国|河北省|保定市|移动\n36.143.72.0|36.143.75.255|中国|河北省|张家口市|移动\n36.143.76.0|36.143.79.255|中国|河北省|保定市|移动\n36.143.80.0|36.143.95.255|中国|河北省|0|移动\n36.143.96.0|36.143.99.255|中国|河北省|唐山市|移动\n36.143.100.0|36.143.107.255|中国|河北省|石家庄市|移动\n36.143.108.0|36.143.115.255|中国|河北省|廊坊市|移动\n36.143.116.0|36.143.119.255|中国|河北省|唐山市|移动\n36.143.120.0|36.143.123.255|中国|河北省|张家口市|移动\n36.143.124.0|36.143.127.255|中国|河北省|保定市|移动\n36.143.128.0|36.143.255.255|中国|北京|北京市|移动\n36.148.0.0|36.148.31.255|中国|湖南省|长沙市|移动\n36.148.32.0|36.148.49.255|中国|湖南省|常德市|移动\n36.148.50.0|36.148.63.255|中国|湖南省|娄底市|移动\n36.148.64.0|36.148.79.255|中国|湖南省|株洲市|移动\n36.148.80.0|36.148.87.255|中国|湖南省|郴州市|移动\n36.148.88.0|36.148.95.255|中国|湖南省|衡阳市|移动\n36.148.96.0|36.148.103.255|中国|湖南省|怀化市|移动\n36.148.104.0|36.148.107.255|中国|湖南省|永州市|移动\n36.148.108.0|36.148.111.255|中国|湖南省|益阳市|移动\n36.148.112.0|36.148.123.255|中国|湖南省|张家界市|移动\n36.148.124.0|36.148.124.255|中国|湖南省|长沙市|移动\n36.148.125.0|36.148.127.255|中国|湖南省|湘潭市|移动\n36.148.128.0|36.148.143.255|中国|湖南省|常德市|移动\n36.148.144.0|36.148.155.255|中国|湖南省|郴州市|移动\n36.148.156.0|36.148.159.255|中国|湖南省|衡阳市|移动\n36.148.160.0|36.148.163.255|中国|湖南省|娄底市|移动\n36.148.164.0|36.148.168.255|中国|湖南省|永州市|移动\n36.148.169.0|36.148.175.255|中国|湖南省|益阳市|移动\n36.148.176.0|36.148.177.255|中国|湖南省|岳阳市|移动\n36.148.178.0|36.148.189.255|中国|湖南省|长沙市|移动\n36.148.190.0|36.148.255.255|中国|湖南省|0|移动\n36.161.0.0|36.161.15.255|中国|安徽省|六安市|移动\n36.161.16.0|36.161.31.255|中国|安徽省|蚌埠市|移动\n36.161.32.0|36.161.47.255|中国|安徽省|淮南市|移动\n36.161.48.0|36.161.63.255|中国|安徽省|铜陵市|移动\n36.161.64.0|36.161.79.255|中国|安徽省|池州市|移动\n36.161.80.0|36.161.95.255|中国|安徽省|黄山市|移动\n36.161.96.0|36.161.127.255|中国|安徽省|合肥市|移动\n36.161.128.0|36.161.131.255|中国|安徽省|淮北市|移动\n36.161.132.0|36.161.135.255|中国|安徽省|合肥市|移动\n36.161.136.0|36.161.143.255|中国|安徽省|宣城市|移动\n36.161.144.0|36.161.151.255|中国|安徽省|马鞍山市|移动\n36.161.152.0|36.161.157.255|中国|安徽省|铜陵市|移动\n36.161.158.0|36.161.159.255|中国|安徽省|合肥市|移动\n36.161.160.0|36.161.167.255|中国|安徽省|池州市|移动\n36.161.168.0|36.161.183.255|中国|安徽省|黄山市|移动\n36.161.184.0|36.161.191.255|中国|安徽省|宿州市|移动\n36.161.192.0|36.161.207.255|中国|安徽省|亳州市|移动\n36.161.208.0|36.161.223.255|中国|安徽省|六安市|移动\n36.161.224.0|36.161.231.255|中国|安徽省|宣城市|移动\n36.161.232.0|36.161.239.255|中国|安徽省|合肥市|移动\n36.161.240.0|36.161.240.255|中国|安徽省|芜湖市|移动\n36.161.241.0|36.161.247.255|中国|安徽省|安庆市|移动\n36.161.248.0|36.161.255.255|中国|安徽省|黄山市|移动\n120.231.0.0|120.231.4.255|中国|广东省|0|移动\n120.231.5.0|120.231.38.255|中国|广东省|湛江市|移动\n120.231.39.0|120.231.68.255|中国|广东省|茂名市|移动\n120.231.69.0|120.231.89.255|中国|广东省|肇庆市|移动\n120.231.90.0|120.231.94.255|中国|广东省|0|移动\n120.231.95.0|120.231.134.255|中国|广东省|中山市|移动\n120.231.135.0|120.231.141.255|中国|广东省|广州市|移动\n120.231.142.0|120.231.161.255|中国|广东省|清远市|移动\n120.231.162.0|120.231.186.255|中国|广东省|珠海市|移动\n120.231.187.0|120.231.201.255|中国|广东省|江门市|移动\n120.231.202.0|120.231.249.255|中国|广东省|深圳市|移动\n120.231.250.0|120.231.255.255|中国|广东省|清远市|移动\n"
  },
  {
    "path": "data/sample/github-issue-287.bug",
    "content": "# report by https://github.com/lionsoul2014/ip2region/issues/287\n# triggered the bug of getInt2\n193.150.116.0|193.150.116.255|俄罗斯|克拉斯诺亚尔斯克边疆区|克拉斯诺亚尔斯克|Federal State Budgetary Educational Institution of Higher Education Krasnoyarsk State Medical University named after Professor V.F. Voino-Yasenetsky of the Ministry of Health of the Russian Federation\n"
  },
  {
    "path": "data/sample/ip.test.txt",
    "content": "1.0.0.0|1.0.0.255|澳大利亚|0|0|0\n1.0.1.0|1.0.3.255|中国|福建省|福州市|电信\n1.0.4.0|1.0.7.255|澳大利亚|维多利亚|墨尔本|0\n1.0.8.0|1.0.15.255|中国|广东省|广州市|电信\n1.0.16.0|1.0.31.255|日本|0|0|0\n1.0.32.0|1.0.63.255|中国|广东省|广州市|电信\n1.0.64.0|1.0.79.255|日本|广岛县|0|0\n1.0.80.0|1.0.127.255|日本|冈山县|0|0\n1.0.128.0|1.0.128.255|泰国|清莱府|0|TOT\n1.0.129.0|1.0.132.191|泰国|曼谷|曼谷|TOT\n1.0.132.192|1.0.132.255|泰国|Nakhon-Ratchasima|0|TOT\n1.0.133.0|1.0.133.255|泰国|素攀武里府|0|TOT\n1.0.134.0|1.0.134.255|泰国|曼谷|曼谷|TOT\n1.0.135.0|1.0.135.127|泰国|华富里府|0|TOT\n1.0.135.128|1.0.135.255|泰国|素攀武里府|0|TOT\n1.0.136.0|1.0.136.255|泰国|龙仔厝府|0|TOT\n1.0.137.0|1.0.137.255|泰国|大城府|0|TOT\n1.0.138.0|1.0.143.255|泰国|曼谷|曼谷|TOT\n1.0.144.0|1.0.159.255|泰国|春蓬府|0|TOT\n1.0.160.0|1.0.162.255|泰国|洛坤府|0|TOT\n1.0.163.0|1.0.163.255|泰国|春蓬府|0|TOT\n1.0.164.0|1.0.164.63|泰国|0|0|TOT\n1.0.164.64|1.0.164.127|泰国|普吉府|0|TOT\n1.0.164.128|1.0.170.255|泰国|0|0|TOT\n1.0.171.0|1.0.175.255|泰国|攀牙府|0|TOT\n"
  },
  {
    "path": "data/sample/segments.tests",
    "content": "192.168.2.1|192.168.2.20|0|0|内网IP|办公室A\n192.168.2.21|192.168.2.30|0|0|内网IP|办公室A\n192.168.2.31|192.168.2.60|0|0|内网IP|办公室B\n192.168.2.61|192.168.2.91|0|0|内网IP|办公室B\n223.255.236.0|223.255.239.255|中国|上海|上海市|电信\n"
  },
  {
    "path": "data/sample/segments.tests.mixed",
    "content": "192.168.2.1|192.168.2.20|0|0|内网IP|办公室A\n192.168.2.21|192.168.2.30|0|0|内网IP|办公室A\n192.168.2.31|192.168.2.60|0|0|内网IP|办公室B\n192.168.2.61|192.168.2.91|0|0|内网IP|办公室B\n223.255.236.0|223.255.239.255|中国|上海|上海市|电信\n2c0f:fff1::|2c0f:ffff:ffff:ffff:ffff:ffff:ffff:ffff|毛里求斯|威廉平原区|卡特勒博尔纳|专线用户\n2e00::|2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff|德国|黑森|美因河畔法兰克福|专线用户\n3000::|fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff|瑞士|弗里堡州||专线用户\nfe00::|fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff|瑞士|弗里堡州||专线用户\nfe80::|febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff|瑞士|弗里堡州||专线用户\n"
  },
  {
    "path": "maker/c/ReadMe.md",
    "content": "# ip2region xdb c语言生成实现\n\n# 数据生成\n\n# 数据查询\n\n# bench 测试\n"
  },
  {
    "path": "maker/cpp/README.md",
    "content": "# :cn: [中文简体]\n\n1. [生成 xdb 文件](../../binding/cpp#4-生成-xdb-文件)\n2. [原始数据编辑](../../binding/cpp#5-原始数据编辑)\n\n# :globe_with_meridians: [English]\n\n1. [make xdb file](../../binding/cpp##4-generate-xdb-file)\n2. [source data editor](../../binding/cpp#5-raw-data-editing)\n"
  },
  {
    "path": "maker/csharp/.gitignore",
    "content": "*.swp\n*.*~\nproject.lock.json\n.DS_Store\n*.pyc\nnupkg/\n\n# Visual Studio Code\n.vscode\n\n# Rider\n.idea\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbuild/\nbld/\n[Bb]in/\n[Oo]bj/\n[Oo]ut/\nmsbuild.log\nmsbuild.err\nmsbuild.wrn\n\n# Visual Studio 2015\n.vs/"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/IP2RegionMaker.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net6.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/Program.cs",
    "content": "﻿using IP2RegionMaker.XDB;\nusing System.Diagnostics;\n\n\nstring srcFile = \"\", dstFile = \"\";\nIndexPolicy indexPolicy = IndexPolicy.VectorIndexPolicy;\n\nif (args.Length < 2)\n{\n    PrintHelp();\n}\n\nstring[] aliases = { \"--src\", \"--dst\", \"--index\" };\nfor (int i = 0; i < args.Length; i++)\n{\n    var arg = args[i];\n\n    var key = aliases.FirstOrDefault(x => arg.StartsWith($\"{x}=\"));\n    if (string.IsNullOrEmpty(key))\n    {\n        continue;\n    }\n\n    var value = arg.Split(\"=\", 2).LastOrDefault()?.Trim();\n\n    if (string.IsNullOrEmpty(value))\n    {\n        continue;\n    }\n\n    switch (key)\n    {\n        case \"--src\":\n            srcFile = value;\n            break;\n        case \"--dst\":\n            dstFile = value;\n            break;\n        case \"--index\":\n            var flag = Enum.TryParse<IndexPolicy>(value, out indexPolicy);\n            Console.WriteLine(\"parse policy failed {arg}\", arg);\n            break;\n    }\n}\n\nConsole.WriteLine(srcFile);\n\nif (string.IsNullOrEmpty(srcFile)||string.IsNullOrEmpty(dstFile))\n{\n    PrintHelp();\n    return;\n}\n\n\nStopwatch stopwatch = new Stopwatch();\nstopwatch.Start();\n\nMaker maker = new Maker(IndexPolicy.VectorIndexPolicy, srcFile, dstFile);\nmaker.Init();\nmaker.Build();\n\nstopwatch.Stop();\nConsole.WriteLine($\"Done, elapsed:{stopwatch.Elapsed.TotalMinutes}m\");\n\n\nvoid PrintHelp()\n{\n    Console.WriteLine($\"ip2region xdb maker\");\n    Console.WriteLine(\"dotnet IP2RegionMaker.dll [command options]\");\n    Console.WriteLine(\"--src string    source ip text file path\");\n    Console.WriteLine(\"--dst string    destination binary xdb file path\");\n}"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/Properties/PublishProfiles/FolderProfile.pubxml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nhttps://go.microsoft.com/fwlink/?LinkID=208121.\n-->\n<Project>\n  <PropertyGroup>\n    <Configuration>Release</Configuration>\n    <Platform>Any CPU</Platform>\n    <PublishDir>bin\\Release\\net6.0\\publish\\</PublishDir>\n    <PublishProtocol>FileSystem</PublishProtocol>\n    <_TargetId>Folder</_TargetId>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/XDB/IndexPolicy.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace IP2RegionMaker.XDB\n{\n    public enum IndexPolicy\n    {\n        VectorIndexPolicy = 1,\n\n        BTreeIndexPolicy = 2,\n    }\n}\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/XDB/Maker.cs",
    "content": "﻿// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Alan Lee <lzh.shap@gmail.com>\n// @Date   2022/8/8\n\n// --- Ip2Region v2.0 data structure\n//\n// +----------------+--------------------------+---------------+--------------+\n// | header space   | vector speed up index    |  data payload | block index  |\n// +----------------+--------------------------+---------------+--------------+\n// | 256 bytes      | 512 KiB (fixed)          | dynamic size  | dynamic size |\n// +----------------+--------------------------+---------------+--------------+\n//\n// 1. padding space : for header info like block index ptr, version, release date eg ... or any other temporary needs.\n// -- 2bytes: version number, different version means structure update, it fixed to 2 for now\n// -- 2bytes: index algorithm code.\n// -- 4bytes: generate unix timestamp (version)\n// -- 4bytes: index block start ptr\n// -- 4bytes: index block end ptr\n//\n//\n// 2. data block : region or whatever data info.\n// 3. segment index block : binary index block.\n// 4. vector index block  : fixed index info for block index search speed up.\n// space structure table:\n// -- 0   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 1   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 2   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- ...\n// -- 255 -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n//\n//\n// super block structure:\n// +-----------------------+----------------------+\n// | first index block ptr | last index block ptr |\n// +-----------------------+----------------------+\n//\n// data entry structure:\n// +--------------------+-----------------------+\n// | 2bytes (for desc)\t| dynamic length\t\t|\n// +--------------------+-----------------------+\n//  data length   whatever in bytes\n//\n// index entry structure\n// +------------+-----------+---------------+------------+\n// | 4bytes\t\t| 4bytes\t| 2bytes\t\t| 4 bytes    |\n// +------------+-----------+---------------+------------+\n//  start ip \t  end ip\t  data length     data ptr\n\nusing System.Text;\n\nnamespace IP2RegionMaker.XDB\n{\n    public class Maker\n    {\n        const ushort VersionNo = 2;\n        const int HeaderInfoLength = 256;\n        const int VectorIndexRows = 256;\n        const int VectorIndexCols = 256;\n        const int VectorIndexSize = 8;\n        const int SegmentIndexSize = 14;\n        const int VectorIndexLength = VectorIndexRows * VectorIndexCols * VectorIndexSize;\n\n        private readonly Stream _srcHandle;\n        private readonly Stream _dstHandle;\n        private readonly IndexPolicy _indexPolicy;\n        private readonly List<Segment> _segments;\n        private readonly Dictionary<string, uint> _regionPool;\n        private readonly byte[] _vectorIndex;\n\n        public Maker(IndexPolicy indexPolicy,string srcFile, string dstFile)\n        {\n            _indexPolicy = indexPolicy;\n\n            _srcHandle = File.Open(@srcFile, FileMode.Open);\n            _dstHandle = File.Open(@dstFile, FileMode.Create);\n            _segments = new List<Segment>();\n            _regionPool = new Dictionary<string, uint>();\n            _vectorIndex = new byte[VectorIndexLength];\n        }\n\n        ~Maker()\n        {\n            _srcHandle.Close();\n            _dstHandle.Close();\n        }\n\n        private void InitDbHeader()\n        {\n            _srcHandle.Seek(0, SeekOrigin.Begin);\n\n            var header = new byte[HeaderInfoLength];\n            BitConverter.GetBytes(VersionNo).CopyTo(header, 0);\n            BitConverter.GetBytes((ushort)_indexPolicy).CopyTo(header, 2);\n            BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeSeconds()).CopyTo(header, 4);\n            BitConverter.GetBytes((uint)0).CopyTo(header, 8);\n            BitConverter.GetBytes((uint)0).CopyTo(header, 12);\n\n            using var writer = new BinaryWriter(_dstHandle, Encoding.UTF8, true);\n            writer.Write(header);\n        }\n\n        private void LoadSegments()\n        {\n            Console.WriteLine(\"try to load the segments ... \");\n\n\n            using var reader = new StreamReader(_srcHandle);\n            while (true)\n            {\n                var line = reader.ReadLine();\n\n\n                if (line == null) break;\n\n                var seg=Util.GetSegment(line);\n\n                _segments?.Add(seg);\n            }\n\n            if (_segments!=null)\n            {\n                Util.CheckSegments(_segments);\n            }\n\n            Console.WriteLine($\"all segments loaded, length: {_segments?.Count}\");\n        }\n\n\n        public void Init()\n        {\n            InitDbHeader();\n            LoadSegments();\n        }\n\n        public void Build()\n        {\n            _dstHandle.Seek(HeaderInfoLength + VectorIndexLength, SeekOrigin.Begin);\n            using var writer = new BinaryWriter(_dstHandle, Encoding.UTF8, false);\n\n            Console.WriteLine(\"try to write the data block ... \");\n\n            foreach (var seg in _segments)\n            {\n                Console.WriteLine($\"try to write region {seg.Region}\");\n\n                if (_regionPool.TryGetValue(seg.Region, out var value))\n                {\n                    Console.WriteLine($\"--[Cached] with ptr={value}\");\n                    continue;\n                }\n\n                var region = Encoding.UTF8.GetBytes(seg.Region);\n\n                if (region.Length > 0xFFFF)\n                {\n                    throw new ArgumentException($\"too long region info `{seg.Region}`: should be less than {0xFFFF} bytes\");\n                }\n\n                var pos = _dstHandle.Seek(0, SeekOrigin.Current);\n                writer.Write(region);\n\n                _regionPool[seg.Region] = (uint)pos;\n            }\n\n            Console.WriteLine(\"try to write the segment index block ... \");\n\n            var indexBuff = new byte[SegmentIndexSize];\n            var counter = 0;\n            long startPtr = -1;\n            long endPtr = -1;\n            foreach (var seg in _segments)\n            {\n                var dataPtr = _regionPool[seg.Region];\n                if (!_regionPool.ContainsKey(seg.Region))\n                {\n                    throw new Exception($\"missing ptr cache for region `{seg.Region}`\");\n                }\n\n                var datalen = Encoding.UTF8.GetBytes(seg.Region).Length;\n\n                if (datalen < 1)\n                {\n                    throw new ArgumentNullException(nameof(seg.Region));\n                }\n\n                var segList = seg.Split();\n                Console.WriteLine($\"try to index segment({segList.Count}) {seg} ...\");\n\n                foreach (var item in segList)\n                {\n                    var pos = _dstHandle.Seek(0, SeekOrigin.Current);\n\n                    BitConverter.GetBytes(item.StartIP).CopyTo(indexBuff, 0);\n                    BitConverter.GetBytes(item.EndIP).CopyTo(indexBuff, 4);\n                    BitConverter.GetBytes((ushort)datalen).CopyTo(indexBuff, 8);\n                    BitConverter.GetBytes(dataPtr).CopyTo(indexBuff, 10);\n\n                    writer.Write(indexBuff);\n\n                    Console.WriteLine($\"|-segment index: {counter}, ptr: {pos}, segment: {seg}\");\n                    SetVectorIndex(item.StartIP, (uint)pos);\n\n                    counter++;\n\n                    if (startPtr == -1)\n                    {\n                        startPtr = pos;\n                    }\n\n                    endPtr = pos;\n                }\n            }\n\n            Console.WriteLine($\"try to write the vector index block ... \");\n\n            _dstHandle.Seek(HeaderInfoLength, SeekOrigin.Begin);\n            writer.Write(_vectorIndex);\n\n\n            Console.WriteLine(\"try to write the segment index ptr ... \");\n            BitConverter.GetBytes((uint)startPtr).CopyTo(indexBuff, 0);\n            BitConverter.GetBytes((uint)endPtr).CopyTo(indexBuff, 4);\n            _dstHandle.Seek(0, SeekOrigin.Begin);\n\n            writer.Write(indexBuff[..8]);\n\n            Console.WriteLine($\"write done, dataBlocks: {_regionPool.Count}, indexBlocks: ({_segments.Count}, {counter}), indexPtr: ({startPtr}, {endPtr})\");\n        }\n\n        private void SetVectorIndex(uint ip, uint ptr)\n        {\n            var il0 = (ip >> 24) & 0xFF;\n            var il1 = (ip >> 16) & 0xFF;\n            var idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;\n\n            ArraySegment<byte> bytes = new(_vectorIndex, (int)idx, _vectorIndex.Length - 1 - (int)idx);\n            var sPtr = BitConverter.ToUInt32(bytes);\n\n            if (sPtr == 0)\n            {\n                BitConverter.GetBytes(ptr).CopyTo(_vectorIndex, idx);\n                BitConverter.GetBytes(ptr + SegmentIndexSize).CopyTo(_vectorIndex, idx + 4);\n            }\n            else\n            {\n                BitConverter.GetBytes(ptr + SegmentIndexSize).CopyTo(_vectorIndex, idx + 4);\n            }\n        }\n\n\n    }\n\n}\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/XDB/Segment.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace IP2RegionMaker.XDB\n{\n    public class Segment\n    {\n        public uint StartIP { get; set; }\n\n        public uint EndIP { get; set; }\n\n        public string Region { get; set; }\n\n        public List<Segment> Split()\n        {\n            var tList = new List<Segment>();\n            var sByte = (StartIP >> 24) & 0xFF;\n            var eByte = (EndIP >> 24) & 0xFF;\n\n            var nSip = StartIP;\n            for (var i = sByte; i <= eByte; i++)\n            {\n                var sip = (i << 24) | (nSip & 0xFFFFFF);\n                var eip = (i << 24) | 0xFFFFFF;\n\n                if (eip < EndIP)\n                {\n                    nSip = (i + 1) << 24;\n                }\n                else\n                {\n                    eip = EndIP;\n                }\n\n                tList.Add(new Segment\n                {\n                    StartIP = sip,\n                    EndIP = eip,\n                });\n            }\n\n            var segList = new List<Segment>();\n\n            foreach (var seg in tList)\n            {\n                var temp = seg.StartIP & 0xFF000000;\n                nSip = seg.StartIP;\n\n                sByte = (seg.StartIP >> 16) & 0xFF;\n                eByte = (seg.EndIP >> 16) & 0xFF;\n\n                for (var i = sByte; i <= eByte; i++)\n                {\n                    var sip = temp | (i << 16) | (nSip & 0xFFFF);\n                    var eip = temp | (i << 16) | 0xFFFF;\n\n                    if (eip < seg.EndIP)\n                    {\n                        nSip = 0;\n                    }\n                    else\n                    {\n                        eip = seg.EndIP;\n                    }\n\n                    segList.Add(new Segment\n                    {\n                        StartIP = sip,\n                        EndIP = eip,\n                        Region = Region,\n                    });\n                }\n            }\n            return segList;\n        }\n\n        public override string ToString()\n        {\n            return $\"{Util.UInt32ToIpAddress(StartIP)}|{Util.UInt32ToIpAddress(EndIP)}|{Region}\";\n        }\n    }\n}\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker/XDB/Util.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace IP2RegionMaker.XDB\n{\n    public static class Util\n    {\n        public static uint IpAddressToUInt32(string ipAddress)\n        {\n            var address = IPAddress.Parse(ipAddress);\n            byte[] bytes = address.GetAddressBytes();\n            Array.Reverse(bytes);\n            return BitConverter.ToUInt32(bytes, 0);\n        }\n\n        public static string UInt32ToIpAddress(uint ipAddress)\n        {\n            byte[] bytes = BitConverter.GetBytes(ipAddress);\n            Array.Reverse(bytes);\n            return new IPAddress(bytes).ToString();\n        }\n\n        public static Segment GetSegment(string line)\n        {\n            var ps = line.Split(\"|\", 3);\n\n            if (ps.Length != 3)\n            {\n                throw new ArgumentException($\"invalid ip segment line {line}\");\n            }\n\n            var sip = Util.IpAddressToUInt32(ps[0]);\n            var eip = Util.IpAddressToUInt32(ps[1]);\n\n            if (sip > eip)\n            {\n                throw new ArgumentException($\"start ip {ps[0]} should not be greater than end ip {ps[1]}\");\n            }\n\n            if (string.IsNullOrEmpty(ps[2]))\n            {\n                throw new ArgumentException($\"empty region info in segment line {line}\");\n            }\n\n            return new Segment\n            {\n                StartIP = sip,\n                EndIP = eip,\n                Region = ps[2],\n            };\n        }\n\n        public static void CheckSegments(List<Segment> segments)\n        {\n            Segment? last = null;\n\n            foreach (var seg in segments)\n            {\n                if (seg.StartIP > seg.EndIP)\n                {\n                    throw new ArgumentException($\"segment `{seg}`: start ip should not be greater than end ip\");\n                }\n\n                if (last != null && last.EndIP + 1 != seg.StartIP)\n                {\n                    throw new ArgumentException($\"discontinuous data segment: last.eip+1({seg.StartIP}) != seg.sip({seg.EndIP},#{seg})\");\n                }\n\n                last = seg;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker.Test/IP2RegionMaker.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net6.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.1.0\" />\n    <PackageReference Include=\"NUnit\" Version=\"3.13.3\" />\n    <PackageReference Include=\"NUnit3TestAdapter\" Version=\"4.2.1\" />\n    <PackageReference Include=\"NUnit.Analyzers\" Version=\"3.3.0\" />\n    <PackageReference Include=\"coverlet.collector\" Version=\"3.1.2\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\IP2RegionMaker\\IP2RegionMaker.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "maker/csharp/IP2RegionMaker.Test/Usings.cs",
    "content": "global using NUnit.Framework;"
  },
  {
    "path": "maker/csharp/IP2RegionMaker.Test/UtilTest.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace IP2RegionMaker.Test\n{\n    [TestFixture]\n    internal class UtilTest\n    {\n        [TestCase(\"114.114.114.114\")]\n        public void TestIpAddressToUInt32(string value)\n        {\n            Assert.DoesNotThrow(() => XDB.Util.IpAddressToUInt32(value));\n        }\n\n        [TestCase(1920103026)]\n        public void TestUInt32ToIpAddress(int value)\n        {\n            Assert.DoesNotThrow(() => XDB.Util.UInt32ToIpAddress((uint)value));\n        }\n\n        [TestCase(\"28.201.224.0|29.34.191.255|美国|0|0|0|0\")]\n        public void TestSplitSegment(string value)\n        {\n            Assert.DoesNotThrow(() =>\n            {\n                var seg=XDB.Util.GetSegment(value);\n\n                var segList= seg.Split();\n\n                XDB.Util.CheckSegments(segList);\n\n                foreach (var item in segList)\n                { \n                    Console.WriteLine(item);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "maker/csharp/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb csharp generation implementation\n\n# Compilation and Installation\n\nCompilation environment: [dotnet6.0](https://dotnet.microsoft.com/zh-cn/download/dotnet/6.0)\n\n```bash\n# cd to maker/csharp/IP2RegionMaker directory\ndotnet publish -o ./bin\n```\n\nThen you will get a packaged file named IP2RegionMaker.dll in the bin directory of the current folder.\n\n# `xdb` Data Generation\n\nGenerate the xdb binary file via `dotnet IP2RegionMaker.dll`:\n\n```bash\n➜  csharp git:(master) ✗ dotnet IP2RegionMaker.dll\nip2region xdb maker\ndotnet IP2RegionMaker.dll [command options]\n--src string    source ip text file path\n--dst string    destination binary xdb file path\n```\n\nFor example, using the default data/ipv4_source.txt source data to generate an ip2region_v4.xdb binary file in the current directory:\n\n```bash\n➜  csharp git:(master) ✗ dotnet ./IP2RegionMaker/bin/IP2RegionMaker.dll --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb\n# You will see a lot of output; eventually, you will see the following output indicating the run was successful\n...\n...\n...\nwrite done, dataBlocks: 13804, indexBlocks: (683591, 720221), indexPtr: (982904, 11065984)\nDone, elapsed:2.1966620833333335m\n```\n\n# Data Query / bench Test\n\nAll [bindings](../../binding/) come with query and bench test programs as well as usage documentation. You can use the searcher of your familiar language for query testing or bench testing to confirm the correctness and integrity of the data.\n"
  },
  {
    "path": "maker/csharp/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb csharp 生成实现\n\n## 编译安装\n编译环境：[dotnet6.0](https://dotnet.microsoft.com/zh-cn/download/dotnet/6.0)\n```bash\n# cd 到 maker/csharp/IP2RegionMaker目录\ndotnet publish -o ./bin\n```\n\n然后会在当前目录的 bin 目录下得到一个 IP2RegionMaker.dll 的打包文件。\n\n# 数据生成\n\n通过 `dotnet IP2RegionMaker.dll` 来生成 xdb 二进制文件：\n```bash  \n➜  csharp git:(master) ✗ dotnet IP2RegionMaker.dll\nip2region xdb maker\ndotnet IP2RegionMaker.dll [command options]\n--src string    source ip text file path\n--dst string    destination binary xdb file path\n```\n\n例如，通过默认的 data/ipv4_source.txt 原数据，在当前目录生成一个 ip2region_v4.xdb 二进制文件：\n```bash\n➜  csharp git:(master) ✗ dotnet ./IP2RegionMaker/bin/IP2RegionMaker.dll --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb\n# 会看到一堆输出，最终会看到如下输出表示运行成功\n...\n...\n...\nwrite done, dataBlocks: 13804, indexBlocks: (683591, 720221), indexPtr: (982904, 11065984)\nDone, elapsed:2.1966620833333335m\n```\n\n# 数据 查询/bench 测试\n\n已经完成开发的 [binding](../../binding/) 都有查询和 bench 测试程序以及使用文档，你可以使用你熟悉的语言的 searcher 进行查询测试或者bench测试，来确认数据的正确性和完整性。\n"
  },
  {
    "path": "maker/golang/Dockerfile",
    "content": "# ============================================\n# ip2region xdb maker - Golang version\n# Multi-stage build, resulting in an image of approximately 10 MB.\n# ============================================\n\nFROM golang:1.22-alpine AS builder\n\nWORKDIR /build\n\nCOPY . ./\n\n# Compile a static binary (disable CGO for fully static linking)\nRUN CGO_ENABLED=0 GOOS=linux go build -ldflags=\"-s -w\" -o xdb_maker\n\nFROM alpine:3.19\n\nRUN apk --no-cache add ca-certificates tzdata && \\\n    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n# Create a non-root user.\nRUN adduser -D -g '' appuser\n\nWORKDIR /app\n\n# Copy binaries from the build stage.\nCOPY --from=builder /build/xdb_maker /app/\n\nRUN mkdir -p /app/data && chown -R appuser:appuser /app\n\nUSER appuser\n\nENTRYPOINT [\"/app/xdb_maker\"]\n\nCMD [\"--help\"]\n"
  },
  {
    "path": "maker/golang/Makefile",
    "content": "# ip2region golang maker makefile\nall: build\n.PHONY: all\n\nbuild:\n\tgo build -o xdb_maker\ntest:\n\tgo test -v ./...\nclean:\n\tfind ./ -name xdb_maker | xargs rm -f\n"
  },
  {
    "path": "maker/golang/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb golang generation implementation\n\n# Program Compilation\n\nCompile to get the xdb_maker executable through the following method:\n\n```\n# cd to the golang maker root directory\nmake\n```\n\nAfter successful compilation, a xdb_maker executable file will be generated in the current directory.\n\n# `xdb` Data Generation\n\nGenerate the ip2region.xdb binary file via the `xdb_maker gen` command:\n\n```\n./xdb_maker gen [command options]\noptions:\n --src string           source ip text file path\n --dst string           destination binary xdb file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \n --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\n --log-level string     set the log level, options: debug/info/warn/error\n```\n\nFor example, generate the xdb file to the current directory using the default source data under the repository's data/ directory:\n\n```bash\n# ipv4 \n./xdb_maker gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n# ipv6\n./xdb_maker gen --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n```\n\nFor custom data fields during the generation process, please refer to [xdb-文件生成#自定义数据字段](https://ip2region.net/doc/data/xdb_make#field-list)\n\n# `xdb` Data Search\n\nTest the input IP via the `xdb_maker search` command:\n\n```\n➜  golang git:(v2.0_xdb) ✗ ./xdb_maker search\n./xdb_maker search [command options]\noptions:\n --db string    ip2region binary xdb file path\n```\n\nFor example, run a search test using the built-in xdb file:\n\n```bash\n# ipv4\n./xdb_maker search --db=../../data/ip2region_v4.xdb\nip2region xdb search test program,\nsource xdb: ../../data/ip2region_v4.xdb (IPv4)\ncommands:\n  loadIndex : load the vector index for search speedup.\n  clearIndex: clear the vector index.\n  quit      : exit the test program\nip2region>> 58.251.30.115\n{region:中国|广东省|深圳市|联通|CN, iocount:2, took:27.893µs}\nip2region>> 1.2.3.4\n{region:Australia|Queensland|Brisbane|0|AU, iocount:5, took:58.746µs}\n\n# ipv6\n./xdb_maker search --db=../../data/ip2region_v6.xdb\nip2region xdb search test program,\nsource xdb: ../../data/ip2region_v6.xdb (IPv6)\ncommands:\n  loadIndex : load the vector index for search speedup.\n  clearIndex: clear the vector index.\n  quit      : exit the test program\nip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff\n{region:United States|Florida|Miami|velia.net Internetdienste GmbH|US, iocount:15, took:140.942µs}\nip2region>> 240e:3b7:3273:51d0:13f9:bf0:3db1:aa3f\n{region:中国|广东省|深圳市|电信|CN, iocount:9, took:67058µs}\n```\n\n# `xdb` Data Editing\n\nEdit the raw IP data via the `xdb_maker edit` command:\n\n```\n./xdb_maker edit [command options]\noptions:\n --src string        source ip text file path\n --version string    IP version, options: ipv4/ipv6, specify this flag so you don't get confused\n```\n\nFor example, opening `./data/ipv4_source.txt` with the editor will show the following operation panel:\n\n```bash\n./xdb_maker edit --src=../../data/ipv4_source.txt --version=ipv4\ninit the editor from source @ `../../data/ipv4_source.txt` ... \nall segments loaded, length: 683591, elapsed: 479.73743ms\ncommand list: \n  put [segment]        : put the specifield $segment\n  put_file [file]      : put all the segments from the specified $file\n  list [offset] [size] : list the first $size segments start from $offset\n  save                 : save all the changes to the destination source file\n  quit                 : exit the program\n  help                 : print this help menu\neditor>>\n```\n\nModify the location information of a specified IP segment using the `put` command, for example:\n\n```bash\neditor>> put 36.132.128.0|36.132.147.255|中国|黑龙江省|哈尔滨市|移动|CN\nPut(36.132.128.0|36.132.147.255|中国|黑龙江省|哈尔滨市|移动|CN): Ok, with 1 deletes and 2 additions\n*editor>> \n```\n\nBatch load modifications from a file using the `put_file` command. The IP segments in the file do not need to be as strict as the data in `./data/ipvx_source.txt`; they do not need to be continuous, and it does not matter if different IP segments overlap. The editor will automatically analyze and process them, for example:\n\n```bash\n*editor>> put_file ../../data/sample/ip.test.txt\nPutFile(../../data/sample/ip.test.txt): Ok, with 25 deletes and 25 additions\n*editor>> \n```\n\nSave modifications using the `save` command. After saving successfully, you can re-generate the xdb from the modified raw IP file using the commands mentioned above:\n\n```bash\n*editor>> save\nall segments saved to ../../data/ipv4_source.txt\neditor>> \n```\n\n# bench Test\n\nIf you have generated the `xdb` file yourself, please ensure you run the following `xdb_maker bench` command to verify the correctness of the generated `xdb` file:\n\n```\n./xdb_maker bench [command options]\noptions:\n --db string            ip2region binary xdb file path\n --src string           source ip text file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \n --log-level string     set the log level, options: debug/info/warn/error\n --ignore-error bool    keep going if bench failed\n```\n\nFor example: use the source files under data to bench test the xdb files in data:\n\n```bash\n# ipv4\n./xdb_maker bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt --version=ipv4\n\n#ipv6\n./xdb_maker bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt --version=ipv6\n```\n\n*Please note that the `src` file used for the bench test must be the same as the source file used to generate the xdb*.\nIf an error occurs during execution, it will stop immediately. You can also execute with the `--ignore-error=true` parameter to ignore errors and view the failed statistics at the end.\n\n\n# Docker\n\n```bash\n# Build the image (run in the maker/golang directory).\ncd ip2region/maker/golang\ndocker build -t ip2region-maker .\n\n# Generate IPv4 xdb\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    gen --src=/app/data/ipv4_source.txt \\\n        --dst=/app/data/ip2region_v4.xdb \\\n        --version=ipv4\n\n# Generate IPv6 xdb\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    gen --src=/app/data/ipv6_source.txt \\\n        --dst=/app/data/ip2region_v6.xdb \\\n        --version=ipv6\n\n# Interactive IPv4 Query\ndocker run -it --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    search --db=/app/data/ip2region_v4.xdb\n\n# Bench test IPv4\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    bench --db=/app/data/ip2region_v4.xdb \\\n          --src=/app/data/ipv4_source.txt \\\n          --version=ipv4\n```\n"
  },
  {
    "path": "maker/golang/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb golang 生成实现\n\n# 程序编译\n\n通过如下方式编译得到 xdb_maker 可执行程序:\n\n```\n# 切换到 golang maker 根目录\nmake\n```\n\n编译成功后会在当前目录生成一个 xdb_maker 的可执行文件\n\n# `xdb` 数据生成\n\n通过 `xdb_maker gen` 命令生成 ip2region.xdb 二进制文件:\n\n```\n./xdb_maker gen [command options]\noptions:\n --src string           source ip text file path\n --dst string           destination binary xdb file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \n --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\n --log-level string     set the log level, options: debug/info/warn/error\n```\n\n例如，使用默认的仓库 data/ 下默认的原始数据生成生成 xdb 文件到当前目录：\n\n```bash\n# ipv4 \n./xdb_maker gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n# ipv6\n./xdb_maker gen --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n```\n\n生成过程中数据字段自定义请参考 [xdb-文件生成#自定义数据字段](https://ip2region.net/doc/data/xdb_make#field-list)\n\n# `xdb` 数据查询\n\n通过 `xdb_maker search` 命令来测试查询输入的 ip：\n\n```\n➜  golang git:(v2.0_xdb) ✗ ./xdb_maker search\n./xdb_maker search [command options]\noptions:\n --db string    ip2region binary xdb file path\n```\n\n例如，使用自带的 xdb 文件来运行查询测试：\n\n```bash\n# ipv4\n./xdb_maker search --db=../../data/ip2region_v4.xdb\nip2region xdb search test program,\nsource xdb: ../../data/ip2region_v4.xdb (IPv4)\ncommands:\n  loadIndex : load the vector index for search speedup.\n  clearIndex: clear the vector index.\n  quit      : exit the test program\nip2region>> 58.251.30.115\n{region:中国|广东省|深圳市|联通|CN, iocount:2, took:27.893µs}\nip2region>> 1.2.3.4\n{region:Australia|Queensland|Brisbane|0|AU, iocount:5, took:58.746µs}\n\n# ipv6\n./xdb_maker search --db=../../data/ip2region_v6.xdb\nip2region xdb search test program,\nsource xdb: ../../data/ip2region_v6.xdb (IPv6)\ncommands:\n  loadIndex : load the vector index for search speedup.\n  clearIndex: clear the vector index.\n  quit      : exit the test program\nip2region>> 2604:bc80:8001:11a4:ffff:ffff:ffff:ffff\n{region:United States|Florida|Miami|velia.net Internetdienste GmbH|US, iocount:15, took:140.942µs}\nip2region>> 240e:3b7:3273:51d0:13f9:bf0:3db1:aa3f\n{region:中国|广东省|深圳市|电信|CN, iocount:9, took:67.058µs}\n```\n\n# `xdb` 数据编辑\n\n通过 `xdb_maker edit` 命令来编辑原始的　IP 数据：\n\n```\n./xdb_maker edit [command options]\noptions:\n --src string        source ip text file path\n --version string    IP version, options: ipv4/ipv6, specify this flag so you don't get confused\n```\n\n例如，使用编辑器打开 `./data/ipv4_source.txt` 会看到如下的操作面板：\n\n```bash\n./xdb_maker edit --src=../../data/ipv4_source.txt --version=ipv4\ninit the editor from source @ `../../data/ipv4_source.txt` ... \nall segments loaded, length: 683591, elapsed: 479.73743ms\ncommand list: \n  put [segment]        : put the specifield $segment\n  put_file [file]      : put all the segments from the specified $file\n  list [offset] [size] : list the first $size segments start from $offset\n  save                 : save all the changes to the destination source file\n  quit                 : exit the program\n  help                 : print this help menu\neditor>>\n```\n\n通过 `put` 命令修改指定 IP 段的定位信息，例如：\n\n```bash\neditor>> put 36.132.128.0|36.132.147.255|中国|黑龙江省|哈尔滨市|移动|CN\nPut(36.132.128.0|36.132.147.255|中国|黑龙江省|哈尔滨市|移动): Ok, with 1 deletes and 2 additions\n*editor>> \n```\n\n通过 `put_file` 命令从文件中批量载入修改，文件中的 IP 段不需要像　./data/ipvx_source.txt 中的数据那么严格，不需要前后连续，不同 IP　段有重叠也没关系，编辑器会自动分析处理，例如：\n\n```bash\n*editor>> put_file ../../data/sample/ip.test.txt\nPutFile(../../data/sample/ip.test.txt): Ok, with 25 deletes and 25 additions\n*editor>> \n```\n\n通过 `save` 命令保存修改，保存成功后，再通过上面的命令从修改后的原始 IP 文件重新生成 xdb 即可：\n\n```bash\n*editor>> save\nall segments saved to ../../data/ipv4_source.txt\neditor>> \n```\n\n# bench 测试\n\n如果你自主生成了 `xdb` 文件，请确保运行如下的 `xdb_maker bench` 命令来确保生成的的 `xdb` 文件的正确性：\n\n```\n./xdb_maker bench [command options]\noptions:\n --db string            ip2region binary xdb file path\n --src string           source ip text file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \n --log-level string     set the log level, options: debug/info/warn/error\n --ignore-error bool    keep going if bench failed\n```\n\n例如：使用 data 下的源文件来 bench 测试 data 的 xdb 文件：\n\n```bash\n# ipv4\n./xdb_maker bench --db=../../data/ip2region_v4.xdb --src=../../data/ipv4_source.txt --version=ipv4\n\n#ipv6\n./xdb_maker bench --db=../../data/ip2region_v6.xdb --src=../../data/ipv6_source.txt --version=ipv6\n```\n\n*请注意 bench 测试使用的 `src` 文件需要是对应的生成 xdb 的源文件相同*。\n如果运行过程中有错误会立马停止运行，也可以执行 --ignore-error=true 参数来忽略错误，在最后看 failed 的统计结果。\n\n\n# Docker\n\n```bash\n# 构建镜像（在 maker/golang 目录下执行）\ncd ip2region/maker/golang\ndocker build -t ip2region-maker .\n\n# 生成 IPv4 xdb\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    gen --src=/app/data/ipv4_source.txt \\\n        --dst=/app/data/ip2region_v4.xdb \\\n        --version=ipv4\n\n# 生成 IPv6 xdb\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    gen --src=/app/data/ipv6_source.txt \\\n        --dst=/app/data/ip2region_v6.xdb \\\n        --version=ipv6\n\n# 交互式查询 IPv4\ndocker run -it --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    search --db=/app/data/ip2region_v4.xdb\n\n# Bench 测试 IPv4\ndocker run --rm -v $(pwd)/../../data:/app/data ip2region-maker \\\n    bench --db=/app/data/ip2region_v4.xdb \\\n          --src=/app/data/ipv4_source.txt \\\n          --version=ipv4\n```\n"
  },
  {
    "path": "maker/golang/cmd/bench.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/xdb\"\n)\n\nfunc Bench() {\n\tvar err error\n\tvar dbFile, srcFile, ipVersion, logLevel = \"\", \"\", \"\", \"\"\n\tvar ignoreError = false\n\tvar fErr = iterateFlags(func(key string, val string) error {\n\t\tswitch key {\n\t\tcase \"db\":\n\t\t\tdbFile = val\n\t\tcase \"src\":\n\t\t\tsrcFile = val\n\t\tcase \"version\":\n\t\t\tipVersion = val\n\t\tcase \"log-level\":\n\t\t\tlogLevel = val\n\t\tcase \"ignore-error\":\n\t\t\tif val == \"true\" || val == \"1\" {\n\t\t\t\tignoreError = true\n\t\t\t} else if val == \"false\" || val == \"0\" {\n\t\t\t\tignoreError = false\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"invalid value for ignore-error option, could be false/0 or true/1\")\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"undefined option '%s=%s'\", key, val)\n\t\t}\n\t\treturn nil\n\t})\n\tif fErr != nil {\n\t\tfmt.Printf(\"failed to parse flags: %s\", fErr)\n\t\treturn\n\t}\n\n\tif dbFile == \"\" || srcFile == \"\" {\n\t\tfmt.Printf(\"%s bench [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --db string            ip2region binary xdb file path\\n\")\n\t\tfmt.Printf(\" --src string           source ip text file path\\n\")\n\t\tfmt.Printf(\" --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \\n\")\n\t\tfmt.Printf(\" --log-level string     set the log level, options: debug/info/warn/error\\n\")\n\t\tfmt.Printf(\" --ignore-error bool    keep going if bench failed\\n\")\n\t\treturn\n\t}\n\n\t// check and define the IP version\n\tvar version *xdb.Version = nil\n\tif len(ipVersion) < 2 {\n\t\tslog.Error(\"please specify the ip version with flag --version, ipv4 or ipv6 ?\")\n\t\treturn\n\t} else if v, err := xdb.VersionFromName(ipVersion); err != nil {\n\t\tslog.Error(\"failed to parse version name\", \"error\", err)\n\t\treturn\n\t} else {\n\t\tversion = v\n\t}\n\n\t// check and apply the log level\n\terr = applyLogLevel(logLevel)\n\tif err != nil {\n\t\tslog.Error(\"failed to apply log level\", \"error\", err)\n\t\treturn\n\t}\n\n\tsearcher, err := xdb.NewSearcher(version, dbFile)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create searcher with `%s`: %s\\n\", dbFile, err)\n\t\treturn\n\t}\n\tdefer func() {\n\t\tsearcher.Close()\n\t}()\n\n\thandle, err := os.OpenFile(srcFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to open source text file: %s\\n\", err)\n\t\treturn\n\t}\n\n\tdefer handle.Close()\n\n\tvar count, errCount, tStart = 0, 0, time.Now()\n\tslog.Info(\"Bench start\", \"xdbPath\", dbFile, \"srcPath\", srcFile)\n\t_, _, iErr := xdb.IterateSegments(handle, false, nil, nil, func(seg *xdb.Segment) error {\n\t\tvar l = fmt.Sprintf(\"%d|%d|%s\", seg.StartIP, seg.EndIP, seg.Region)\n\t\tslog.Debug(\"try to bench\", \"segment\", l)\n\t\t// mip := xdb.IPMiddle(seg.StartIP, seg.EndIP)\n\t\t// for _, ip := range [][]byte{seg.StartIP, xdb.IPMiddle(seg.EndIP, mip), mip, xdb.IPMiddle(mip, seg.EndIP), seg.EndIP} {\n\t\tfor _, ip := range [][]byte{seg.StartIP, seg.EndIP} {\n\t\t\tslog.Debug(\"|-try to bench\", \"ip\", xdb.IP2String(ip))\n\t\t\tr, _, err := searcher.Search(ip)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to search ip '%s': %s\", xdb.IP2Long(ip), err)\n\t\t\t}\n\n\t\t\t// check the region info\n\t\t\tcount++\n\t\t\tif r != seg.Region {\n\t\t\t\terrCount++\n\t\t\t\tslog.Error(\" --[Failed] region not match\", \"src\", r, \"dst\", seg.Region)\n\t\t\t\tif !ignoreError {\n\t\t\t\t\treturn fmt.Errorf(\"\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tslog.Debug(\" --[Ok]\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif iErr != nil {\n\t\tfmt.Printf(\"%s\", err)\n\t\treturn\n\t}\n\n\tslog.Info(\"Bench finished\", \"count\", count, \"failed\", errCount, \"elapsed\", time.Since(tStart))\n}\n"
  },
  {
    "path": "maker/golang/cmd/edit.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/xdb\"\n)\n\n// source ip data editor\n\nfunc Edit() {\n\tvar err error\n\tvar srcFile, ipVersion = \"\", \"\"\n\tvar fErr = iterateFlags(func(key string, val string) error {\n\t\tswitch key {\n\t\tcase \"src\":\n\t\t\tsrcFile = val\n\t\tcase \"version\":\n\t\t\tipVersion = val\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"undefined option '%s=%s'\", key, val)\n\t\t}\n\t\treturn nil\n\t})\n\tif fErr != nil {\n\t\tfmt.Printf(\"failed to parse flags: %s\", fErr)\n\t\treturn\n\t}\n\n\tif srcFile == \"\" {\n\t\tfmt.Printf(\"%s edit [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --src string        source ip text file path\\n\")\n\t\tfmt.Printf(\" --version string    IP version, options: ipv4/ipv6, specify this flag so you don't get confused \\n\")\n\t\treturn\n\t}\n\n\t// check and define the IP version\n\tvar version *xdb.Version = nil\n\tif len(ipVersion) < 2 {\n\t\tslog.Error(\"please specify the ip version with flag --version, ipv4 or ipv6 ?\")\n\t\treturn\n\t} else if v, err := xdb.VersionFromName(ipVersion); err != nil {\n\t\tslog.Error(\"failed to parse version name\", \"error\", err)\n\t\treturn\n\t} else {\n\t\tversion = v\n\t}\n\n\trExp, err := regexp.Compile(`\\s+`)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to compile regexp: %s\\n\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"init the editor from source @ `%s` ... \\n\", srcFile)\n\tvar tStart = time.Now()\n\teditor, err := xdb.NewEditor(version, srcFile)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to init editor: %s\", err)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"all segments loaded, length: %d, elapsed: %s\\n\", editor.SegLen(), time.Since(tStart))\n\tvar help = func() {\n\t\tfmt.Printf(\"command list: \\n\")\n\t\tfmt.Printf(\"  put [segment]        : put the specifield $segment\\n\")\n\t\tfmt.Printf(\"  put_file [file]      : put all the segments from the specified $file\\n\")\n\t\tfmt.Printf(\"  list [offset] [size] : list the first $size segments start from $offset\\n\")\n\t\tfmt.Printf(\"  save                 : save all the changes to the destination source file\\n\")\n\t\tfmt.Printf(\"  quit                 : exit the program\\n\")\n\t\tfmt.Printf(\"  help                 : print this help menu\\n\")\n\t}\n\n\thelp()\n\tvar sTip = \"\"\n\tvar reader = bufio.NewReader(os.Stdin)\n\tfor {\n\t\tif editor.NeedSave() {\n\t\t\tsTip = \"*\"\n\t\t} else {\n\t\t\tsTip = \"\"\n\t\t}\n\n\t\tfmt.Printf(\"%seditor>> \", sTip)\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"failed to read line from cli: %s\\n\", err)\n\t\t\tbreak\n\t\t}\n\n\t\tcmd := strings.TrimSpace(line)\n\t\tif cmd == \"help\" {\n\t\t\thelp()\n\t\t} else if cmd == \"quit\" {\n\t\t\tif editor.NeedSave() {\n\t\t\t\tfmt.Printf(\"there are changes that need to save, type 'quit!' to force quit\\n\")\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t} else if cmd == \"quit!\" {\n\t\t\t// quit directly\n\t\t\tbreak\n\t\t} else if cmd == \"save\" {\n\t\t\terr = editor.Save()\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"failed to save the changes: %s\\n\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfmt.Printf(\"all segments saved to %s\\n\", srcFile)\n\t\t} else if strings.HasPrefix(cmd, \"list\") {\n\t\t\tvar sErr error\n\t\t\toff, size, l := 0, 10, len(\"list\")\n\t\t\tstr := strings.TrimSpace(cmd)\n\t\t\tif len(str) > l {\n\t\t\t\tsets := rExp.Split(cmd, 3)\n\t\t\t\tswitch len(sets) {\n\t\t\t\tcase 2:\n\t\t\t\t\t_, sErr = fmt.Sscanf(cmd, \"%s %d\", &str, &off)\n\t\t\t\tcase 3:\n\t\t\t\t\t_, sErr = fmt.Sscanf(cmd, \"%s %d %d\", &str, &off, &size)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif sErr != nil {\n\t\t\t\tfmt.Printf(\"failed to parse the offset and size: %s\\n\", sErr)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"+-slice(%d,%d): \\n\", off, size)\n\t\t\tfor _, s := range editor.Slice(off, size) {\n\t\t\t\tfmt.Printf(\"%s\\n\", s)\n\t\t\t}\n\t\t} else if strings.HasPrefix(cmd, \"put \") {\n\t\t\tseg := strings.TrimSpace(cmd[len(\"put \"):])\n\t\t\to, n, err := editor.Put(seg)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"failed to Put(%s): %s\\n\", seg, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfmt.Printf(\"Put(%s): Ok, with %d deletes and %d additions\\n\", seg, o, n)\n\t\t} else if strings.HasPrefix(cmd, \"put_file \") {\n\t\t\tfile := strings.TrimSpace(cmd[len(\"put_file \"):])\n\t\t\to, n, err := editor.PutFile(file)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"failed to PutFile(%s): %s\\n\", file, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfmt.Printf(\"PutFile(%s): Ok, with %d deletes and %d additions\\n\", file, o, n)\n\t\t} else if len(cmd) > 0 {\n\t\t\thelp()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "maker/golang/cmd/generate.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/xdb\"\n)\n\n// script to do the xdb generate\n\nfunc Generate() {\n\tvar err error\n\tvar srcFile, dstFile = \"\", \"\"\n\tvar ipVersion, fieldList, logLevel = \"\", \"\", \"info\"\n\tvar indexPolicy = xdb.VectorIndexPolicy\n\tvar fErr = iterateFlags(func(key string, val string) error {\n\t\tswitch key {\n\t\tcase \"src\":\n\t\t\tsrcFile = val\n\t\tcase \"dst\":\n\t\t\tdstFile = val\n\t\tcase \"version\":\n\t\t\tipVersion = val\n\t\tcase \"log-level\":\n\t\t\tlogLevel = val\n\t\tcase \"field-list\":\n\t\t\tfieldList = val\n\t\tcase \"index\":\n\t\t\tindexPolicy, err = xdb.IndexPolicyFromString(val)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"parse policy: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"undefine option `%s=%s`\", key, val)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif fErr != nil {\n\t\tfmt.Printf(\"failed to parse flags: %s\", fErr)\n\t\treturn\n\t}\n\n\tif srcFile == \"\" || dstFile == \"\" {\n\t\tfmt.Printf(\"%s gen [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --src string           source ip text file path\\n\")\n\t\tfmt.Printf(\" --dst string           destination binary xdb file path\\n\")\n\t\tfmt.Printf(\" --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused \\n\")\n\t\tfmt.Printf(\" --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\\n\")\n\t\tfmt.Printf(\" --log-level string     set the log level, options: debug/info/warn/error\\n\")\n\t\treturn\n\t}\n\n\t// check and define the IP version\n\tvar version *xdb.Version = nil\n\tif len(ipVersion) < 2 {\n\t\tslog.Error(\"please specify the ip version with flag --version, ipv4 or ipv6 ?\")\n\t\treturn\n\t} else if v, err := xdb.VersionFromName(ipVersion); err != nil {\n\t\tslog.Error(\"failed to parse version name\", \"error\", err)\n\t\treturn\n\t} else {\n\t\tversion = v\n\t}\n\n\t// check and apply the log level\n\terr = applyLogLevel(logLevel)\n\tif err != nil {\n\t\tslog.Error(\"failed to apply log level\", \"error\", err)\n\t\treturn\n\t}\n\n\tfields, err := getFilterFields(fieldList)\n\tif err != nil {\n\t\tslog.Error(\"failed to get filter fields\", \"error\", err)\n\t\treturn\n\t}\n\n\t// make the binary file\n\ttStart := time.Now()\n\tmaker, err := xdb.NewMaker(version, indexPolicy, srcFile, dstFile, fields)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create %s\\n\", err)\n\t\treturn\n\t}\n\n\tslog.Info(\"Generating xdb with\", \"src\", srcFile, \"dst\", dstFile, \"logLevel\", logLevel)\n\terr = maker.Init()\n\tif err != nil {\n\t\tfmt.Printf(\"failed Init: %s\\n\", err)\n\t\treturn\n\t}\n\n\terr = maker.Start()\n\tif err != nil {\n\t\tfmt.Printf(\"failed Start: %s\\n\", err)\n\t\treturn\n\t}\n\n\terr = maker.End()\n\tif err != nil {\n\t\tfmt.Printf(\"failed End: %s\\n\", err)\n\t}\n\n\tslog.Info(\"make done\", \"elapsed\", time.Since(tStart))\n}\n"
  },
  {
    "path": "maker/golang/cmd/process.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/xdb\"\n)\n\n// source data process, sort, de-duplicate, merge\n\nfunc Process() {\n\tvar err error\n\tvar srcFile, dstFile = \"\", \"\"\n\tvar fieldList, logLevel = \"\", \"\"\n\tvar clearBasedIndex = -1\n\tvar clearValueEqual, clearValueExcept = \"\", \"\"\n\tvar fErr = iterateFlags(func(key string, val string) error {\n\t\tswitch key {\n\t\tcase \"src\":\n\t\t\tsrcFile = val\n\t\tcase \"dst\":\n\t\t\tdstFile = val\n\t\tcase \"field-list\":\n\t\t\tfieldList = val\n\t\tcase \"clear-based-index\":\n\t\t\tnum, err := strconv.Atoi(val)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid clear-based-index '%s=%s', integer expected\", key, val)\n\t\t\t}\n\n\t\t\tclearBasedIndex = num\n\t\tcase \"clear-value-equal\":\n\t\t\tclearValueEqual = val\n\t\tcase \"clear-value-except\":\n\t\t\tclearValueExcept = val\n\t\tcase \"log-level\":\n\t\t\tlogLevel = val\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"undefined option '%s=%s'\", key, val)\n\t\t}\n\t\treturn nil\n\t})\n\tif fErr != nil {\n\t\tfmt.Printf(\"failed to parse flags: %s\", fErr)\n\t\treturn\n\t}\n\n\tif srcFile == \"\" || dstFile == \"\" {\n\t\tfmt.Printf(\"%s process [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --src string                 source ip text file path\\n\")\n\t\tfmt.Printf(\" --dst string                 target ip text file path\\n\")\n\t\tfmt.Printf(\" --field-list string          field index list imploded with ',' eg: 0,1,2,3-6,7\\n\")\n\t\tfmt.Printf(\" --clear-based-index integer  clear based index eg: 3\\n\")\n\t\tfmt.Printf(\" --clear-value-equal string   clear value equal to the specified one\\n\")\n\t\tfmt.Printf(\" --clear-value-except string  clear value except the specified one\\n\")\n\t\tfmt.Printf(\" --log-level string           set the log level, options: debug/info/warn/error\\n\")\n\t\treturn\n\t}\n\n\tif clearBasedIndex > -1 {\n\t\tif len(clearValueEqual) > 0 && len(clearValueExcept) > 0 {\n\t\t\tfmt.Print(\"Only one can be specified besides clear-value-equal and clear-value-except\")\n\t\t\treturn\n\t\t}\n\n\t\tif len(clearValueEqual) == 0 && len(clearValueExcept) == 0 {\n\t\t\tfmt.Print(\"At least one must be specified for clear-value-equal and clear-value-except\")\n\t\t\treturn\n\t\t}\n\t}\n\n\t// check and apply the log level\n\terr = applyLogLevel(logLevel)\n\tif err != nil {\n\t\tslog.Error(\"failed to apply log level\", \"error\", err)\n\t\treturn\n\t}\n\n\tfields, err := getFilterFields(fieldList)\n\tif err != nil {\n\t\tslog.Error(\"failed to get filter fields\", \"error\", err)\n\t\treturn\n\t}\n\n\t// make the binary file\n\ttStart := time.Now()\n\tprocessor, err := xdb.NewProcessor(srcFile, dstFile, fields, clearBasedIndex, clearValueEqual, clearValueExcept)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create %s\\n\", err)\n\t\treturn\n\t}\n\n\terr = processor.Init()\n\tif err != nil {\n\t\tfmt.Printf(\"failed Init: %s\\n\", err)\n\t\treturn\n\t}\n\n\tslog.Info(\"Processing\", \"src\", srcFile, \"dst\", dstFile, \"fields\", fields, \"clearBasedIndex\",\n\t\tclearBasedIndex, \"clearValueEqual\", clearValueEqual, \"clearValueExcept\", clearValueExcept, \"logLevel\", logLevel)\n\terr = processor.Start()\n\tif err != nil {\n\t\tfmt.Printf(\"failed Start: %s\\n\", err)\n\t\treturn\n\t}\n\n\terr = processor.End()\n\tif err != nil {\n\t\tfmt.Printf(\"failed End: %s\\n\", err)\n\t}\n\n\tslog.Info(\"processor done\", \"elapsed\", time.Since(tStart))\n}\n"
  },
  {
    "path": "maker/golang/cmd/search.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage cmd\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/xdb\"\n)\n\n// xdb searcher test\n\nfunc Search() {\n\tvar err error\n\tvar dbFile = \"\"\n\tvar fErr = iterateFlags(func(key string, val string) error {\n\t\tif key == \"db\" {\n\t\t\tdbFile = val\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"undefined option '%s=%s'\", key, val)\n\t\t}\n\t\treturn nil\n\t})\n\tif fErr != nil {\n\t\tfmt.Printf(\"failed to parse flags: %s\", fErr)\n\t\treturn\n\t}\n\n\tif dbFile == \"\" {\n\t\tfmt.Printf(\"%s search [command options]\\n\", os.Args[0])\n\t\tfmt.Printf(\"options:\\n\")\n\t\tfmt.Printf(\" --db string         ip2region binary xdb file path\\n\")\n\t\treturn\n\t}\n\n\t// detect the version from the xdb header\n\theader, err := xdb.LoadXdbHeaderFromFile(dbFile)\n\tif err != nil {\n\t\tslog.Error(\"failed to load xdb header\", \"error\", err)\n\t\treturn\n\t}\n\n\tvar version *xdb.Version = nil\n\tversionNo := binary.LittleEndian.Uint16(header[0:])\n\tif versionNo == 2 {\n\t\t// old xdb file\n\t\tversion = xdb.IPv4\n\t} else if versionNo == 3 {\n\t\tipNo := int(binary.LittleEndian.Uint16(header[16:]))\n\t\tif ipNo == xdb.IPv4.Id {\n\t\t\tversion = xdb.IPv4\n\t\t} else if ipNo == xdb.IPv6.Id {\n\t\t\tversion = xdb.IPv6\n\t\t} else {\n\t\t\tslog.Error(\"invalid ip version\", \"id\", ipNo)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tslog.Error(\"invalid xdb version\", \"versionNo\", versionNo, \"xdbFile\", dbFile)\n\t\treturn\n\t}\n\n\tsearcher, err := xdb.NewSearcher(version, dbFile)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to create searcher with `%s`: %s\\n\", dbFile, err.Error())\n\t\treturn\n\t}\n\tdefer func() {\n\t\tsearcher.Close()\n\t\tfmt.Printf(\"test program exited, thanks for trying\\n\")\n\t}()\n\n\tfmt.Printf(`ip2region xdb search test program,\nsource xdb: %s (%s)\ncommands:\n  loadIndex : load the vector index for search speedup.\n  clearIndex: clear the vector index.\n  quit      : exit the test program\n`, dbFile, version.Name)\n\treader := bufio.NewReader(os.Stdin)\n\tfor {\n\t\tfmt.Print(\"ip2region>> \")\n\t\tstr, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tslog.Error(\"failed to read string\", \"error\", err)\n\t\t\treturn\n\t\t}\n\n\t\tline := strings.TrimSpace(strings.TrimSuffix(str, \"\\n\"))\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// command interception and execution\n\t\tif line == \"loadIndex\" {\n\t\t\terr = searcher.LoadVectorIndex()\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"failed to load vector index\", \"error\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Printf(\"vector index cached\\n\")\n\t\t\tcontinue\n\t\t} else if line == \"clearIndex\" {\n\t\t\tsearcher.ClearVectorIndex()\n\t\t\tfmt.Printf(\"vector index cleared\\n\")\n\t\t\tcontinue\n\t\t} else if line == \"quit\" {\n\t\t\tbreak\n\t\t}\n\n\t\tip, err := xdb.ParseIP(line)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"invalid ip address `%s`\\n\", line)\n\t\t\tcontinue\n\t\t}\n\n\t\ttStart := time.Now()\n\t\tregion, ioCount, err := searcher.Search(ip)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\x1b[0;31m{err:%s, iocount:%d}\\x1b[0m\\n\", err.Error(), ioCount)\n\t\t} else {\n\t\t\tfmt.Printf(\"\\x1b[0;32m{region:%s, iocount:%d, took:%s}\\x1b[0m\\n\", region, ioCount, time.Since(tStart))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "maker/golang/cmd/util.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc PrintHelp() {\n\tfmt.Printf(\"ip2region xdb maker\\n\")\n\tfmt.Printf(\"%s [command] [command options]\\n\", os.Args[0])\n\tfmt.Printf(\"Command: \\n\")\n\tfmt.Printf(\"  gen      generate the binary xdb file\\n\")\n\tfmt.Printf(\"  search   binary xdb search test\\n\")\n\tfmt.Printf(\"  bench    binary xdb bench test\\n\")\n\tfmt.Printf(\"  edit     edit the source ip data\\n\")\n\tfmt.Printf(\"  process  process the source ip data\\n\")\n}\n\n// Iterate the cli flags\nfunc iterateFlags(cb func(key string, val string) error) error {\n\tfor i := 2; i < len(os.Args); i++ {\n\t\tr := os.Args[i]\n\t\tif len(r) < 5 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Index(r, \"--\") != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar sIdx = strings.Index(r, \"=\")\n\t\tif sIdx < 0 {\n\t\t\treturn fmt.Errorf(\"missing = for args pair '%s'\", r)\n\t\t}\n\n\t\tif err := cb(r[2:sIdx], r[sIdx+1:]); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc applyLogLevel(logLevel string) error {\n\t// check and apply the log level\n\tvar levelLog = slog.LevelInfo\n\tswitch strings.ToLower(logLevel) {\n\tcase \"debug\":\n\t\tlevelLog = slog.LevelDebug\n\tcase \"info\":\n\t\tlevelLog = slog.LevelInfo\n\tcase \"warn\":\n\t\tlevelLog = slog.LevelWarn\n\tcase \"error\":\n\t\tlevelLog = slog.LevelError\n\tcase \"\":\n\t\t// ignore the empty value\n\t\t// and default it to LevelInfo\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid log level %s\", logLevel)\n\t}\n\n\tslog.SetLogLoggerLevel(levelLog)\n\treturn nil\n}\n\nvar pattern = regexp.MustCompile(`^(\\d+(-\\d+)?)$`)\n\nfunc getFilterFields(fieldList string) ([]int, error) {\n\tif len(fieldList) == 0 {\n\t\treturn []int{}, nil\n\t}\n\n\tvar fields []int\n\tvar mapping = make(map[string]string)\n\tfList := strings.Split(fieldList, \",\")\n\tfor _, f := range fList {\n\t\tf = strings.TrimSpace(f)\n\t\tif len(f) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"empty field index value `%s`\", f)\n\t\t}\n\n\t\tms := pattern.FindString(f)\n\t\tif len(ms) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"field `%s` is not a number or number range\", f)\n\t\t}\n\n\t\t// if strings.Index(ms, \"-\") == -1 {\n\t\tif !strings.Contains(ms, \"-\") {\n\t\t\tif _, ok := mapping[ms]; ok {\n\t\t\t\treturn nil, fmt.Errorf(\"duplicate option `%s`\", f)\n\t\t\t}\n\n\t\t\tidx, err := strconv.Atoi(ms)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"field index `%s` not an integer\", f)\n\t\t\t}\n\n\t\t\tmapping[ms] = ms\n\t\t\tfields = append(fields, idx)\n\t\t\tcontinue\n\t\t}\n\n\t\tra := strings.Split(ms, \"-\")\n\t\tif len(ra) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"invalid field index range `%s`\", ms)\n\t\t}\n\n\t\tstart, err := strconv.Atoi(ra[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"range start `%s` not an integer\", ra[0])\n\t\t}\n\n\t\tend, err := strconv.Atoi(ra[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"range end `%s` not an integer\", ra[1])\n\t\t}\n\n\t\tif start > end {\n\t\t\treturn nil, fmt.Errorf(\"index range start(%d) should <= end(%d)\", start, end)\n\t\t}\n\n\t\tfor i := start; i <= end; i++ {\n\t\t\ts := strconv.Itoa(i)\n\t\t\tif _, ok := mapping[s]; ok {\n\t\t\t\treturn nil, fmt.Errorf(\"duplicate option `%s`\", s)\n\t\t\t}\n\n\t\t\tmapping[s] = s\n\t\t\tfields = append(fields, i)\n\t\t}\n\t}\n\n\t// sort the fields\n\tsort.Ints(fields)\n\t// fmt.Printf(\"%+v\\n\", fields)\n\treturn fields, nil\n}\n"
  },
  {
    "path": "maker/golang/go.mod",
    "content": "module github.com/lionsoul2014/ip2region/maker/golang\n\ngo 1.17\n"
  },
  {
    "path": "maker/golang/go.sum",
    "content": ""
  },
  {
    "path": "maker/golang/main.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/lionsoul2014/ip2region/maker/golang/cmd\"\n)\n\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\tcmd.PrintHelp()\n\t\treturn\n\t}\n\n\tlog.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)\n\tswitch strings.ToLower(os.Args[1]) {\n\tcase \"gen\":\n\t\tcmd.Generate()\n\tcase \"search\":\n\t\tcmd.Search()\n\tcase \"bench\":\n\t\tcmd.Bench()\n\tcase \"edit\":\n\t\tcmd.Edit()\n\tcase \"process\":\n\t\tcmd.Process()\n\tdefault:\n\t\tcmd.PrintHelp()\n\t}\n}\n"
  },
  {
    "path": "maker/golang/make.bat",
    "content": "::ip2region golang maker makefile in windows\n@echo off\n\nif [%1] == [] goto:build\n\nif %1==clean (\n\tcall:clean\n) else if %1==build (\n\tcall:build\n)\nexit /b 0\n\n:build\ngo build -o xdb_maker.exe\nexit /b 0\n\n:clean\ndel/f/s/q xdb_maker.exe\n"
  },
  {
    "path": "maker/golang/xdb/editor.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// original source ip editor\n\npackage xdb\n\nimport (\n\t\"container/list\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n)\n\ntype Editor struct {\n\tverison *Version\n\n\t// source ip file\n\tsrcPath   string\n\tsrcHandle *os.File\n\ttoSave    bool\n\n\t// segments list\n\tsegments *list.List\n}\n\nfunc NewEditor(version *Version, srcFile string) (*Editor, error) {\n\t// check the src and dst file\n\tsrcPath, err := filepath.Abs(srcFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsrcHandle, err := os.OpenFile(srcPath, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\te := &Editor{\n\t\tverison:   version,\n\t\tsrcPath:   srcPath,\n\t\tsrcHandle: srcHandle,\n\t\ttoSave:    false,\n\t\tsegments:  list.New(),\n\t}\n\n\t// load the segments\n\tif err = e.loadSegments(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to load segments: %s\", err)\n\t}\n\n\treturn e, nil\n}\n\n// Load all the segments from the source file\nfunc (e *Editor) loadSegments() error {\n\tvar last *Segment = nil\n\tvar segments []*Segment\n\tvar sorting = false\n\n\t_, _, iErr := IterateSegments(e.srcHandle, true, func(l string) {\n\t\t// do nothing here\n\t}, nil, func(seg *Segment) error {\n\t\t// version check\n\t\tif len(seg.StartIP) != e.verison.Bytes {\n\t\t\treturn fmt.Errorf(\"invalid ip segment(%s expected)\", e.verison.Name)\n\t\t}\n\n\t\t// check the order of the data segment\n\t\t// if err := seg.RightBehind(last); err != nil {\n\t\tif err := seg.After(last); err != nil {\n\t\t\t// return err\n\t\t\t// @Note: If the continuity is disrupted,\n\t\t\t// we will sort all these segments later.\n\t\t\tsorting = true\n\t\t}\n\n\t\t// e.segments.PushBack(seg)\n\t\tsegments = append(segments, seg)\n\t\tlast = seg\n\t\treturn nil\n\t})\n\tif iErr != nil {\n\t\treturn iErr\n\t}\n\n\t// check and do the sorting\n\tif sorting {\n\t\tsort.Slice(segments, func(i, j int) bool {\n\t\t\treturn IPCompare(segments[i].StartIP, segments[j].StartIP) < 0\n\t\t})\n\n\t\t// open the to save\n\t\te.toSave = true\n\t}\n\n\t// check and fill in the discontinuous segments\n\t// to Keep the entire data continuous.\n\tlast = nil\n\tfor _, seg := range segments {\n\t\tif err := seg.After(last); err != nil {\n\t\t}\n\n\t\tif last == nil {\n\t\t\tif IPCompare(seg.StartIP, e.verison.Min) > 0 {\n\t\t\t\te.segments.PushBack(&Segment{\n\t\t\t\t\tStartIP: e.verison.Min,\n\t\t\t\t\tEndIP:   IPSubOne(seg.StartIP),\n\t\t\t\t\tRegion:  \"\",\n\t\t\t\t})\n\t\t\t}\n\t\t} else if err := seg.RightBehind(last); err == nil {\n\t\t\t// Do nothing here since it just right behind the last\n\t\t} else if err := seg.After(last); err != nil {\n\t\t\t// segments overlap\n\t\t\treturn fmt.Errorf(\"overlap checking: %w\", err)\n\t\t} else {\n\t\t\t// push the padding segments\n\t\t\te.segments.PushBack(&Segment{\n\t\t\t\tStartIP: IPAddOne(last.EndIP),\n\t\t\t\tEndIP:   IPSubOne(seg.StartIP),\n\t\t\t\tRegion:  \"\",\n\t\t\t})\n\t\t}\n\n\t\t// push the current segment\n\t\te.segments.PushBack(seg)\n\n\t\t// reset the last\n\t\tlast = seg\n\t}\n\n\t// check and padding the tailing segmnet\n\tif back := e.segments.Back(); back != nil {\n\t\tif IPCompare(e.verison.Max, back.Value.(*Segment).EndIP) > 0 {\n\t\t\te.segments.PushBack(&Segment{\n\t\t\t\tStartIP: IPAddOne(back.Value.(*Segment).EndIP),\n\t\t\t\tEndIP:   e.verison.Max,\n\t\t\t\tRegion:  \"\",\n\t\t\t})\n\t\t}\n\t}\n\n\tsegments = nil // let GC do it work\n\treturn nil\n}\n\nfunc (e *Editor) NeedSave() bool {\n\treturn e.toSave\n}\n\nfunc (e *Editor) SegLen() int {\n\treturn e.segments.Len()\n}\n\nfunc (e *Editor) Slice(offset int, size int) []*Segment {\n\tvar index = -1\n\tvar out []*Segment\n\tvar next *list.Element\n\tfor ele := e.segments.Front(); ele != nil; ele = next {\n\t\tnext = ele.Next()\n\t\ts, ok := ele.Value.(*Segment)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// offset match\n\t\tindex++\n\t\tif index < offset {\n\t\t\tcontinue\n\t\t}\n\n\t\tout = append(out, s)\n\t\tif len(out) >= size {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc (e *Editor) Put(ip string) (int, int, error) {\n\tseg, err := SegmentFrom(ip)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\treturn e.PutSegment(seg)\n}\n\n// PutSegment put the specified segment into the current segment list with\n// the following position relationships.\n// 1, A - fully contained like:\n// StartIP------seg.StartIP--------seg.EndIP----EndIP\n//\n//\t|------------------|\n//\n// 2, B - intersect like:\n// StartIP------seg.StartIP------EndIP------|\n//\n//\t|---------------------seg.EndIP\nfunc (e *Editor) PutSegment(seg *Segment) (int, int, error) {\n\tvar next *list.Element\n\tvar eList []*list.Element\n\tvar found, counter = false, 0\n\tfor ele := e.segments.Front(); ele != nil; ele = next {\n\t\tnext = ele.Next()\n\t\ts, ok := ele.Value.(*Segment)\n\t\tif !ok {\n\t\t\t// could this even be a case ?\n\t\t\treturn 0, 0, fmt.Errorf(\"type error: ele not a Segment ptr\")\n\t\t}\n\n\t\tcounter++\n\n\t\t// found the related segment\n\t\tif found {\n\t\t\t// just keep going\n\t\t} else if IPCompare(seg.StartIP, s.StartIP) >= 0 &&\n\t\t\tIPCompare(seg.StartIP, s.EndIP) <= 0 {\n\t\t\tfound = true\n\t\t} else {\n\t\t\tcontinue\n\t\t}\n\n\t\teList = append(eList, ele)\n\t\tif IPCompare(seg.EndIP, s.EndIP) <= 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(eList) == 0 {\n\t\t// could this even be a case ?\n\t\treturn 0, 0, fmt.Errorf(\"failed to find the related segment\")\n\t}\n\n\t// print for debug\n\t// for i, s := range eList {\n\t// \tfmt.Printf(\"ele %d: %s\\n\", i, s.Value.(*Segment))\n\t// }\n\n\t// segment split\n\tvar sList []*Segment\n\tvar head = eList[0].Value.(*Segment)\n\tif IPCompare(seg.StartIP, head.StartIP) > 0 {\n\t\tsList = append(sList, &Segment{\n\t\t\tStartIP: head.StartIP,\n\t\t\tEndIP:   IPSubOne(seg.StartIP),\n\t\t\tRegion:  head.Region,\n\t\t})\n\t}\n\n\t// append the new segment\n\tsList = append(sList, seg)\n\n\t// check and do the tailing segment append\n\tvar tail = eList[len(eList)-1].Value.(*Segment)\n\tif IPCompare(seg.EndIP, tail.EndIP) < 0 {\n\t\tsList = append(sList, &Segment{\n\t\t\tStartIP: IPAddOne(seg.EndIP),\n\t\t\tEndIP:   tail.EndIP,\n\t\t\tRegion:  tail.Region,\n\t\t})\n\t}\n\n\t// print for debug\n\t// for i, s := range sList {\n\t// \tfmt.Printf(\"%d: %s\\n\", i, s)\n\t// }\n\n\t// delete all the in-range segments and\n\tvar base *list.Element\n\tvar oldRows, newRows = len(eList), len(sList)\n\tfor _, ele := range eList {\n\t\tbase = ele.Next()\n\t\te.segments.Remove(ele)\n\t}\n\n\t// add all the new segments\n\tif base == nil {\n\t\tfor _, s := range sList {\n\t\t\te.segments.PushBack(s)\n\t\t}\n\t} else {\n\t\tfor _, s := range sList {\n\t\t\te.segments.InsertBefore(s, base)\n\t\t}\n\t}\n\n\t// open the to save flag\n\te.toSave = true\n\n\treturn oldRows, newRows, nil\n}\n\nfunc (e *Editor) PutFile(src string) (int, int, error) {\n\thandle, err := os.OpenFile(src, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tvar oldRows, newRows = 0, 0\n\t_, _, iErr := IterateSegments(handle, true, func(l string) {\n\t\t// do nothing here\n\t}, nil, func(seg *Segment) error {\n\t\to, n, err := e.PutSegment(seg)\n\t\tif err == nil {\n\t\t\toldRows += o\n\t\t\tnewRows += n\n\t\t}\n\n\t\treturn err\n\t})\n\tif iErr != nil {\n\t\treturn oldRows, newRows, iErr\n\t}\n\n\t_ = handle.Close()\n\treturn oldRows, newRows, nil\n}\n\nfunc (e *Editor) Save() error {\n\t// check the to-save flag\n\tif !e.toSave {\n\t\treturn fmt.Errorf(\"nothing changed\")\n\t}\n\n\tdstHandle, err := os.OpenFile(e.srcPath, os.O_WRONLY|os.O_TRUNC, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// loop and flush all the segments to the dstHandle\n\tvar next *list.Element\n\tfor ele := e.segments.Front(); ele != nil; ele = next {\n\t\tnext = ele.Next()\n\t\ts, ok := ele.Value.(*Segment)\n\t\tif !ok {\n\t\t\t// could this even be a case ?\n\t\t\tcontinue\n\t\t}\n\n\t\t// ignore the padded or empty segment\n\t\tif s.Region == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// var l = s.String()\n\t\t// _, err = dstHandle.WriteString(fmt.Sprintf(\"%s\\n\", l))\n\t\t_, err = fmt.Fprintln(dstHandle, s.String())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// close the handle\n\t// and close the to-save flag\n\t_ = dstHandle.Close()\n\te.toSave = false\n\n\treturn nil\n}\n\nfunc (e *Editor) Close() {\n\t_ = e.srcHandle.Close()\n}\n"
  },
  {
    "path": "maker/golang/xdb/index.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype IndexPolicy int\n\nconst (\n\tVectorIndexPolicy IndexPolicy = 1\n\tBTreeIndexPolicy  IndexPolicy = 2\n)\n\nfunc IndexPolicyFromString(str string) (IndexPolicy, error) {\n\tswitch strings.ToLower(str) {\n\tcase \"vector\":\n\t\treturn VectorIndexPolicy, nil\n\tcase \"btree\":\n\t\treturn BTreeIndexPolicy, nil\n\tdefault:\n\t\treturn VectorIndexPolicy, fmt.Errorf(\"invalid policy '%s'\", str)\n\t}\n}\n"
  },
  {
    "path": "maker/golang/xdb/maker.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ----\n// Ip2Region database v2.0 structure\n//\n// +----------------+-------------------+---------------+--------------+\n// | header space   | speed up index    |  data payload | block index  |\n// +----------------+-------------------+---------------+--------------+\n// | 256 bytes      | 512 KiB (fixed)   | dynamic size  | dynamic size |\n// +----------------+-------------------+---------------+--------------+\n//\n// 1. padding space : for header info like block index ptr, version, release date eg ... or any other temporary needs.\n// -- 2bytes: version number, different version means structure update,\n//\t\t\tit fixed to 2 before IPv6 supporting, then updated to 3 since IPv6 supporting\n// -- 2bytes: index algorithm code.\n// -- 4bytes: generate unix timestamp (version)\n// -- 4bytes: index block start ptr\n// -- 4bytes: index block end ptr\n// -- 2bytes: ip version number (4/6 since IPv6 supporting)\n// -- 2bytes: runtime ptr bytes\n//\n//\n// 2. data block: region or whatever data info.\n// 3. segment index block: binary index block.\n// 4. vector index block: fixed index info for block index search speedup.\n// space structure table:\n// -- 0   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 1   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 2   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- ...\n// -- 255 -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n//\n//\n// super block structure:\n// +-----------------------+----------------------+\n// | first index block ptr | last index block ptr |\n// +-----------------------+----------------------+\n//\n// data entry structure:\n// +--------------------+-----------------------+\n// | 2bytes (for desc)\t| dynamic length        |\n// +--------------------+-----------------------+\n//  data length   whatever in bytes\n//\n// index entry structure\n// +------------+-----------+---------------+------------+\n// | 4bytes     | 4bytes    | 2bytes        | 4 bytes    |\n// +------------+-----------+---------------+------------+\n//  start ip \t  end ip\t  data length     data ptr\n\npackage xdb\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n)\n\nconst (\n\tVersionNo         = 3 // since 2025/09/01 (IPv6 supporting)\n\tHeaderInfoLength  = 256\n\tVectorIndexRows   = 256\n\tVectorIndexCols   = 256\n\tVectorIndexSize   = 8 // in bytes\n\tRuntimePtrSize    = 4 // in bytes\n\tVectorIndexLength = VectorIndexRows * VectorIndexCols * VectorIndexSize\n)\n\ntype Maker struct {\n\tversion *Version\n\n\tsrcHandle *os.File\n\tdstHandle *os.File\n\n\t// self-define field index\n\tfields []int\n\n\tindexPolicy IndexPolicy\n\tsegments    []*Segment\n\tregionPool  map[string]uint32\n\tvectorIndex []byte\n}\n\nfunc NewMaker(version *Version, policy IndexPolicy, srcFile string, dstFile string, fields []int) (*Maker, error) {\n\t// open the source file with READONLY mode\n\tvar err error\n\tvar srcHandle *os.File\n\tif srcFile == \"\" {\n\t\tsrcHandle = nil\n\t} else {\n\t\tsrcHandle, err = os.OpenFile(srcFile, os.O_RDONLY, 0600)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"open source file `%s`: %w\", srcFile, err)\n\t\t}\n\t}\n\n\t// open the destination file with Read/Write mode\n\tdstHandle, err := os.OpenFile(dstFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open target file `%s`: %w\", dstFile, err)\n\t}\n\n\treturn &Maker{\n\t\tversion: version,\n\n\t\tsrcHandle: srcHandle,\n\t\tdstHandle: dstHandle,\n\n\t\t// fields filter index\n\t\tfields: fields,\n\n\t\tindexPolicy: policy,\n\t\tsegments:    []*Segment{},\n\t\tregionPool:  map[string]uint32{},\n\t\tvectorIndex: make([]byte, VectorIndexLength),\n\t}, nil\n}\n\nfunc (m *Maker) initDbHeader() error {\n\tslog.Info(\"try to init the db header ... \")\n\n\t_, err := m.dstHandle.Seek(0, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// make and write the header space\n\tvar header = make([]byte, 256)\n\n\t// 1, data version number\n\tbinary.LittleEndian.PutUint16(header, uint16(VersionNo))\n\n\t// 2, index policy code\n\tbinary.LittleEndian.PutUint16(header[2:], uint16(m.indexPolicy))\n\n\t// 3, generate unix timestamp\n\tbinary.LittleEndian.PutUint32(header[4:], uint32(time.Now().Unix()))\n\n\t// 4, index block start ptr\n\tbinary.LittleEndian.PutUint32(header[8:], uint32(0))\n\n\t// 5, index block end ptr\n\tbinary.LittleEndian.PutUint32(header[12:], uint32(0))\n\n\t// 6, ip version\n\tbinary.LittleEndian.PutUint16(header[16:], uint16(m.version.Id))\n\n\t// 7, runtime ptr bytes\n\tbinary.LittleEndian.PutUint16(header[18:], uint16(RuntimePtrSize))\n\n\t_, err = m.dstHandle.Write(header)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (m *Maker) loadSegments() error {\n\tslog.Info(\"try to load the segments ... \")\n\tvar last *Segment = nil\n\tvar tStart = time.Now()\n\tvar sorting = false\n\n\t_, mergeCount, iErr := IterateSegments(m.srcHandle, true, func(l string) {\n\t\tslog.Debug(\"loaded\", \"segment\", l)\n\t}, func(region string) (string, error) {\n\t\t// apply the field filter\n\t\treturn RegionFiltering(region, m.fields)\n\t}, func(seg *Segment) error {\n\t\t// ip version check\n\t\tif len(seg.StartIP) != m.version.Bytes {\n\t\t\treturn fmt.Errorf(\"invalid ip segment(%s expected)\", m.version.Name)\n\t\t}\n\n\t\t// check the order of the data segment\n\t\tif sorting {\n\t\t\t// just keep going\n\t\t} else if err := seg.After(last); err != nil {\n\t\t\t// return err\n\t\t\t// @Note: If the continuity is disrupted,\n\t\t\t// we will sort all these segments later.\n\t\t\tsorting = true\n\t\t}\n\n\t\tm.segments = append(m.segments, seg)\n\t\tlast = seg\n\t\treturn nil\n\t})\n\tif iErr != nil {\n\t\treturn fmt.Errorf(\"failed to load segments: %s\", iErr)\n\t}\n\n\t// check and do the sorting\n\tif sorting {\n\t\tslog.Info(\"try to sort all the segments based on its start ip ...\")\n\t\tsort.Slice(m.segments, func(i, j int) bool {\n\t\t\treturn IPCompare(m.segments[i].StartIP, m.segments[j].StartIP) < 0\n\t\t})\n\n\t\tslog.Info(\"try to check if there is overlap in the segments  ...\")\n\t\tlast = nil\n\t\tfor _, seg := range m.segments {\n\t\t\t// check the order of the data segment\n\t\t\tif err := seg.After(last); err != nil {\n\t\t\t\treturn fmt.Errorf(\"overlap checking: %w\", err)\n\t\t\t}\n\n\t\t\t// reset the last\n\t\t\tlast = seg\n\t\t}\n\t}\n\n\tslog.Info(\"all segments loaded\", \"length\", len(m.segments), \"merged\", mergeCount, \"sorting\", sorting, \"elapsed\", time.Since(tStart))\n\treturn nil\n}\n\n// Init the db binary file\nfunc (m *Maker) Init() error {\n\t// init the db header\n\terr := m.initDbHeader()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"init db header: %w\", err)\n\t}\n\n\t// load all the segments\n\tif m.srcHandle == nil {\n\t\t// do nothing here\n\t} else {\n\t\terr = m.loadSegments()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"load segments: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Append a new segment\nfunc (m *Maker) Append(seg *Segment) {\n\tm.segments = append(m.segments, seg)\n}\n\n// refresh the vector index of the specified ip\nfunc (m *Maker) setVectorIndex(ip []byte, ptr uint32) {\n\tvar segIdxSize = uint32(m.version.SegmentIndexSize)\n\tvar il0, il1 = int(ip[0]), int(ip[1])\n\tvar idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize\n\tvar sPtr = binary.LittleEndian.Uint32(m.vectorIndex[idx:])\n\tif sPtr == 0 {\n\t\tbinary.LittleEndian.PutUint32(m.vectorIndex[idx:], ptr)\n\t\tbinary.LittleEndian.PutUint32(m.vectorIndex[idx+4:], ptr+segIdxSize)\n\t} else {\n\t\tbinary.LittleEndian.PutUint32(m.vectorIndex[idx+4:], ptr+segIdxSize)\n\t}\n}\n\n// Start to make the binary file\nfunc (m *Maker) Start() error {\n\tif len(m.segments) < 1 {\n\t\treturn fmt.Errorf(\"empty segment list\")\n\t}\n\n\t// 1, write all the region/data to the binary file\n\t_, err := m.dstHandle.Seek(int64(HeaderInfoLength+VectorIndexLength), 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"seek to data first ptr: %w\", err)\n\t}\n\n\tslog.Info(\"try to write the data block ... \")\n\tfor _, seg := range m.segments {\n\t\tslog.Debug(\"try to write\", \"region\", seg.Region)\n\t\tptr, has := m.regionPool[seg.Region]\n\t\tif has {\n\t\t\tslog.Debug(\" --[Cached]\", \"ptr=\", ptr)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar region = []byte(seg.Region)\n\t\tif len(region) > 0xFFFF {\n\t\t\treturn fmt.Errorf(\"too long region info `%s`: should be less than %d bytes\", seg.Region, 0xFFFF)\n\t\t}\n\n\t\t// get the first ptr of the next region\n\t\tpos, err := m.dstHandle.Seek(0, 1)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"seek to current ptr: %w\", err)\n\t\t}\n\n\t\t// @TODO: remove this if the long ptr operation were supported\n\t\tif pos >= math.MaxUint32 {\n\t\t\treturn fmt.Errorf(\"region ptr exceed the max length of %d\", math.MaxUint32)\n\t\t}\n\n\t\t_, err = m.dstHandle.Write(region)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"write region '%s': %w\", seg.Region, err)\n\t\t}\n\n\t\tm.regionPool[seg.Region] = uint32(pos)\n\t\tslog.Debug(\" --[Added] with\", \"ptr\", pos)\n\t}\n\n\t// 2, write the index block and cache the super index block\n\tslog.Info(\"try to write the segment index block ... \")\n\tvar indexBuff = make([]byte, m.version.SegmentIndexSize)\n\tvar counter, startIndexPtr, endIndexPtr = 0, int64(-1), int64(-1)\n\tfor _, seg := range m.segments {\n\t\tdataPtr, has := m.regionPool[seg.Region]\n\t\tif !has {\n\t\t\treturn fmt.Errorf(\"missing ptr cache for region `%s`\", seg.Region)\n\t\t}\n\n\t\t// @Note: data length should be the length of bytes.\n\t\t// this works fine because of the string feature (byte sequence) of golang.\n\t\tvar dataLen = len(seg.Region)\n\t\tif dataLen < 1 {\n\t\t\t// @TODO: could this even be a case ?\n\t\t\t// \treturn fmt.Errorf(\"empty region info for segment '%s'\", seg)\n\t\t\t// Allow empty region info since 2024/09/24\n\t\t}\n\n\t\tvar _offset = 0\n\t\tvar segList = seg.Split()\n\t\tslog.Debug(\"try to index segment\", \"length\", len(segList), \"splits\", seg.String())\n\t\tfor _, s := range segList {\n\t\t\tpos, err := m.dstHandle.Seek(0, 1)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"seek to segment index block: %w\", err)\n\t\t\t}\n\n\t\t\t// @TODO: remove this if the long ptr operation were supported\n\t\t\tif pos >= math.MaxUint32 {\n\t\t\t\treturn fmt.Errorf(\"segment index ptr exceed the max length of %d\", math.MaxUint32)\n\t\t\t}\n\n\t\t\t// encode the segment index.\n\t\t\t// @Note by Leon at 2025/09/05:\n\t\t\t// This is a tough decision since the directly copy of the bytes will make everything simpler.\n\t\t\t// But in order to compatible with the old searcher implementation we had to keep encoding the IPv4 bytes with little endian.\n\t\t\t// @TODO: we may choose to use the big-endian byte order in the future.\n\t\t\t// But now compatibility is the most important !!!\n\n\t\t\tm.version.PutBytes(indexBuff[0:], s.StartIP)\n\t\t\tm.version.PutBytes(indexBuff[len(s.StartIP):], s.EndIP)\n\t\t\t_offset = len(s.StartIP) + len(s.EndIP)\n\t\t\tbinary.LittleEndian.PutUint16(indexBuff[_offset:], uint16(dataLen))\n\t\t\tbinary.LittleEndian.PutUint32(indexBuff[_offset+2:], dataPtr)\n\t\t\t_, err = m.dstHandle.Write(indexBuff)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"write segment index for '%s': %w\", s.String(), err)\n\t\t\t}\n\n\t\t\tslog.Debug(\"|-segment index\", \"counter\", counter, \"ptr\", pos, \"segment\", s.String())\n\t\t\tm.setVectorIndex(s.StartIP, uint32(pos))\n\t\t\tcounter++\n\n\t\t\t// check and record the start index ptr\n\t\t\tif startIndexPtr == -1 {\n\t\t\t\tstartIndexPtr = pos\n\t\t\t}\n\n\t\t\tendIndexPtr = pos\n\t\t}\n\t}\n\n\t// synchronized the vector index block\n\tslog.Info(\"try to write the vector index block ... \")\n\t_, err = m.dstHandle.Seek(int64(HeaderInfoLength), 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"seek vector index first ptr: %w\", err)\n\t}\n\t_, err = m.dstHandle.Write(m.vectorIndex)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write vector index: %w\", err)\n\t}\n\n\t// synchronized the segment index info\n\tslog.Info(\"try to write the segment index ptr ... \")\n\tbinary.LittleEndian.PutUint32(indexBuff, uint32(startIndexPtr))\n\tbinary.LittleEndian.PutUint32(indexBuff[4:], uint32(endIndexPtr))\n\t_, err = m.dstHandle.Seek(8, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"seek segment index ptr: %w\", err)\n\t}\n\n\t_, err = m.dstHandle.Write(indexBuff[:8])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write segment index ptr: %w\", err)\n\t}\n\n\tslog.Info(\"write done\", \"dataBlocks\", len(m.regionPool), \"indexBlocks\", len(m.segments),\n\t\t\"counter\", counter, \"startIndexPtr\", startIndexPtr, \"endIndexPtr\", endIndexPtr)\n\n\treturn nil\n}\n\nfunc (m *Maker) End() error {\n\terr := m.dstHandle.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = m.srcHandle.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "maker/golang/xdb/processor.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// original source ip processor\n\npackage xdb\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype Processor struct {\n\tsrcHandle *os.File\n\tdstHandle *os.File\n\n\t// value clear\n\tclearBasedIndex  int\n\tclearValueEqual  string\n\tclearValueExcept string\n\n\tfields   []int\n\tsegments []*Segment\n}\n\nfunc NewProcessor(srcFile string, dstFile string, fields []int,\n\tclearBasedIndex int, clearValueEqual string, clearValueExcept string) (*Processor, error) {\n\t// open the source file with READONLY mode\n\tsrcHandle, err := os.OpenFile(srcFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open source file `%s`: %w\", srcFile, err)\n\t}\n\n\t// open the destination file with Read/Write mode\n\tdstHandle, err := os.OpenFile(dstFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open target file `%s`: %w\", dstFile, err)\n\t}\n\n\treturn &Processor{\n\t\tsrcHandle: srcHandle,\n\t\tdstHandle: dstHandle,\n\n\t\t// clear\n\t\tclearBasedIndex:  clearBasedIndex,\n\t\tclearValueEqual:  clearValueEqual,\n\t\tclearValueExcept: clearValueExcept,\n\n\t\t// filter fields index\n\t\tfields: fields,\n\n\t\tsegments: []*Segment{},\n\t}, nil\n}\n\nfunc (p *Processor) loadSegments() error {\n\tslog.Info(\"try to load the segments ... \")\n\tvar tStart = time.Now()\n\n\t_, mergeCount, iErr := IterateSegments(p.srcHandle, true, func(l string) {\n\t\tslog.Debug(\"loaded\", \"segment\", l)\n\t}, func(region string) (string, error) {\n\t\tif p.clearBasedIndex > -1 {\n\t\t\tvar ps = strings.Split(region, \"|\")\n\t\t\tvar pl = len(ps)\n\t\t\tif p.clearBasedIndex >= pl {\n\t\t\t\treturn region, fmt.Errorf(\"clearBasedIndex(%d) >= fields length(%d)\", p.clearBasedIndex, pl)\n\t\t\t}\n\n\t\t\tclear := false\n\t\t\tif len(p.clearValueEqual) > 0 {\n\t\t\t\tif ps[p.clearBasedIndex] == p.clearValueEqual {\n\t\t\t\t\tclear = true\n\t\t\t\t}\n\t\t\t} else if len(p.clearValueExcept) > 0 {\n\t\t\t\tif ps[p.clearBasedIndex] != p.clearValueExcept {\n\t\t\t\t\tclear = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif clear {\n\t\t\t\tfor i := 0; i < pl; i++ {\n\t\t\t\t\tps[i] = \"\"\n\t\t\t\t}\n\n\t\t\t\t// reset the region\n\t\t\t\tregion = strings.Join(ps, \"|\")\n\t\t\t}\n\t\t}\n\n\t\treturn RegionFiltering(region, p.fields)\n\t}, func(seg *Segment) error {\n\t\t// check the continuity of the data segment\n\t\t// if err := seg.AfterCheck(last); err != nil {\n\t\t// \treturn err\n\t\t// }\n\n\t\t// slog.Info(\"filtered\", \"source\", seg.Region, \"filtered\", region)\n\t\tp.segments = append(p.segments, seg)\n\t\treturn nil\n\t})\n\tif iErr != nil {\n\t\treturn fmt.Errorf(\"failed to load segments: %s\", iErr)\n\t}\n\n\tslog.Info(\"all segments loaded\", \"length\", len(p.segments), \"merged\", mergeCount, \"elapsed\", time.Since(tStart))\n\treturn nil\n}\n\n// Init the db binary file\nfunc (p *Processor) Init() error {\n\t// load all the segments\n\terr := p.loadSegments()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"load segments: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *Processor) Start() error {\n\tslog.Info(\"try to sort all the segments based on its start ip ...\")\n\tsort.Slice(p.segments, func(i, j int) bool {\n\t\treturn IPCompare(p.segments[i].StartIP, p.segments[j].StartIP) < 0\n\t})\n\n\tslog.Info(\"try to write all segments to target file ...\")\n\tfor _, seg := range p.segments {\n\t\t_, err := fmt.Fprintln(p.dstHandle, seg.String())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"write segment index for '%s': %w\", seg.String(), err)\n\t\t}\n\t}\n\n\tslog.Info(\"process done\", \"segments\", len(p.segments))\n\treturn nil\n}\n\nfunc (p *Processor) End() error {\n\terr := p.dstHandle.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = p.srcHandle.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "maker/golang/xdb/searcher.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\n// ---\n// Ip2Region database v2.0 searcher.\n// this is part of the maker for testing and validate.\n// please use the searcher in binding/golang for production use.\n// And this is a Not thread safe implementation.\n\npackage xdb\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n)\n\ntype Searcher struct {\n\tversion *Version\n\n\thandle *os.File\n\n\t// header info\n\theader []byte\n\n\t// use it only when this feature enabled.\n\t// Preload the vector index will reduce the number of IO operations\n\t// thus speedup the search process\n\tvectorIndex []byte\n}\n\nfunc NewSearcher(version *Version, dbFile string) (*Searcher, error) {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Searcher{\n\t\tversion: version,\n\n\t\thandle: handle,\n\t\theader: nil,\n\n\t\tvectorIndex: nil,\n\t}, nil\n}\n\nfunc (s *Searcher) Close() {\n\tif s.handle != nil {\n\t\terr := s.handle.Close()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// LoadVectorIndex load and cache the vector index for search speedup.\n// this will take up VectorIndexRows x VectorIndexCols x VectorIndexSize bytes memory.\nfunc (s *Searcher) LoadVectorIndex() error {\n\t// loaded already\n\tif s.vectorIndex != nil {\n\t\treturn nil\n\t}\n\n\t// load all the vector index block\n\t_, err := s.handle.Seek(HeaderInfoLength, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"seek to vector index: %w\", err)\n\t}\n\n\tvar buff = make([]byte, VectorIndexLength)\n\trLen, err := s.handle.Read(buff)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif rLen != len(buff) {\n\t\treturn fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t}\n\n\ts.vectorIndex = buff\n\treturn nil\n}\n\n// ClearVectorIndex clear preloaded vector index cache\nfunc (s *Searcher) ClearVectorIndex() {\n\ts.vectorIndex = nil\n}\n\n// Search find the region for the specified ip address\nfunc (s *Searcher) Search(ip []byte) (string, int, error) {\n\t// version check\n\tif len(ip) != s.version.Bytes {\n\t\treturn \"\", 0, fmt.Errorf(\"invalid ip address(%s expected)\", s.version.Name)\n\t}\n\n\t// locate the segment index block based on the vector index\n\tvar ioCount = 0\n\tvar il0, il1, bytes, tBytes = int(ip[0]), int(ip[1]), len(ip), len(ip) << 1\n\tvar idx = il0*VectorIndexCols*VectorIndexSize + il1*VectorIndexSize\n\tvar sPtr, ePtr = uint32(0), uint32(0)\n\tif s.vectorIndex != nil {\n\t\tsPtr = binary.LittleEndian.Uint32(s.vectorIndex[idx:])\n\t\tePtr = binary.LittleEndian.Uint32(s.vectorIndex[idx+4:])\n\t} else {\n\t\tpos, err := s.handle.Seek(int64(HeaderInfoLength+idx), 0)\n\t\tif err != nil {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"seek to vector index %d: %w\", HeaderInfoLength+idx, err)\n\t\t}\n\n\t\tioCount++\n\t\tvar buff = make([]byte, VectorIndexSize)\n\t\trLen, err := s.handle.Read(buff)\n\t\tif err != nil {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"read vector index at %d: %w\", pos, err)\n\t\t}\n\n\t\tif rLen != len(buff) {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t\t}\n\n\t\tsPtr = binary.LittleEndian.Uint32(buff)\n\t\tePtr = binary.LittleEndian.Uint32(buff[4:])\n\t}\n\n\t//log.Printf(\"vIndex=%s\", vIndex)\n\tif sPtr == 0 || ePtr == 0 {\n\t\treturn \"\", ioCount, nil\n\t}\n\n\t// binary search the segment index to get the region\n\tvar segIndexSize = uint32(s.version.SegmentIndexSize)\n\tvar dataLen, dataPtr = 0, uint32(0)\n\tvar buff = make([]byte, segIndexSize)\n\tvar l, h = 0, int((ePtr - sPtr) / segIndexSize)\n\tfor l <= h {\n\t\t// log.Printf(\"l=%d, h=%d\", l, h)\n\t\tm := (l + h) >> 1\n\t\tp := sPtr + uint32(m)*segIndexSize\n\t\t// log.Printf(\"m=%d, p=%d\", m, p)\n\t\t_, err := s.handle.Seek(int64(p), 0)\n\t\tif err != nil {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"seek to segment block at %d: %w\", p, err)\n\t\t}\n\n\t\tioCount++\n\t\trLen, err := s.handle.Read(buff)\n\t\tif err != nil {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"read segment index at %d: %w\", p, err)\n\t\t}\n\n\t\tif rLen != len(buff) {\n\t\t\treturn \"\", ioCount, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t\t}\n\n\t\t// decode the data step by step to reduce the unnecessary calculations\n\t\tif s.version.IPCompare(ip, buff[0:bytes]) < 0 {\n\t\t\th = m - 1\n\t\t} else if s.version.IPCompare(ip, buff[bytes:tBytes]) > 0 {\n\t\t\tl = m + 1\n\t\t} else {\n\t\t\tdataLen = int(binary.LittleEndian.Uint16(buff[tBytes:]))\n\t\t\tdataPtr = binary.LittleEndian.Uint32(buff[tBytes+2:])\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif dataLen == 0 {\n\t\treturn \"\", ioCount, nil\n\t}\n\n\t// load and return the region data\n\t_, err := s.handle.Seek(int64(dataPtr), 0)\n\tif err != nil {\n\t\treturn \"\", ioCount, fmt.Errorf(\"seek to data block at %d: %w\", dataPtr, err)\n\t}\n\n\tioCount++\n\tvar regionBuff = make([]byte, dataLen)\n\trLen, err := s.handle.Read(regionBuff)\n\tif err != nil {\n\t\treturn \"\", ioCount, fmt.Errorf(\"read region data at %d: %w\", dataPtr, err)\n\t}\n\n\tif rLen != dataLen {\n\t\treturn \"\", ioCount, fmt.Errorf(\"incomplete read: readed bytes should be %d\", dataLen)\n\t}\n\n\treturn string(regionBuff), ioCount, nil\n}\n\n// LoadXdbHeader load the header info from the specified handle\nfunc LoadXdbHeader(handle *os.File) ([]byte, error) {\n\t_, err := handle.Seek(0, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"seek to the header: %w\", err)\n\t}\n\n\tvar buff = make([]byte, HeaderInfoLength)\n\trLen, err := handle.Read(buff)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif rLen != len(buff) {\n\t\treturn nil, fmt.Errorf(\"incomplete read: readed bytes should be %d\", len(buff))\n\t}\n\n\treturn buff, nil\n}\n\n// LoadXdbHeaderFromFile load header info from the specified db file path\nfunc LoadXdbHeaderFromFile(dbFile string) ([]byte, error) {\n\thandle, err := os.OpenFile(dbFile, os.O_RDONLY, 0600)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open xdb file `%s`: %w\", dbFile, err)\n\t}\n\n\tdefer func(handle *os.File) {\n\t\t_ = handle.Close()\n\t}(handle)\n\n\theader, err := LoadXdbHeader(handle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn header, nil\n}\n"
  },
  {
    "path": "maker/golang/xdb/segment.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype Segment struct {\n\tStartIP []byte\n\tEndIP   []byte\n\tRegion  string\n}\n\nfunc SegmentFrom(seg string) (*Segment, error) {\n\tvar ps = strings.SplitN(strings.TrimSpace(seg), \"|\", 3)\n\tif len(ps) != 3 {\n\t\treturn nil, fmt.Errorf(\"invalid ip segment `%s`\", seg)\n\t}\n\n\tsip, err := ParseIP(ps[0])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"check start ip `%s`: %s\", ps[0], err)\n\t}\n\n\teip, err := ParseIP(ps[1])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"check end ip `%s`: %s\", ps[1], err)\n\t}\n\n\tif IPCompare(sip, eip) > 0 {\n\t\treturn nil, fmt.Errorf(\"start ip(%s) should not be greater than end ip(%s)\", ps[0], ps[1])\n\t}\n\n\treturn &Segment{\n\t\tStartIP: sip,\n\t\tEndIP:   eip,\n\t\tRegion:  ps[2],\n\t}, nil\n}\n\n// RightBehind check the current segment is just right behind the specified one\n// which mean last.EndIP + 1 = s.startIP\nfunc (s *Segment) RightBehind(last *Segment) error {\n\tif last != nil {\n\t\tif IPCompare(IPAddOne(last.EndIP), s.StartIP) != 0 {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"discontinuous data segment: last.eip(%s)+1 != seg.sip(%s, %s)\",\n\t\t\t\tIP2String(last.EndIP), IP2String(s.StartIP), s.Region,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// After check the current segment is after the specified one\n// which means last.EndIP < s.startIP\nfunc (s *Segment) After(last *Segment) error {\n\tif last != nil {\n\t\tif IPCompare(last.EndIP, s.StartIP) >= 0 {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"disorder data segment: last.eip(%s) >= seg.sip(%s, %s)\",\n\t\t\t\tIP2String(last.EndIP), IP2String(s.StartIP), s.Region,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Split the segment based on the pre-two bytes\nfunc (s *Segment) Split() []*Segment {\n\t// 1, split the segment with the first byte\n\tvar tList []*Segment\n\tvar sByte1, eByte1 = int(s.StartIP[0]), int(s.EndIP[0])\n\t// var nSip = s.StartIP\n\tfor i := sByte1; i <= eByte1; i++ {\n\t\t// Make and init the new start & end IP\n\t\tsip := make([]byte, len(s.StartIP))\n\t\teip := make([]byte, len(s.StartIP))\n\n\t\tif i == sByte1 {\n\t\t\tsip = s.StartIP\n\t\t} else {\n\t\t\tsip[0] = byte(i)\n\t\t}\n\n\t\tif i == eByte1 {\n\t\t\teip = s.EndIP\n\t\t} else {\n\t\t\t// set the first byte\n\t\t\teip[0] = byte(i)\n\t\t\t// fill the buffer with 0xFF\n\t\t\tfor j := 1; j < len(eip); j++ {\n\t\t\t\teip[j] = 0xFF\n\t\t\t}\n\t\t}\n\n\t\t// sip := (i << 24) | (nSip & 0xFFFFFF)\n\t\t// eip := (i << 24) | 0xFFFFFF\n\t\t// if eip < s.EndIP {\n\t\t// \tnSip = (i + 1) << 24\n\t\t// } else {\n\t\t// \teip = s.EndIP\n\t\t// }\n\n\t\t// fmt.Printf(\"sip:%+v, eip: %+v\\n\", sip, eip)\n\t\t// append the new segment (maybe)\n\t\ttList = append(tList, &Segment{\n\t\t\tStartIP: sip,\n\t\t\tEndIP:   eip,\n\t\t\t// @Note: don't bother to copy the region\n\t\t\t/// Region: s.Region,\n\t\t})\n\t}\n\n\t// 2, split the segments with the second byte\n\tvar segList []*Segment\n\tfor _, seg := range tList {\n\t\t// base := seg.StartIP & 0xFF000000\n\t\t// nSip := seg.StartIP\n\t\t// sb2, eb2 := (seg.StartIP>>16)&0xFF, (seg.EndIP>>16)&0xFF\n\t\tsb2, eb2 := int(seg.StartIP[1]), int(seg.EndIP[1])\n\t\t// fmt.Printf(\"seg: %s, sb2: %d, eb2: %d\\n\", seg.String(), sb2, eb2)\n\t\tfor i := sb2; i <= eb2; i++ {\n\t\t\t// sip := base | (i << 16) | (nSip & 0xFFFF)\n\t\t\t// eip := base | (i << 16) | 0xFFFF\n\t\t\t// if eip < seg.EndIP {\n\t\t\t// \tnSip = 0\n\t\t\t// } else {\n\t\t\t// \teip = seg.EndIP\n\t\t\t// }\n\n\t\t\tsip := make([]byte, len(s.StartIP))\n\t\t\teip := make([]byte, len(s.StartIP))\n\t\t\tsip[0] = seg.StartIP[0]\n\t\t\teip[0] = seg.StartIP[0]\n\n\t\t\tif i == sb2 {\n\t\t\t\tsip = seg.StartIP\n\t\t\t} else {\n\t\t\t\tsip[1] = byte(i)\n\t\t\t}\n\n\t\t\tif i == eb2 {\n\t\t\t\teip = seg.EndIP\n\t\t\t} else {\n\t\t\t\teip[1] = byte(i)\n\t\t\t\tfor j := 2; j < len(eip); j++ {\n\t\t\t\t\teip[j] = 0xFF\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// fmt.Printf(\"i=%d, sip:%+v, eip: %+v\\n\", i, sip, eip)\n\t\t\tsegList = append(segList, &Segment{\n\t\t\t\tStartIP: sip,\n\t\t\t\tEndIP:   eip,\n\t\t\t\tRegion:  s.Region,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn segList\n}\n\nfunc (s *Segment) String() string {\n\treturn fmt.Sprintf(\"%s|%s|%s\", IP2String(s.StartIP), IP2String(s.EndIP), s.Region)\n}\n\n// Contains checks if an IP address is within this segment\nfunc (s *Segment) Contains(ip []byte) bool {\n\treturn IPCompare(s.StartIP, ip) <= 0 && IPCompare(ip, s.EndIP) <= 0\n}\n"
  },
  {
    "path": "maker/golang/xdb/util.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n)\n\n// Util function\n\nfunc ParseIP(ip string) ([]byte, error) {\n\tparsedIP := net.ParseIP(ip)\n\tif parsedIP == nil {\n\t\treturn nil, fmt.Errorf(\"invalid ip address: %s\", ip)\n\t}\n\n\tv4 := parsedIP.To4()\n\tif v4 != nil {\n\t\treturn v4, nil\n\t}\n\n\tv6 := parsedIP.To16()\n\tif v6 != nil {\n\t\treturn v6, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"invalid ip address: %s\", ip)\n}\n\nfunc IP2String(ip []byte) string {\n\treturn net.IP(ip[:]).String()\n}\n\nfunc IP2Long(ip []byte) *big.Int {\n\treturn big.NewInt(0).SetBytes(ip)\n}\n\n// IPCompare compares two IP addresses\n// Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\nfunc IPCompare(ip1, ip2 []byte) int {\n\t// for i := 0; i < len(ip1); i++ {\n\t// \tif ip1[i] < ip2[i] {\n\t// \t\treturn -1\n\t// \t}\n\n\t// \tif ip1[i] > ip2[i] {\n\t// \t\treturn 1\n\t// \t}\n\t// }\n\n\t// return 0\n\treturn bytes.Compare(ip1, ip2)\n}\n\nfunc IPAddOne(ip []byte) []byte {\n\tvar r = make([]byte, len(ip))\n\tcopy(r, ip)\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\tr[i]++\n\t\tif r[i] != 0 { // No overflow\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc IPSubOne(ip []byte) []byte {\n\tvar r = make([]byte, len(ip))\n\tcopy(r, ip)\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\tif r[i] != 0 { // No borrow needed\n\t\t\tr[i]--\n\t\t\tbreak\n\t\t}\n\t\tr[i] = 0xFF // borrow from the next byte\n\t}\n\n\treturn r\n}\n\n// IPSub Sub the spcecified two byte ip\nfunc IPSub(sip, eip []byte) ([]byte, error) {\n\tif len(sip) != len(eip) {\n\t\treturn []byte{}, fmt.Errorf(\"length of the two ips are not the same\")\n\t}\n\n\tvar carry uint16 = 0\n\tvar result = make([]byte, len(sip)+1)\n\n\tfor i := len(sip) - 1; i >= 0; i-- {\n\t\tsum := uint16(sip[i]) + uint16(eip[i]) + carry\n\t\tresult[i+1] = byte(sum) // Store standard 8-bit result\n\t\tcarry = sum >> 8        // Extract the 1-bit carry for the next byte\n\t}\n\n\t// check and append the carry\n\tif carry > 0 {\n\t\tresult[0] = byte(carry)\n\t\treturn result, nil\n\t} else {\n\t\treturn result[1:], nil\n\t}\n}\n\n// IPHalf get the half value of an input byte ip\nfunc IPHalf(ip []byte) []byte {\n\tvar length = len(ip)\n\tvar result = make([]byte, length)\n\t// Tracks the bit falling off from the previous byte\n\tvar carry byte = 0\n\n\tfor i := 0; i < length; i++ {\n\t\t// 1. Shift current byte right by 1\n\t\t// 2. Or (|) with the carry from the previous byte (shifted to the MSB position)\n\t\tresult[i] = (ip[i] >> 1) | (carry << 7)\n\n\t\t// 3. Capture the Least Significant Bit (LSB) to use as carry for the next byte\n\t\tcarry = ip[i] & 1\n\t}\n\n\treturn result\n}\n\n// IPMiddle get the middle value of two input ip address\nfunc IPMiddle(sip, eip []byte) ([]byte, error) {\n\tbuf, err := IPSub(sip, eip)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"IPSub(%s, %s): %w\", IP2String(sip), IP2String(eip), err)\n\t}\n\n\treturn IPHalf(buf), nil\n}\n\nfunc IterateSegments(handle *os.File, autoMerge bool, before func(l string), filter func(region string) (string, error), done func(seg *Segment) error) (int, int, error) {\n\tvar last *Segment = nil\n\tvar totalCount, mergeCount = 0, 0\n\tvar scanner = bufio.NewScanner(handle)\n\tscanner.Split(bufio.ScanLines)\n\tfor scanner.Scan() {\n\t\tvar l = strings.TrimSpace(strings.TrimSuffix(scanner.Text(), \"\\n\"))\n\t\tif len(l) < 1 { // ignore empty line\n\t\t\tcontinue\n\t\t}\n\n\t\tif l[0] == '#' { // ignore the comment line\n\t\t\tcontinue\n\t\t}\n\n\t\ttotalCount++\n\t\tif before != nil {\n\t\t\tbefore(l)\n\t\t}\n\n\t\tvar ps = strings.SplitN(l, \"|\", 3)\n\t\tif len(ps) != 3 {\n\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"invalid ip segment line `%s`\", l)\n\t\t}\n\n\t\tsip, err := ParseIP(ps[0])\n\t\tif err != nil {\n\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"check start ip `%s`: %s\", ps[0], err)\n\t\t}\n\n\t\teip, err := ParseIP(ps[1])\n\t\tif err != nil {\n\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"check end ip `%s`: %s\", ps[1], err)\n\t\t}\n\n\t\tif len(sip) != len(eip) {\n\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"invalid ip segment line `%s`, sip/eip version not match\", l)\n\t\t}\n\n\t\tif IPCompare(sip, eip) > 0 {\n\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"start ip(%s) should not be greater than end ip(%s)\", ps[0], ps[1])\n\t\t}\n\n\t\t// Allow empty region info since 2024/09/24\n\t\t// if len(ps[2]) < 1 {\n\t\t// \treturn fmt.Errorf(\"empty region info in segment line `%s`\", l)\n\t\t// }\n\n\t\t// check and do the region filter\n\t\tvar region = ps[2]\n\t\tif filter != nil {\n\t\t\tregion, err = filter(ps[2])\n\t\t\tif err != nil {\n\t\t\t\treturn totalCount, mergeCount, fmt.Errorf(\"failed to filter region `%s`: %s\", ps[2], err)\n\t\t\t}\n\t\t}\n\n\t\tvar seg = &Segment{\n\t\t\tStartIP: sip,\n\t\t\tEndIP:   eip,\n\t\t\tRegion:  region,\n\t\t}\n\n\t\t// check and automatic merging the Consecutive Segments, which means:\n\t\t// 1, region info is the same\n\t\t// 2, last.eip+1 = cur.sip\n\t\tif last == nil {\n\t\t\tlast = seg\n\t\t\tcontinue\n\t\t} else if autoMerge && last.Region == seg.Region {\n\t\t\tif err = seg.RightBehind(last); err == nil {\n\t\t\t\tmergeCount++\n\t\t\t\tlast.EndIP = seg.EndIP\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif err = done(last); err != nil {\n\t\t\treturn totalCount, mergeCount, err\n\t\t}\n\n\t\t// reset the last\n\t\tlast = seg\n\t}\n\n\t// process the last segment\n\tif last != nil {\n\t\treturn totalCount, mergeCount, done(last)\n\t}\n\n\treturn totalCount, mergeCount, nil\n}\n\nfunc CheckSegments(segList []*Segment) error {\n\tvar last *Segment\n\tfor _, seg := range segList {\n\t\t// sip must <= eip\n\t\tif IPCompare(seg.StartIP, seg.EndIP) > 0 {\n\t\t\treturn fmt.Errorf(\"segment `%s`: start ip should not be greater than end ip\", seg.String())\n\t\t}\n\n\t\t// check the continuity of the data segment\n\t\tif last != nil {\n\t\t\tif IPCompare(IPAddOne(last.EndIP), seg.StartIP) != 0 {\n\t\t\t\treturn fmt.Errorf(\"discontinuous segment `%s`: last.eip+1 != cur.sip\", seg.String())\n\t\t\t}\n\t\t}\n\n\t\tlast = seg\n\t}\n\n\treturn nil\n}\n\nfunc RegionFiltering(region string, fields []int) (string, error) {\n\tif len(fields) == 0 {\n\t\treturn region, nil\n\t}\n\n\tfs := strings.Split(region, \"|\")\n\tvar sb []string\n\tfor _, idx := range fields {\n\t\tif idx < 0 {\n\t\t\treturn \"\", fmt.Errorf(\"negative filter index %d\", idx)\n\t\t}\n\n\t\tif idx >= len(fs) {\n\t\t\treturn \"\", fmt.Errorf(\"field index %d exceeded the max length of %d\", idx, len(fs))\n\t\t}\n\n\t\tsb = append(sb, fs[idx])\n\t}\n\n\treturn strings.Join(sb, \"|\"), nil\n}\n"
  },
  {
    "path": "maker/golang/xdb/util_test.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestParseIP(t *testing.T) {\n\tvar ips = []string{\"29.34.191.255\", \"2c0f:fff0::\", \"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"}\n\tfor _, ip := range ips {\n\t\tbytes, err := ParseIP(ip)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"check ip `%s`: %s\\n\", IP2String(bytes), err)\n\t\t}\n\n\t\tnip := IP2String(bytes)\n\t\tfmt.Printf(\"checkip: (%s / %s), isEqual: %v\\n\", ip, nip, ip == nip)\n\t}\n}\n\nfunc TestIPCompare(t *testing.T) {\n\tvar ipPairs = [][]string{\n\t\t{\"1.2.3.4\", \"1.2.3.5\"},\n\t\t{\"58.250.36.41\", \"58.250.30.41\"},\n\t\t{\"2c10::\", \"2e00::\"},\n\t\t{\"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"},\n\t\t{\"fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"fe00::\"},\n\t}\n\n\tfor _, pairs := range ipPairs {\n\t\tfmt.Printf(\"IPCompare(%s, %s): %d\\n\", pairs[0], pairs[1], IPCompare([]byte(pairs[0]), []byte(pairs[1])))\n\t}\n}\n\nfunc TestIPAddOne(t *testing.T) {\n\tvar ipPairs = [][]string{\n\t\t{\"1.2.3.4\", \"1.2.3.5\"},\n\t\t{\"2.3.4.5\", \"2.3.4.6\"},\n\t\t{\"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"fe00::\"},\n\t\t{\"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"3000::\"},\n\t\t{\"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"3000::1\"},\n\t}\n\n\tfor _, pairs := range ipPairs {\n\t\tsip, err := ParseIP(pairs[0])\n\t\tif err != nil {\n\t\t\tt.Errorf(\"parse ip `%s`: %s\\n\", pairs[0], err)\n\t\t}\n\n\t\teip, err := ParseIP(pairs[1])\n\t\tif err != nil {\n\t\t\tt.Errorf(\"parse ip `%s`: %s\\n\", pairs[1], err)\n\t\t}\n\n\t\tfmt.Printf(\"IPAddOne(%s) = %s ? %d\\n\",\n\t\t\tpairs[0], pairs[1], IPCompare(IPAddOne(sip), eip))\n\t}\n}\n\nfunc TestIPAddOne2(t *testing.T) {\n\tvar ip = []byte{0, 1, 2, 3}\n\tnip := IPAddOne(ip)\n\tfmt.Printf(\"nip: %+v, ip:%+v\", ip, nip)\n}\n\nfunc TestIPSubOne(t *testing.T) {\n\tvar ipPairs = [][]string{\n\t\t{\"1.2.3.4\", \"1.2.3.5\"},\n\t\t{\"2.3.4.5\", \"2.3.4.6\"},\n\t\t{\"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"fe00::\"},\n\t\t{\"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"3000::\"},\n\t\t{\"2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\", \"3000::1\"},\n\t}\n\n\tfor _, pairs := range ipPairs {\n\t\tsip, err := ParseIP(pairs[0])\n\t\tif err != nil {\n\t\t\tt.Errorf(\"parse ip `%s`: %s\\n\", pairs[0], err)\n\t\t}\n\n\t\teip, err := ParseIP(pairs[1])\n\t\tif err != nil {\n\t\t\tt.Errorf(\"parse ip `%s`: %s\\n\", pairs[1], err)\n\t\t}\n\n\t\tfmt.Printf(\"IPSubOne(%s) = %s ? %d\\n\",\n\t\t\tpairs[1], pairs[0], IPCompare(IPSubOne(eip), sip))\n\t}\n}\n\nfunc TestIPSubOne2(t *testing.T) {\n\tvar ip = []byte{0, 1, 2, 3}\n\tnip := IPSubOne(ip)\n\tfmt.Printf(\"nip: %+v, ip:%+v\", ip, nip)\n}\n\nfunc TestIPSub(t *testing.T) {\n\tvar strToSub = \"1.2.3.4\"\n\tbytesToSub, err := ParseIP(strToSub)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse ip %s\", strToSub)\n\t}\n\tvar intToSub = int(binary.BigEndian.Uint32(bytesToSub))\n\tt.Logf(\"to sub ip: %d -> %s\", intToSub, strToSub)\n\n\tcounter := 0\n\tbuf := make([]byte, 4)\n\tfor i := 0; i < 0x2FFFFFFF; i++ {\n\t\tbinary.BigEndian.PutUint32(buf, uint32(i))\n\t\tsubVal, err := IPSub(buf, bytesToSub)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to IPSub(%s,%s): %s\", IP2String(buf), strToSub, err)\n\t\t}\n\n\t\t// do it as two integers\n\t\tbyteSub := int(binary.BigEndian.Uint32(subVal))\n\t\tintSub := i + intToSub\n\t\tif byteSub != intSub {\n\t\t\tt.Fatal(\"byte and int sub value are not the same\")\n\t\t}\n\n\t\tcounter++\n\t}\n\n\tt.Logf(\"test done with %d ips\", counter)\n}\n\nfunc TestIPHalf(t *testing.T) {\n\tvar buf = make([]byte, 4)\n\tfor i := 0; i < 0xFFFFFFFF; i++ {\n\t\tbinary.BigEndian.PutUint32(buf, uint32(i))\n\t\thalf := IPHalf(buf)\n\n\t\t// do it as two integers\n\t\tbyteMiddle := binary.BigEndian.Uint32(half)\n\t\tintMidle := i >> 1\n\t\tif byteMiddle != uint32(intMidle) {\n\t\t\tt.Fatal(\"byte middle and int middle are not the same\")\n\t\t}\n\t}\n}\n\nfunc TestSubOverflow(t *testing.T) {\n\tvar ip1Str = \"255.255.255.250\"\n\tip1Bytes, err := ParseIP(ip1Str)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to ParseIP(%s): %s\", ip1Str, err)\n\t}\n\n\tvar buff = make([]byte, 4)\n\tfor i := 0; i < 10; i++ {\n\t\tbinary.BigEndian.PutUint32(buff, uint32(i))\n\t\tipSub, err := IPSub(ip1Bytes, buff)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to IPSub(%s, %s): %s\", ip1Str, IP2String(buff), err)\n\t\t}\n\n\t\tt.Logf(\"IPSub(%s, %s) = %+v\", ip1Str, IP2String(buff), ipSub)\n\t}\n}\n\nfunc TestIPMiddle(t *testing.T) {\n\tvar sIPStr = \"0.0.0.0\"\n\tsBytes, err := ParseIP(sIPStr)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse ip %s\", sIPStr)\n\t}\n\tvar sInt = int(binary.BigEndian.Uint32(sBytes))\n\tt.Logf(\"start ip: %d -> %s\", sInt, sIPStr)\n\n\tcounter := 0\n\tbuf := make([]byte, 4)\n\tfor i := 0; i < 0x0FFFFFFF; i++ {\n\t\tbinary.BigEndian.PutUint32(buf, uint32(i))\n\t\tmidVal, err := IPMiddle(sBytes, buf)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to IPMiddle(%s,%s): %s\", sIPStr, IP2String(buf), err)\n\t\t}\n\n\t\t// do it as two integers\n\t\tbyteMid := int(binary.BigEndian.Uint32(midVal))\n\t\tintMid := (sInt + i) >> 1\n\t\tif byteMid != intMid {\n\t\t\tt.Fatal(\"byte and int middle value are not the same\")\n\t\t}\n\n\t\tcounter++\n\t}\n\n\tt.Logf(\"test done with %d ips\", counter)\n}\n\nfunc TestSplitSegmentV4(t *testing.T) {\n\t// var str = \"1.1.0.0|1.3.3.24|中国|广东|深圳|电信\"\n\t// var str = \"0.0.0.0|1.255.225.254|0|0|0|内网IP|内网IP\"\n\t// var str = \"29.0.0.0|29.34.191.255|美国|0|0|0|0\"\n\tvar str = \"28.201.224.0|29.34.191.255|美国|0|0|0|0\"\n\tseg, err := SegmentFrom(str)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parser segment '%s': %s\", str, err)\n\t}\n\n\tfmt.Printf(\"idx: src, seg: %s\\n\", seg.String())\n\tvar segList = seg.Split()\n\terr = CheckSegments(segList)\n\tif err != nil {\n\t\tt.Fatalf(\"check segments: %s\", err.Error())\n\t}\n\n\tfor i, s := range segList {\n\t\tfmt.Printf(\"idx: %3d, seg: %s\\n\", i, s.String())\n\t}\n}\n\nfunc TestRegionFiltering(t *testing.T) {\n\tvar line = \"2001:1203:31:8000::|2001:1203:31:bfff:ffff:ffff:ffff:ffff||墨西哥|瓜纳华托州||||专线用户|\"\n\tseg, err := SegmentFrom(line)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse segment '%s': %s\", line, err)\n\t}\n\n\tfReg, err := RegionFiltering(seg.Region, []int{1, 2, 4, 6})\n\tif err != nil {\n\t\tt.Fatalf(\"failed to filter region '%s': %s\", seg.Region, err)\n\t}\n\n\tfmt.Printf(\"region: %s, filtered: %s\\n\", seg.Region, fReg)\n}\n\nfunc TestSplitSegmentV6(t *testing.T) {\n\tvar str = \"fec0::|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff||瑞士|弗里堡州||||专线用户|IANA\"\n\tseg, err := SegmentFrom(str)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parser segment '%s': %s\", str, err)\n\t}\n\n\tfmt.Printf(\"idx: src, seg: %s\\n\", seg.String())\n\tvar segList = seg.Split()\n\terr = CheckSegments(segList)\n\tif err != nil {\n\t\tt.Fatalf(\"check segments: %s\", err.Error())\n\t}\n\n\tfor i, s := range segList {\n\t\tfmt.Printf(\"idx: %3d, seg: %s\\n\", i, s.String())\n\t}\n}\n\nfunc TestIterateSegments(t *testing.T) {\n\thandle, err := os.OpenFile(\"../../../data/sample/segments.tests.mixed\", os.O_RDONLY, 0600)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to open tests file: %s\", err)\n\t}\n\n\t_, _, _ = IterateSegments(handle, true, func(l string) {\n\t\t// fmt.Printf(\"load segment: `%s`\\n\", l)\n\t}, nil, func(seg *Segment) error {\n\t\tfmt.Printf(\"get segment: `%s`\\n\", seg)\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "maker/golang/xdb/version.go",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage xdb\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tIPv4VersionNo = 4\n\tIPv6VersionNo = 6\n)\n\ntype Version struct {\n\tId               int\n\tName             string\n\tBytes            int\n\tSegmentIndexSize int\n\n\t// bytes encode\n\tPutBytes func([]byte, []byte) int\n\n\t// ip compares\n\tIPCompare func([]byte, []byte) int\n\n\tMin []byte\n\tMax []byte\n}\n\nfunc (v *Version) String() string {\n\treturn fmt.Sprintf(\"{Id:%d, Name:%s, Bytes:%d, IndexSize: %d}\", v.Id, v.Name, v.Bytes, v.SegmentIndexSize)\n}\n\nvar (\n\tIPvx = &Version{}\n\tIPv4 = &Version{\n\t\tId:               4,\n\t\tName:             \"IPv4\",\n\t\tBytes:            4,\n\t\tSegmentIndexSize: 14, // 4 + 4 + 2 + 4,\n\t\tPutBytes: func(buff []byte, ip []byte) int {\n\t\t\t// binary.LittleEndian.PutUint32(buff, binary.BigEndian.Uint32(ip))\n\t\t\t// Little Endian byte order for compatible with the old searcher implementation\n\t\t\tbuff[0] = ip[3]\n\t\t\tbuff[1] = ip[2]\n\t\t\tbuff[2] = ip[1]\n\t\t\tbuff[3] = ip[0]\n\t\t\treturn len(ip)\n\t\t},\n\t\tIPCompare: func(ip1 []byte, ip2 []byte) int {\n\t\t\t// ip1 - with Bit endian parsed from an input\n\t\t\t// ip2 - with Little endian read from the xdb index\n\t\t\tip2[0], ip2[3] = ip2[3], ip2[0]\n\t\t\tip2[1], ip2[2] = ip2[2], ip2[1]\n\t\t\treturn bytes.Compare(ip1, ip2)\n\t\t},\n\t\tMin: []byte{0x00, 0x00, 0x00, 0x00},\n\t\tMax: []byte{0xff, 0xff, 0xff, 0xff},\n\t}\n\tIPv6 = &Version{\n\t\tId:               6,\n\t\tName:             \"IPv6\",\n\t\tBytes:            16,\n\t\tSegmentIndexSize: 38, // 16 + 16 + 2 + 4,\n\t\t// Big Endian byte order to follow the network byte order\n\t\tPutBytes: func(buff []byte, ip []byte) int {\n\t\t\treturn copy(buff, ip)\n\t\t},\n\t\tIPCompare: func(ip1, ip2 []byte) int {\n\t\t\treturn bytes.Compare(ip1, ip2)\n\t\t},\n\t\tMin: []byte{\n\t\t\t0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x00, 0x00, 0x00,\n\t\t},\n\t\tMax: []byte{\n\t\t\t0xff, 0xff, 0xff, 0xff,\n\t\t\t0xff, 0xff, 0xff, 0xff,\n\t\t\t0xff, 0xff, 0xff, 0xff,\n\t\t\t0xff, 0xff, 0xff, 0xff,\n\t\t},\n\t}\n)\n\nfunc VersionFromIP(ip string) (*Version, error) {\n\tr, err := ParseIP(ip)\n\tif err != nil {\n\t\treturn IPvx, fmt.Errorf(\"parse ip fail: %w\", err)\n\t}\n\n\tif len(r) == 4 {\n\t\treturn IPv4, nil\n\t}\n\n\treturn IPv6, nil\n}\n\nfunc VersionFromName(name string) (*Version, error) {\n\tswitch strings.ToUpper(name) {\n\tcase \"V4\", \"IPV4\":\n\t\treturn IPv4, nil\n\tcase \"V6\", \"IPV6\":\n\t\treturn IPv6, nil\n\tdefault:\n\t\treturn IPvx, fmt.Errorf(\"invalid version name `%s`\", name)\n\t}\n}\n"
  },
  {
    "path": "maker/java/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb java generation implementation\n\n# Compilation and Installation\n\nCompile the executable jar program via maven:\n\n```bash\n# cd to the maker/java root directory\nmvn clean compile package\n```\n\nThen you will get an ip2region-maker-{version}.jar package file in the target directory of the current directory.\n\n# Data Generation\n\nGenerate the xdb binary file via `java -jar ip2region-maker-{version}.jar`:\n\n```bash\n➜  java git:(master) java -jar target/ip2region-maker-3.0.0.jar \nip2region xdb maker\njava -jar ip2region-maker-{version}.jar [command options]\noptions:\n --src string           source ip text file path\n --dst string           destination binary xdb file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused\n --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\n --log-level string     set the log level, options: debug/info/warn/error\n```\n\nFor example, generate an IPv4 ip2region_v4.xdb binary file in the current directory using the default data/ipv4_source.txt raw data:\n\n```bash\njava -jar target/ip2region-maker-3.0.0.jar --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n...\n2025-09-13 00:33:06 INFO  org.lionsoul.ip2region.xdb.Maker write done, dataBlocks: 13827, indexBlocks: (683843, 720464), indexPtr: (955933, 11042415)\n2025-09-13 00:33:06 INFO  org.lionsoul.ip2region.MakerApp Done, elapsed: 2 s\n```\n\nFor example, generate an IPv6 ip2region_v6.xdb binary file in the current directory using the default data/ipv6_source.txt raw data:\n\n```bash\njava -jar target/ip2region-maker-3.0.0.jar --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n...\n2025-09-13 00:35:34 INFO  org.lionsoul.ip2region.xdb.Maker write done, dataBlocks: 120446, indexBlocks: (16789611, 16855074), indexPtr: (6585371, 647078145)\n2025-09-13 00:35:34 INFO  org.lionsoul.ip2region.MakerApp Done, elapsed: 67 s\n```\n\nFor custom data fields during the generation process, please refer to [xdb-文件生成#自定义数据字段](https://ip2region.net/doc/data/xdb_make#field-list)\n\n# Data Search/bench Test\n\nAll [bindings](../../binding/) come with search and bench test programs as well as usage documentation. You can use the searcher of your familiar language for query testing or bench testing to confirm the correctness and integrity of the data.\n"
  },
  {
    "path": "maker/java/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb java 生成实现\n\n# 编译安装\n通过 maven 来编译可运行 jar 程序：\n```bash\n# cd 到 maker/java 根目录\nmvn clean compile package\n```\n\n然会会在当前目录的 target 目录下得到一个 ip2region-maker-{version}.jar 的打包文件。\n\n# 数据生成\n\n通过 `java -jar ip2region-maker-{version}.jar` 来生成 xdb 二进制文件：\n```bash\n➜  java git:(master) java -jar target/ip2region-maker-3.0.0.jar \nip2region xdb maker\njava -jar ip2region-maker-{version}.jar [command options]\noptions:\n --src string           source ip text file path\n --dst string           destination binary xdb file path\n --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused\n --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\n --log-level string     set the log level, options: debug/info/warn/error\n```\n\n例如，通过默认的 data/ipv4_source.txt 原始数据，在当前目录生成一个 IPv4 的 ip2region_v4.xdb 二进制文件：\n```bash\njava -jar target/ip2region-maker-3.0.0.jar --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n...\n2025-09-13 00:33:06 INFO  org.lionsoul.ip2region.xdb.Maker write done, dataBlocks: 13827, indexBlocks: (683843, 720464), indexPtr: (955933, 11042415)\n2025-09-13 00:33:06 INFO  org.lionsoul.ip2region.MakerApp Done, elapsed: 2 s\n```\n\n例如，通过默认的 data/ipv6_source.txt 有原始据，在当前目录生成一个 IPv6 的 ip2region_v6.xdb 二进制文件：\n```bash\njava -jar target/ip2region-maker-3.0.0.jar --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n...\n2025-09-13 00:35:34 INFO  org.lionsoul.ip2region.xdb.Maker write done, dataBlocks: 120446, indexBlocks: (16789611, 16855074), indexPtr: (6585371, 647078145)\n2025-09-13 00:35:34 INFO  org.lionsoul.ip2region.MakerApp Done, elapsed: 67 s\n```\n\n生成过程中数据字段自定义请参考 [xdb-文件生成#自定义数据字段](https://ip2region.net/doc/data/xdb_make#field-list)\n\n# 数据 查询/bench 测试\n\n已经完成开发的 [binding](../../binding/) 都有查询和 bench 测试程序以及使用文档，你可以使用你熟悉的语言的 searcher 进行查询测试或者bench测试，来确认数据的正确性和完整性。\n"
  },
  {
    "path": "maker/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.lionsoul</groupId>\n    <artifactId>ip2region-maker</artifactId>\n    <version>3.1.0</version>\n    <packaging>jar</packaging>\n\n    <name>ip2region</name>\n    <url>https://github.com/lionsoul2014/ip2region</url>\n    <description>Open source offline internet address db manager framework and locator</description>\n\n    <licenses>\n        <license>\n            <name>The Apache Software License, Version 2.0</name>\n            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <scm>\n        <url>git@github.com:lionsoul2014/ip2region.git</url>\n        <connection>scm:git:git@github.com:lionsoul2014/ip2region.git</connection>\n        <developerConnection>scm:git:git@github.com:lionsoul2014/ip2region.git</developerConnection>\n    </scm>\n\n    <developers>\n        <developer>\n            <id>lionsoul</id>\n            <name>chenxin</name>\n            <email>chenxin619315@gmail.com</email>\n        </developer>\n    </developers>\n\n    <issueManagement>\n        <url>https://github.com/lionsoul2014/ip2region/issues</url>\n        <system>Github issues</system>\n    </issueManagement>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <maven.compiler.source>1.8</maven.compiler.source>\n        <maven.compiler.target>1.8</maven.compiler.target>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.13.2</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>2.1.2</version>\n                <executions>\n                    <execution>\n                        <id>attach-sources</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>2.9</version>\n                <executions>\n                    <execution>\n                        <id>attach-javadocs</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                        <configuration>\n                            <additionalparam>${javadoc.opts}</additionalparam>\n                        </configuration>\n                    </execution>\n                </executions>\n                <configuration>\n                    <failOnError>false</failOnError>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>1.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>org.lionsoul.ip2region.MakerApp</mainClass>\n                                </transformer>\n                            </transformers>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <source>1.8</source>\n                    <target>1.8</target>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>java8-doclint-disabled</id>\n            <activation>\n                <jdk>[1.8,)</jdk>\n            </activation>\n            <properties>\n                <javadoc.opts>-Xdoclint:none</javadoc.opts>\n            </properties>\n        </profile>\n        <profile>\n            <id>release</id>\n            <build>\n                <plugins>\n                    <!-- Source -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-source-plugin</artifactId>\n                        <version>2.2.1</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>jar-no-fork</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <!-- Javadoc -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>2.9.1</version>\n                        <executions>\n                            <execution>\n                                <phase>package</phase>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                                <configuration>\n                                    <additionalparam>${javadoc.opts}</additionalparam>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <!-- GPG -->\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <version>1.5</version>\n                        <executions>\n                            <execution>\n                                <phase>verify</phase>\n                                <goals>\n                                    <goal>sign</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n            <distributionManagement>\n                <snapshotRepository>\n                    <id>oss</id>\n                    <url>https://oss.sonatype.org/content/repositories/snapshots/</url>\n                </snapshotRepository>\n                <repository>\n                    <id>oss</id>\n                    <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n                </repository>\n            </distributionManagement>\n        </profile>\n    </profiles>\n\n</project>\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/MakerApp.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/12\n\npackage org.lionsoul.ip2region;\n\nimport org.lionsoul.ip2region.xdb.IndexPolicy;\nimport org.lionsoul.ip2region.xdb.Log;\nimport org.lionsoul.ip2region.xdb.Maker;\nimport org.lionsoul.ip2region.xdb.Version;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class MakerApp {\n\n    public final static Log log = Log.getLogger(MakerApp.class);\n    public final static Pattern p = Pattern.compile(\"^(\\\\d+(-\\\\d+)?)$\");\n\n    public static void printHelp(String[] args) {\n        System.out.println(\"ip2region xdb maker\");\n        System.out.println(\"java -jar ip2region-maker-{version}.jar [command options]\");\n        System.out.println(\"options:\");\n        System.out.println(\" --src string           source ip text file path\");\n        System.out.println(\" --dst string           destination binary xdb file path\");\n        System.out.println(\" --version string       IP version, options: ipv4/ipv6, specify this flag so you don't get confused\");\n        System.out.println(\" --field-list string    field index list imploded with ',' eg: 0,1,2,3-6,7\");\n        System.out.println(\" --log-level string     set the log level, options: debug/info/warn/error\");\n    }\n\n    private static int[] getFieldList(String fieldList) {\n        final ArrayList<Integer> list = new ArrayList<Integer>();\n        final Map<String, String> map = new HashMap<String, String>();\n        if (!fieldList.isEmpty()) {\n            String[] fList =  fieldList.split(\",\");\n            for (String f : fList) {\n                final String s = f.trim();\n                if (s.isEmpty()) {\n                    log.errorf(\"undefined option `%s`\", f);\n                    return null;\n                }\n\n                final Matcher m = p.matcher(s);\n                if (!m.matches()) {\n                    log.errorf(\"field `%s` is not a number\", f);\n                    return null;\n                }\n\n                final String ms = m.group(1);\n                if (ms.indexOf('-') == -1) {\n                    if (map.containsKey(s)) {\n                        log.errorf(\"duplicate field index `%s`\", s);\n                        return null;\n                    }\n\n                    map.put(s, s);\n                    final int idx = Integer.parseInt(s);\n                    if (idx < 0) {\n                        log.errorf(\"field index `%s` is negative\", s);\n                        return null;\n                    }\n\n                    list.add(idx);\n                    continue;\n                }\n\n                // index range parse\n                final String[] ra = ms.split(\"-\");\n                if (ra.length != 2) {\n                    log.errorf(\"field `%s` is not a valid range\", ms);\n                    return null;\n                }\n\n                final int start = Integer.parseInt(ra[0]);\n                final int end = Integer.parseInt(ra[1]);\n                if (start > end) {\n                    log.errorf(\"index range start(%d) should <= end(%d)\", start, end);\n                    return null;\n                }\n\n                for (int i = start; i <= end; i++) {\n                    final String _s = String.valueOf(i);\n                    if (map.containsKey(_s)) {\n                        log.errorf(\"duplicate field index `%s`\", _s);\n                        return null;\n                    }\n\n                    map.put(_s, _s);\n                    list.add(i);\n                }\n            }\n        }\n\n        // let's keep it a complex way so the old JDK could run these pieces.\n        final int[] fields = new int[list.size()];\n        for (int i = 0; i < list.size(); i++) {\n            fields[i] = list.get(i);\n        }\n\n        // sort the fields to make sure the fields follow the original index order\n        Arrays.sort(fields);\n        return fields;\n    }\n\n    public static void genDb(String[] args) throws Exception {\n        String srcFile = \"\", dstFile = \"\", ipVersion = \"\";\n        String fieldList = \"\", logLevel = \"info\";\n        int indexPolicy = IndexPolicy.Vector;\n        for (final String r : args) {\n            if (r.length() < 5) {\n                continue;\n            }\n\n            if (r.indexOf(\"--\") != 0) {\n                continue;\n            }\n\n            int sIdx = r.indexOf('=');\n            if (sIdx < 0) {\n                System.out.printf(\"missing = for args pair `%s`\\n\", r);\n                return;\n            }\n\n            String key = r.substring(2, sIdx);\n            String val = r.substring(sIdx + 1);\n            // System.out.printf(\"key=%s, val=%s\\n\", key, val);\n            if (\"src\".equals(key)) {\n                srcFile = val;\n            } else if (\"dst\".equals(key)) {\n                dstFile = val;\n            } else if (\"version\".equals(key)) {\n                ipVersion = val;\n            } else if (\"field-list\".equals(key)) {\n                fieldList = val;\n            } else if (\"log-level\".equals(key)) {\n                logLevel = val;\n            } else {\n                System.out.printf(\"undefined option `%s`\\n\", r);\n                return;\n            }\n        }\n\n        if (srcFile.isEmpty() || dstFile.isEmpty()) {\n            printHelp(args);\n            return;\n        }\n\n        // IP version\n        final Version version = Version.fromName(ipVersion);\n\n        final int[] fields = getFieldList(fieldList);\n        if (fields == null) {\n            return;\n        }\n\n        // check and make the field list\n        long tStart = System.currentTimeMillis();\n        final Maker maker = new Maker(version, indexPolicy, srcFile, dstFile, fields);\n        log.infof(\"Generating xdb with src=%s, dst=%s, logLevel=%s\", srcFile, dstFile, logLevel);\n        MakerApp.log.setLevel(logLevel);\n        maker.init();\n        maker.start();\n        maker.end();\n\n        log.infof(\"Done, elapsed: %d s\", (System.currentTimeMillis() - tStart) / 1000);\n    }\n\n    public static void main(String[] args) {\n        if (args.length < 1) {\n            printHelp(args);\n            return;\n        }\n\n        try {\n            genDb(args);\n        } catch (Exception e) {\n            System.out.printf(\"failed running genDb: %s\\n\", e);\n        }\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IPv4 version implementation\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class IPv4 extends Version {\n    public IPv4() {\n        // segmentIndex: 4 + 4 + 2 + 4\n        super(4, \"IPv4\", 4, 14);\n    }\n\n    @Override\n    public int putBytes(byte[] buff, int offset, byte[] ip) {\n        // use the Little endian byte order to compatible with the old searcher implementation\n        buff[offset++] = ip[3];\n        buff[offset++] = ip[2];\n        buff[offset++] = ip[1];\n        buff[offset  ] = ip[0];\n        return ip.length;\n    }\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IPv4 version implementation\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class IPv6 extends Version {\n\n    public IPv6() {\n        // segmentIndex: 16 + 16 + 2 + 4\n        super(6, \"IPv6\", 16, 38);\n    }\n\n    @Override\n    public int putBytes(byte[] buff, int offset, byte[] ip) {\n        System.arraycopy(ip, 0, buff, offset, ip.length);\n        return ip.length;\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/IndexPolicy.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\npublic class IndexPolicy {\n    public static final int Vector = 1;\n    public static final int BTree = 2;\n\n    // parser the index policy from string\n    public static int parse(String policy) throws Exception {\n        String v = policy.toLowerCase();\n        if (\"vector\".equals(v)) {\n            return Vector;\n        } else if (\"btree\".equals(v)) {\n            return BTree;\n        } else {\n            throw new Exception(\"unknown index policy `\"+policy+\"`\");\n        }\n    }\n}"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/InvalidInetAddressException.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\npublic class InvalidInetAddressException extends Exception {\n\n    public InvalidInetAddressException(String str) {\n        super(str);\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// Little Endian basic data type decode and encode.\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic class LittleEndian {\n\n    public final static int[] shiftIndex = {0, 8, 16, 24, 32, 40, 48, 56};\n    \n    // put specified bytes to the buffer started from the offset\n    public static void put(final byte[] buff, int offset, long value, int bytes) {\n        if (bytes > 8) {\n            throw new IndexOutOfBoundsException(\"bytes should be <= 8\");\n        }\n\n        for (int i = 0; i < bytes; i++) {\n            buff[offset++] = (byte)((value >>> shiftIndex[i]) & 0xFF);\n        }\n    }\n\n    // put an uint32 (4 bytes long) to the buffer from the offset\n    public static void putUint32(final byte[] buff, int offset, long value) {\n        buff[offset++] = (byte) (value & 0xFF);\n        buff[offset++] = (byte) ((value >>  8) & 0xFF);\n        buff[offset++] = (byte) ((value >> 16) & 0xFF);\n        buff[offset  ] = (byte) ((value >> 24) & 0xFF);\n    }\n\n    // put a 2-bytes int to the buffer from the specified offset\n    public static void putInt2(final byte[] buff, int offset, int value) {\n        buff[offset++] = (byte) (value & 0xFF);\n        buff[offset  ] = (byte) ((value >>  8) & 0xFF);\n    }\n\n    // get an uint32 from a byte array from the specified offset\n    public static long getUint32(final byte[] buff, int offset) {\n        return (\n            ((buff[offset++] & 0x000000FFL)) |\n            ((buff[offset++] <<  8) & 0x0000FF00L) |\n            ((buff[offset++] << 16) & 0x00FF0000L) |\n            ((buff[offset  ] << 24) & 0xFF000000L)\n        );\n    }\n\n    // get an 2 bytes int from a byte array from the specified offset\n    public static int getInt2(final byte[] buff, int offset) {\n        return (\n            ((buff[offset++]) & 0x000000FF) |\n            ((buff[offset  ] << 8) & 0x0000FF00)\n        );\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\n\n// simple log implementation\npublic class Log {\n\n    /* Log level constants define */\n    public static final int DEBUG = 0;\n    public static final int INFO = 1;\n    public static final int WARN = 2;\n    public static final int ERROR = 3;\n\n    // level name\n    public static final String[] level_string = new String[] {\n        \"DEBUG\",\n        \"INFO\",\n        \"WARN\",\n        \"ERROR\"\n    };\n\n    public final Class<?> baseClass;\n    private int level = INFO;\n\n    public Log(Class<?> baseClass) {\n        this.baseClass = baseClass;\n    }\n\n    public static Log getLogger(Class<?> baseClass) {\n        return new Log(baseClass);\n    }\n\n    public String format(int level, String format, Object... args) {\n        // append the datetime\n        final StringBuilder sb = new StringBuilder();\n        final SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        sb.append(String.format(\"%s %-5s \", sdf.format(new Date()), level_string[level]));\n\n        // append the class name\n        sb.append(baseClass.getName()).append(' ');\n        sb.append(String.format(format, args));\n        return sb.toString();\n    }\n\n    public void printf(int level, String format, Object... args) {\n        if (level < DEBUG || level > ERROR) {\n            throw new IndexOutOfBoundsException(\"invalid level index \" + level);\n        }\n\n        // level filter\n        if (level < this.level) {\n            return;\n        }\n\n        System.out.println(format(level, format, args));\n        System.out.flush();\n    }\n\n    public String getDebugf(String format, Object... args) {\n        return format(DEBUG, format, args);\n    }\n\n    public void debugf(String format, Object... args) {\n        printf(DEBUG, format, args);\n    }\n\n    public String getInfof(String format, Object... args) {\n        return format(INFO, format, args);\n    }\n\n    public void infof(String format, Object... args) {\n        printf(INFO, format, args);\n    }\n\n    public String getWarnf(String format, Object... args) {\n        return format(WARN, format, args);\n    }\n\n    public void warnf(String format, Object... args) {\n        printf(WARN, format, args);\n    }\n\n    public String getErrorf(String format, Object... args) {\n        return format(ERROR, format, args);\n    }\n\n    public void errorf(String format, Object... args) {\n        printf(ERROR, format, args);\n    }\n\n    public Log setLevel(int level) {\n        this.level = level;\n        return this;\n    }\n\n    public Log setLevel(String level) {\n        String v = level.toLowerCase();\n        if (\"debug\".equals(v)) {\n            this.level = DEBUG;\n        } else if (\"info\".equals(v)) {\n            this.level = INFO;\n        } else if (\"warn\".equals(v)) {\n            this.level = WARN;\n        } else if (\"error\".equals(v)) {\n            this.level = ERROR;\n        }\n\n        return this;\n    }\n\n}"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/Maker.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/12\n\n// --- Ip2Region v2.0 data structure\n//\n// +----------------+--------------------------+---------------+--------------+\n// | header space   | vector speed up index    |  data payload | block index  |\n// +----------------+--------------------------+---------------+--------------+\n// | 256 bytes      | 512 KiB (fixed)          | dynamic size  | dynamic size |\n// +----------------+--------------------------+---------------+--------------+\n//\n// 1. padding space : for header info like block index ptr, version, release date eg ... or any other temporary needs.\n// -- 2bytes: version number, different version means structure update, it fixed to 2 for now\n// -- 2bytes: index algorithm code.\n// -- 4bytes: generate unix timestamp (version)\n// -- 4bytes: index block start ptr\n// -- 4bytes: index block end ptr\n// -- 2bytes: ip version number (4/6 since IPv6 supporting)\n// -- 2bytes: runtime ptr bytes\n//\n//\n// 2. data block : region or whatever data info.\n// 3. segment index block : binary index block.\n// 4. vector index block  : fixed index info for block index search speed up.\n// space structure table:\n// -- 0   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 1   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- 2   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n// -- ...\n// -- 255 -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n//\n//\n// super block structure:\n// +-----------------------+----------------------+\n// | first index block ptr | last index block ptr |\n// +-----------------------+----------------------+\n//\n// data entry structure:\n// +--------------------+-----------------------+\n// | 2bytes (for desc)\t| dynamic length        |\n// +--------------------+-----------------------+\n//  data length   whatever in bytes\n//\n// index entry structure\n// +------------+-----------+---------------+------------+\n// | 4bytes     | 4bytes    | 2bytes        | 4 bytes    |\n// +------------+-----------+---------------+------------+\n//  start ip \t  end ip\t  data length     data ptr\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\nimport java.util.*;\n\nimport org.lionsoul.ip2region.xdb.Segment.IterateAction;\n\npublic class Maker {\n    // constants define\n    public static final int VersionNo         = 3;  // 2 for XDB 2.0, 3 for XDB 3.0\n    public static final int HeaderInfoLength  = 256;\n    public static final int VectorIndexRows   = 256;\n    public static final int VectorIndexCols   = 256;\n    public static final int VectorIndexSize   = 8;  // in bytes\n    public static final int RuntimePtrSize    = 4;  // in bytes\n    public static final long MaxFilePointer   = (1L << (RuntimePtrSize * 8)) - 1;\n    public static final int VectorIndexLength = VectorIndexRows * VectorIndexCols * VectorIndexSize;\n\n    public static final Log log = Log.getLogger(Maker.class);\n\n    // IP version since xdb 3.0\n    private final Version version;\n\n    // source text file handle\n    private final File srcFile;\n    private final int[] fields;\n\n    private final List<Segment> segments;\n    private final Charset bytesCharset;\n\n    // destination binary file handle\n    private final RandomAccessFile dstHandle;\n\n    // index policy\n    private final int indexPolicy;\n\n    // region pool\n    private final Map<String, DataEntry> regionPool;\n\n    // vector index raw bytes\n    private final byte[] vectorIndex;\n\n    public Maker(Version version, int policy, String srcPath, String dstPath, int[] fields) throws IOException {\n        this.srcFile = new File(srcPath);\n        if (!this.srcFile.exists()) {\n            throw new FileNotFoundException(\"source text file `\" + srcPath + \"` not found\");\n        }\n\n        this.version = version;\n        this.fields  = fields;\n\n        /// check and delete the target xdb file if it exists\n        /// final File dstFile = new File(dstPath);\n        /// if (dstFile.exists() && !dstFile.delete()) {\n        ///     log.warnf(\"failed to delete the dest xdb file `%s`\", dstPath);\n        /// }\n\n        this.bytesCharset = Charset.forName(\"utf-8\");\n        this.segments = new ArrayList<Segment>();\n        this.dstHandle = new RandomAccessFile(dstPath, \"rw\");\n        this.indexPolicy = policy;\n        this.regionPool = new HashMap<String, DataEntry>();\n        this.vectorIndex = new byte[VectorIndexLength]; // all filled with 0\n\n        // truncate the original xdb file\n        this.dstHandle.setLength(0);\n    }\n\n    // init the header of the target xdb binary file\n    private void initHeader() throws IOException {\n        log.infof(\"try to init the db header ... \");\n        dstHandle.seek(0);\n\n        // make and write the header space\n        final byte[] header = new byte[HeaderInfoLength];\n\n        // encode the data\n        // 1, data version number\n        LittleEndian.put(header, 0, VersionNo, 2);\n\n        // 2, index policy code\n        LittleEndian.put(header, 2, indexPolicy, 2);\n\n        // 3, generate unix timestamp\n        LittleEndian.put(header, 4, System.currentTimeMillis() / 1000, 4);\n\n        // 4, index block start ptr\n        LittleEndian.put(header, 8, 0, 4);    // start index ptr\n\n        // 5, index block end ptr\n        LittleEndian.put(header, 12, 0, 4);   // end index ptr\n\n        // since xdb 3.0\n        // 6, IP version\n        LittleEndian.put(header, 16, version.id, 2);        // IP version\n\n        // 7, runtime ptr bytes\n        LittleEndian.put(header, 18, RuntimePtrSize, 2);    // runtime ptr bytes\n\n        dstHandle.write(header);\n    }\n\n    // load all the segments.\n    private void loadSegments() throws Exception {\n        log.infof(\"try to load the segments ... \");\n        final long tStart = System.currentTimeMillis();\n        final IterateAction itAct = new Segment.IterateAction() {\n            private Segment last = null;\n            private boolean sorting = false;\n\n            @Override\n            public void before(String line) {\n                log.debugf(\"load segment: `%s`\", line);\n            }\n\n            @Override\n            public String filter(String region) {\n                return Util.regionFiltering(region, fields);\n            }\n\n            @Override\n            public void handle(Segment seg) throws Exception {\n                // ip version check\n                if (seg.startIP.length != version.bytes) {\n                    throw new Exception(\"invalid ip segment(\"+version.name+\" expected)\");\n                }\n\n                if (sorting) {\n                    // just keep going\n                } else if (last != null && !seg.after(last)) {\n                    // throw new Exception(\"discontinuous data segment: last.eip(\"\n                    //    + Util.ipToString(last.endIP)+\")+1 != seg.sip(\"+ Util.ipToString(seg.startIP) + \", \"+ seg.region +\")\");\n\n                    // @Note: If the continuity is disrupted,\n                    // we will sort all these segments later.\n                    sorting = true;\n                }\n\n                segments.add(seg);\n                last = seg;\n            }\n\n            public boolean sorting() {\n                return sorting;\n            }\n        };\n\n        // load iterate all the segments\n        Segment.iterate(srcFile, itAct);\n        final boolean sorting = itAct.sorting();\n\n        // check and sort all the segments\n        if (sorting) {\n            log.infof(\"try to sort all the segments based on its start ip ...\");\n            segments.sort((o1, o2) -> {return Util.ipCompare(o1.startIP, o2.startIP);});\n\n            log.infof(\"try to check if there is overlap in the segments  ...\");\n            Segment last = null;\n            for (final Segment seg : segments) {\n                // check the order of the data segment\n                if (last != null && !seg.after(last)) {\n                    throw new Exception(\"overlap checking: last.eip(\"\n                       + Util.ipToString(last.endIP)+\") >= seg.sip(\"+ Util.ipToString(seg.startIP) + \", \"+ seg.region +\")\");\n                }\n\n                // reset the last\n                last = seg;\n            }\n        }\n\n        log.infof(\n            \"all segments loaded, length: %d, sorting: %s, elapsed: %d ms\", \n            segments.size(), sorting ? \"true\" : \"false\", System.currentTimeMillis() - tStart\n        );\n    }\n\n    // init the maker\n    public void init() throws Exception {\n        // init the db header\n        initHeader();\n\n        // load all the segments\n        loadSegments();\n    }\n\n    // set the vector index info of the specified ip\n    private void setVectorIndex(final byte[] ip, long ptr) {\n        final int il0 = (int) (ip[0] & 0xFF);\n        final int il1 = (int) (ip[1] & 0xFF);\n        final int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;\n        final long sPtr = LittleEndian.getUint32(vectorIndex, idx);\n        if (sPtr == 0) {\n            LittleEndian.put(vectorIndex, idx, ptr, 4);\n            LittleEndian.put(vectorIndex, idx + 4, ptr + version.segmentIndexSize, 4);\n        } else {\n            LittleEndian.put(vectorIndex, idx + 4, ptr + version.segmentIndexSize, 4);\n        }\n    }\n\n    // start to make the binary file\n    public void start() throws Exception {\n        if (segments.isEmpty()) {\n            throw new Exception(\"empty segment list\");\n        }\n\n        // 1, write all the region/data to the binary file\n        dstHandle.seek(HeaderInfoLength + VectorIndexLength);\n\n        log.infof(\"try to write the data block ... \");\n        for (final Segment seg : segments) {\n            log.debugf(\"try to write region `%s` ... \", seg.region);\n            final DataEntry e = regionPool.get(seg.region);\n            if (e != null) {\n                log.debugf(\" --[Cached] with ptr=%d\", e.ptr);\n                continue;\n            }\n\n            // get the utf-8 bytes of the region info\n            final byte[] regionBuff = seg.region.getBytes(bytesCharset);\n            if (regionBuff.length < 1) {\n                // allow empty region info\n                // throw new Exception(\"empty region info for segment `\"+seg+\"`\");\n            } else if (regionBuff.length > 0xFFFF) {\n                throw new Exception(\"too long region info `\"+seg.region+\"`: should be less than 65535 bytes\");\n            }\n\n            // record the current ptr\n            final long pos = dstHandle.getFilePointer();\n            dstHandle.write(regionBuff);\n\n            // @TODO: remove this if the long ptr operation were supported\n            if (pos >= MaxFilePointer) {\n                throw new IOException(\"region ptr exceed the max length of '\" + MaxFilePointer + \"'\");\n            }\n\n            // record the mapping\n            regionPool.put(seg.region, new DataEntry(regionBuff.length, pos));\n            log.debugf(\" --[Added] with ptr=%d\", pos);\n        }\n\n        // 2, write the index block cache the super index block\n        log.infof(\"try to write the segment index block ... \");\n        int counter = 0;\n        long startIndexPtr = -1, endIndexPtr = -1;\n        final byte[] indexBuff = new byte[version.segmentIndexSize];\n        for (Segment seg : segments) {\n            // we need the region ptr\n            final DataEntry e = regionPool.get(seg.region);\n            if (e == null) {\n                throw new Exception(\"missing ptr cache for region `\"+seg.region+\"`\");\n            }\n\n            int _offset = 0;\n            List<Segment> segList = seg.split();\n            log.debugf(\"try to index segment(%d splits) %s ... \", segList.size(), seg);\n            for (final Segment s : segList) {\n                long pos = dstHandle.getFilePointer();\n\n                // @TODO: remove this if the long ptr operation were supported\n                if (pos >= MaxFilePointer) {\n                    throw new IOException(\"region ptr exceed the max length of '\" + MaxFilePointer + \"'\");\n                }\n\n                // encode the segment index info.\n                // @Note: in order to compatible with the old searcher implementation we choose to keep\n                // encode the IPv4 bytes with little endian.\n                // for IPv6 we choose the Network byte order (Big endian).\n                version.putBytes(indexBuff, 0, s.startIP);\n                version.putBytes(indexBuff, s.startIP.length, s.endIP);\n                _offset = s.startIP.length + s.endIP.length;\n                LittleEndian.put(indexBuff, _offset, e.length, 2);\n                LittleEndian.put(indexBuff, _offset + 2, e.ptr, 4);\n                dstHandle.write(indexBuff);\n\n                log.debugf(\"|-segment index: %d, ptr: %d, segment: %s\", counter, pos, s);\n                setVectorIndex(s.startIP, pos);\n                counter++;\n\n                // check and record the start index ptr\n                if (startIndexPtr == -1) {\n                    startIndexPtr = pos;\n                }\n\n                endIndexPtr = pos;\n            }\n        }\n\n        // 3, synchronize the vector index block\n        log.infof(\"try to write the vector index block ... \");\n        dstHandle.seek(HeaderInfoLength);\n        dstHandle.write(vectorIndex);\n\n        // 4, synchronize the segment index info\n        log.infof(\"try to write the segment index ptr ... \");\n        LittleEndian.put(indexBuff, 0, startIndexPtr, 4);\n        LittleEndian.put(indexBuff, 4, endIndexPtr, 4);\n        dstHandle.seek(8);\n        dstHandle.write(indexBuff, 0, 8);\n\n        log.infof(\"write done, dataBlocks: %d, indexBlocks: (%d, %d), indexPtr: (%d, %d)\",\n            regionPool.size(), segments.size(), counter, startIndexPtr, endIndexPtr);\n    }\n\n    // end the make, do the resource clean up\n    public void end() throws IOException {\n        this.dstHandle.close();\n    }\n\n    private static class DataEntry {\n        final long ptr;\n        final int length; // in bytes\n\n        DataEntry(int length, long ptr) {\n            this.length = length;\n            this.ptr = ptr;\n        }\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/Segment.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class Segment {\n    public final byte[] startIP;\n    public final byte[] endIP;\n    public final String region;\n\n    public Segment(final byte[] startIP, final byte[] endIP, String region) {\n        this.startIP = startIP;\n        this.endIP = endIP;\n        this.region = region;\n    }\n\n    // split the current segment for vector index\n    public List<Segment> split() {\n        final int sByte1 = (int) (startIP[0] & 0xFF);\n        final int eByte1 = (int) (endIP[0] & 0xFF);\n        final List<Segment> tList =  new ArrayList<Segment>();\n        for (int i = sByte1; i <= eByte1; i++) {\n            final byte[] sip = new byte[startIP.length];\n            final byte[] eip = new byte[startIP.length];\n\n            if (i == sByte1) {\n                System.arraycopy(startIP, 0, sip, 0, sip.length);\n            } else {\n                sip[0] = (byte) (i & 0xFF);\n            }\n\n            if (i == eByte1) {\n                System.arraycopy(endIP, 0, eip, 0, eip.length);\n            } else {\n                eip[0] = (byte)(i & 0xFF);\n                // fill the rest buffer with 0xFF\n                for (int j = 1; j < eip.length; j++) {\n                    eip[j] = (byte) (0xFF);\n                }\n            }\n\n            // append the new segment:\n            // @Note: Don't bother to copy the region.\n            tList.add(new Segment(sip, eip, null));\n        }\n\n        // 2, split the segments with the second byte\n        final List<Segment> segList = new ArrayList<Segment>();\n        for (Segment seg : tList) {\n            final int sByte2 = (int) (seg.startIP[1] & 0xFF);\n            final int eByte2 = (int) (seg.endIP[1] & 0xFF);\n            for (int i = sByte2; i <= eByte2; i++) {\n                final byte[] sip = new byte[seg.startIP.length];\n                final byte[] eip = new byte[seg.startIP.length];\n                sip[0] = seg.startIP[0];\n                eip[0] = seg.startIP[0];\n\n                if (i == sByte2) {\n                    System.arraycopy(seg.startIP, 0, sip, 0, seg.startIP.length);\n                } else {\n                    sip[1] = (byte) (i & 0xFF);\n                }\n\n                if (i == eByte2) {\n                    System.arraycopy(seg.endIP, 0, eip, 0, seg.endIP.length);\n                } else {\n                    eip[1] = (byte) (i & 0xFF);\n                    for (int j = 2; j < seg.endIP.length; j++) {\n                        eip[j] = (byte) 0xFF;\n                    }\n                }\n\n                // append the new segment\n                segList.add(new Segment(sip, eip, region));\n            }\n        }\n\n        return segList;\n    }\n\n    @Override public String toString() {\n        return Util.ipToString(startIP) + \"|\" + Util.ipToString(endIP) + \"|\" + region;\n    }\n\n    // check if the an IP address in within this segment\n    public boolean contains(final byte[] ip) {\n        return Util.ipCompare(ip, startIP) >= 0 && Util.ipCompare(ip, endIP) <= 0;\n    }\n\n    // check if the current segment just right behind the specified one.\n    // which mean last.endIP + 1 = this.startIP\n    public boolean rightBehind(final Segment last) {\n        return Util.ipCompare(Util.ipAddOne(last.endIP), startIP) == 0;\n    }\n\n    // check if the current segment is after the specified one.\n    // which means last.endIP < this.startIP\n    public boolean after(final Segment last) {\n        return Util.ipCompare(last.endIP, startIP) < 0;\n    }\n\n    // parser the Segment from an input string\n    public static Segment parse(String input) throws Exception {\n        final String[] ps = input.trim().split(\"\\\\|\", 3);\n        if (ps.length != 3) {\n            throw new Exception(\"invalid ip segment `\"+input+\"`\");\n        }\n\n        final byte[] sip = Util.parseIP(ps[0]);\n        final byte[] eip = Util.parseIP(ps[1]);\n        if (Util.ipCompare(sip, eip) > 0) {\n            throw new Exception(\"start ip `\"+ps[0]+\"` should not be greater than end ip `\"+ps[1]+\"`\");\n        }\n\n        return new Segment(sip, eip, ps[2]);\n    }\n\n    // static class to handler the iterate callback\n    public static interface IterateAction {\n        // need sort all the iterated segments ?\n        default boolean sorting() {\n            return false;\n        }\n\n        public void before(final String line);\n        public String filter(final String region);\n        public void handle(final Segment seg) throws Exception;\n    }\n\n    // iterate the segments from the specified ip source file and call the handler\n    public static void iterate(final String srcFile, IterateAction action) throws Exception {\n        iterate(new File(srcFile), action);\n    }\n\n    public static void iterate(final File srcFile, IterateAction action) throws Exception {\n        Segment last = null;\n        String line = null;\n        final FileInputStream fis = new FileInputStream(srcFile);\n        final BufferedReader br = new BufferedReader(new InputStreamReader(fis, \"utf-8\"));\n        while ((line = br.readLine()) != null) {\n            final String l = line.trim();\n            // ignore empty line\n            if (l.length() < 1) {\n                continue;   \n            }\n\n            // ignore comment line\n            if (l.charAt(0) == '#') {\n                continue;\n            }\n\n            // call the action.before\n            action.before(l);\n\n            // split the line to create the segment\n            final String[] ps = line.split(\"\\\\|\", 3);\n            if (ps.length != 3) {\n                br.close();\n                throw new Exception(\"invalid ip segment line `\"+ps[0]+\"`\");\n            }\n\n            final byte[] sip = Util.parseIP(ps[0]);\n            final byte[] eip = Util.parseIP(ps[1]);\n            if (sip.length != eip.length) {\n                br.close();\n                throw new Exception(\"invalid ip segment line `\" + line + \"`: sip/eip version not match\");\n            }\n\n            if (Util.ipCompare(sip, eip) > 0) {\n                br.close();\n                throw new Exception(\"start ip(\"+ps[0]+\") should not be greater than end ip(\"+ps[1]+\")\");\n            }\n\n            // allow empty region info\n            // if (ps[2].isEmpty()) {\n            //     br.close();\n            //     throw new Exception(\"empty region info in segment line `\"+ps[2]+\"`\");\n            // }\n\n            final Segment seg = new Segment(sip, eip, action.filter(ps[2]));\n            // check and set the last segment\n            if (last == null) {\n                last = seg;\n                continue;\n            }\n            \n            // check and automatic merging the Consecutive Segments, which means:\n            // 1, region info is the same\n            // 2, last.eip+1 = cur.sip\n            if (last.region.equals(seg.region) && seg.rightBehind(last)) {\n                // last.endIP = seg.endIP;\n                System.arraycopy(seg.endIP, 0, last.endIP, 0, seg.endIP.length);\n                continue;\n            }\n\n            // pass the segment to the aciton.handle\n            try {\n                action.handle(last);\n            } catch (Exception e) {\n                // break the loop if the handle return false\n                br.close();\n                throw new Exception(e.getMessage());\n            }\n\n            // reset the last\n            last = seg;\n        }\n\n        // process the last segment\n        if (last != null) {\n            action.handle(last);\n        }\n\n        br.close();\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java",
    "content": "// Copyright 2022 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n//\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2022/07/14\n\npackage org.lionsoul.ip2region.xdb;\n\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\n\npublic class Util\n{\n\n    // parse the specified IP address and return its bytes.\n    // returns: byte[4] for IPv4 and byte[16] for IPv6 and the bytes should be in Big endian order.\n    public static byte[] parseIP(String ip) throws InvalidInetAddressException {\n        try {\n            return InetAddress.getByName(ip).getAddress();\n        } catch (UnknownHostException e) {\n            throw new InvalidInetAddressException(\"invalid ip address `\"+ip+\"`\");\n        }\n    }\n\n    // convert the byte[] ip to string ip address\n    public static String ipToString(final byte[] ip) {\n        if (ip.length != 4 && ip.length != 16) {\n            return String.format(\"invalid-ip-address-length: %d\", ip.length);\n        }\n\n        try {\n            return InetAddress.getByAddress(ip).getHostAddress();\n        } catch (UnknownHostException e) {\n            return String.format(\"invalid-ip-address `%s`\", ipArrayString(ip));\n        }\n    }\n\n    // implode the byte[] ip with its byte value.\n    public static String ipArrayString(byte[] ip) {\n        final StringBuffer sb = new StringBuffer();\n        sb.append(\"[\");\n        for (int i = 0; i < ip.length; i++) {\n            if (i > 0) {\n                sb.append(',');\n            }\n            sb.append((ip[i] & 0xFF));\n        }\n        sb.append(\"]\");\n        return sb.toString();\n    }\n\n    // compare two byte ip\n    // Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\n    public static int ipCompare(byte[] ip1, byte[] ip2) {\n        for (int i = 0; i < ip1.length; i++) {\n            // covert the byte to int to sure the uint8 attribute\n            final int i1 = (int)(ip1[i] & 0xFF);\n            final int i2 = (int)(ip2[i] & 0xFF);\n            if (i1 < i2) {\n                return -1;\n            }\n\n            if (i1 > i2) {\n                return 1;\n            }\n        }\n\n        return 0;\n    }\n\n    public static byte[] ipAddOne(byte[] ip) {\n        final byte[] r = new byte[ip.length];\n        System.arraycopy(ip, 0, r, 0, ip.length);\n        for (int i = ip.length - 1; i >= 0; i--) {\n            final int v = (int)(r[i] & 0xFF);\n            if (v < 255) {    // No overflow\n                r[i]++;\n                break;\n            }\n\n            r[i] = 0;\n        }\n\n        return r;\n    }\n\n    public static byte[] ipSubOne(byte[] ip) {\n        final byte[] r = new byte[ip.length];\n        System.arraycopy(ip, 0, r, 0, ip.length);\n        for (int i = ip.length - 1; i >= 0; i--) {\n            final int v = (int)(r[i] & 0xFF);\n            if (v > 0) {    // No borrow needed\n                r[i]--;\n                break;\n            }\n\n            r[i] = (byte) 0xFF; // borrow from the next byte\n        }\n\n        return r;\n    }\n\n    // region filtering\n    public static String regionFiltering(String region, int[] fields) {\n        if (fields.length == 0) {\n            return region;\n        }\n\n        final String[] fs = region.split(\"\\\\|\", -1);\n        final StringBuilder sb = new StringBuilder();\n        final int tailing = fields.length - 1;\n        for (int i = 0; i < fields.length; i++) {\n            final int idx = fields[i];\n            if (idx >= fs.length) {\n                throw new IllegalArgumentException(\"field index `\"\n                        + idx + \"` exceeded the max length `\" + fs.length + \"`\");\n            }\n\n            sb.append(fs[idx]);\n            if (i < tailing) {\n                sb.append(\"|\");\n            }\n        }\n\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java",
    "content": "// Copyright 2025 The Ip2Region Authors. All rights reserved.\n// Use of this source code is governed by a Apache2.0-style\n// license that can be found in the LICENSE file.\n\npackage org.lionsoul.ip2region.xdb;\n\n// IP version abstract manager (IPv4 & IPv6)\n// @Author Lion <chenxin619315@gmail.com>\n// @Date   2025/09/10\n\npublic abstract class Version {\n    public static final int IPv4VersionNo = 4;\n    public static final int IPv6VersionNo = 6;\n\n    public static final IPv4 IPv4 = new IPv4();\n    public static final IPv6 IPv6 = new IPv6();\n\n    // version id and name\n    public final int id;\n    public final String name;\n    \n    // the numbers of bytes for one IP\n    public final int bytes;\n\n    // segment index size (bytes)\n    public final int segmentIndexSize;\n\n    public Version(int id, String name, int bytes, int segmentIndexSize) {\n        this.id = id;\n        this.name = name;\n        this.bytes = bytes;\n        this.segmentIndexSize = segmentIndexSize;\n    }\n\n    // encode the specified IP bytes to the specified buffer\n    public abstract int putBytes(byte[] buff, int offset, byte[] ip);\n\n    // compare the two IPs with the current version.\n    // Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2\n    public int ipCompare(byte[] ip1, byte[] ip2) {\n        return Util.ipCompare(ip1, ip2);\n    }\n\n    // parse the version from an name\n    public static final Version fromName(String name) throws Exception {\n        final String n = name.toUpperCase();\n        if (n.equals(\"V4\") || n.equals(\"IPV4\")) {\n            return IPv4;\n        } else if (n.equals(\"V6\") || n.equals(\"IPV6\")) {\n            return IPv6;\n        } else {\n            throw new Exception(\"invalid version name `\"+name+\"`\");\n        }\n    }\n\n    @Override public String toString() {\n        return String.format(\"{Id:%d, Name:%s, Bytes:%d, IndexSize: %d}\", id, name, bytes, segmentIndexSize);\n    }\n}"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/IndexPolicyTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport org.junit.Test;\n\npublic class IndexPolicyTest {\n\n    private static final Log log = Log.getLogger(IndexPolicyTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testParse() {\n        final String[] inputs = {\"vector\", \"btree\", \"VecTor\", \"BTree\", \"abc\"};\n        for (String str : inputs) {\n            try {\n                int policy = IndexPolicy.parse(str);\n                log.infof(\"parse(%s)=%d\", str, policy);\n            } catch (Exception e) {\n                log.errorf(\"parse index policy `%s`: %s\", str, e.getMessage());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\npublic class LittleEndianTest {\n\n    private static final Log log = Log.getLogger(LittleEndianTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testAll() {\n        final byte[] buff = new byte[14];\n\n        // encode\n        // do the put\n        LittleEndian.put(buff, 0, 1L, 4);\n        LittleEndian.put(buff, 4, 2L, 4);\n\n        // putUint32\n        LittleEndian.putInt2(buff, 8, 24);\n        LittleEndian.putUint32(buff, 10, 1024L);\n\n        // decode\n        assertEquals(LittleEndian.getUint32(buff, 0), 1);\n        assertEquals(LittleEndian.getUint32(buff, 4), 2);\n        assertEquals(LittleEndian.getInt2(buff, 8), 24);\n        assertEquals(LittleEndian.getUint32(buff, 10), 1024);\n\n        log.debugf(\"uint32(buff, 0): %d\", LittleEndian.getUint32(buff, 0));\n        log.debugf(\"uint32(buff, 4): %d\", LittleEndian.getUint32(buff, 4));\n        log.debugf(\"int2(buff, 8): %d\", LittleEndian.getInt2(buff, 8));\n        log.debugf(\"uint32(buff, 10): %d\", LittleEndian.getUint32(buff, 10));\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/MakerTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport org.junit.Test;\n\npublic class MakerTest {\n\n    private static final Log log = Log.getLogger(MakerTest.class);\n\n    @Test\n    public void testInit() {\n        log.infof(\"MaxFilePointer(%d bytes): %d\", Maker.RuntimePtrSize, Maker.MaxFilePointer);\n        log.infof(\"MaxFilePoiner(5 bytes): %d\", ((1L << (5 * 8)) - 1));\n        log.infof(\"MaxFilePoiner(6 bytes): %d\", ((1L << (6 * 8)) - 1));\n    }\n}\n"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/SegmentTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport java.io.File;\nimport java.net.URL;\n\nimport org.junit.Test;\n\npublic class SegmentTest {\n\n    private final static Log log = Log.getLogger(SegmentTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testParse() throws Exception {\n        final String[] strs = {\n            \"1.1.0.0|1.3.3.24|中国|广东|深圳|电信\",\n            \"28.201.224.0|29.34.191.255|美国|0|0|0|0\",\n            \"2001:4:112::|2001:4:112:ffff:ffff:ffff:ffff:ffff|德国|黑森|美因河畔法兰克福|专线用户\"\n        };\n\n        for (final String str : strs) {\n            final Segment seg = Segment.parse(str);\n            log.debugf(\"seg: %s\", seg.toString());\n        }\n    }\n\n    @Test\n    public void testSplit() throws Exception {\n        final String[] t_segs = {\n            \"1.1.0.0|1.3.3.24|中国|广东|深圳|电信\",\n            \"28.201.224.0|29.34.191.255|美国|0|0|0|0\",\n            \"fec0::|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff|瑞士|弗里堡州||专线用户|IANA\"\n        };\n\n        for (String str : t_segs) {\n            final Segment seg = Segment.parse(str);\n            log.infof(\"segment(%s)->split: \", seg.toString());\n            for (final Segment s : seg.split()) {\n                log.debugf(s.toString());\n            }\n        }\n    }\n\n    @Test\n    public void testIterate() throws Exception {\n        final URL res = getClass().getClassLoader().getResource(\"\");\n        if (res == null) {\n            throw new Exception(\"unable to get the resource path\");\n        }\n\n        final String base = new File(res.getPath()).getParentFile().getParentFile().getParentFile().getParent();\n        Segment.iterate(base+\"/data/sample/segments.tests.mixed\", new Segment.IterateAction() {\n            @Override\n            public void before(String line) {\n                // log.debugf(\"load segment: `%s`\", line);\n            }\n\n            @Override\n            public String filter(String region) {\n                return region;\n            }\n\n            @Override\n            public void handle(Segment seg) throws Exception {\n                log.infof(\"handle segment: `%s`\", seg.toString());\n            }\n        });\n    }\n}"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport org.junit.Test;\n\npublic class UtilTest {\n\n    private static final Log log = Log.getLogger(UtilTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testCheckIP() throws InvalidInetAddressException {\n        final String[] ips = new String[]{\n            \"192.168.1.102\",\n            \"219.133.111.87\",\n            \"::\",\n            \"3000::\",\n            \"::1001:ffff\",\n            \"2001:2:0:ffff:ffff:ffff:ffff:ffff\",\n            \"::ffff:114.114.114.114\"\n        };\n\n        for (String ip : ips) {\n            final byte[] ipBytes = Util.parseIP(ip);\n            log.debugf(\"%s(v=%s) => %s\", ip, Util.ipArrayString(ipBytes), Util.ipToString(ipBytes));\n        }\n    }\n\n    @Test\n    public void testIpCompare() throws InvalidInetAddressException {\n        final String[][] ipPairs = new String[][]{\n            {\"1.0.0.0\", \"1.0.0.1\"},\n            {\"192.168.1.101\", \"192.168.1.90\"},\n            {\"219.133.111.87\", \"114.114.114.114\"},\n            {\"2000::\", \"2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff\"},\n            {\"2001:4:112::\", \"2001:4:112:ffff:ffff:ffff:ffff:ffff\"},\n            {\"ffff::\", \"2001:4:ffff:ffff:ffff:ffff:ffff:ffff\"}\n        };\n\n        for (String[] ips : ipPairs) {\n            final byte[] ip1 = Util.parseIP(ips[0]);\n            final byte[] ip2 = Util.parseIP(ips[1]);\n            log.debugf(\"compare(%s, %s): %d\", ips[0], ips[1], Util.ipCompare(ip1, ip2));\n        }\n    }\n\n    @Test\n    public void testIpAddOne() throws InvalidInetAddressException {\n        final String[] ips = new String[] {\n            \"1.0.0.0\",\n            \"192.168.1.255\",\n            \"2000::\",\n            \"255.255.255.254\",\n            \"0.0.0.255\",\n            \"0.255.255.255\",\n            \"1.1.255.255\"\n        };\n        for (String ip : ips) {\n            final byte[] ipBytes = Util.parseIP(ip);\n            log.debugf(\"ipAddOne(%s): %s\", ip, Util.ipToString(Util.ipAddOne(ipBytes)));\n        }\n    }\n\n    @Test\n    public void testIpSubOne() throws InvalidInetAddressException {\n        final String[] ips = new String[] {\n            \"192.168.1.255\",\n            \"1.0.0.1\",\n            \"1.0.0.0\",\n            \"2.0.0.0\",\n            \"2000::\",\n            \"ffff::\",\n            \"1::1\",\n        };\n        for (String ip : ips) {\n            final byte[] ipBytes = Util.parseIP(ip);\n            log.debugf(\"ipSubOne(%s): %s\", ip, Util.ipToString(Util.ipSubOne(ipBytes)));\n        }\n    }\n\n    @Test\n    public void testRegionFiltering() {\n        final String[] regions = new String[]{\n            \"亚洲|中国|广东|深圳|宝安|电信|113.88311|22.55371|440306|0755|518100|Asia/Shanghai|CNY|11|CHXX0120\",\n            \"大洲|国家|省份|城市|区县|ISP|经度|纬度|0|0|0|0|0\",\n            \"||||||||||||\",\n            \"亚洲|中国|||||||||||\",\n            \"美国|加利福尼亚州|洛杉矶|专线用户|||||||||\"\n        };\n\n        final int[] fields = new int[] {1,2,3,4,6,7};\n        for (String region : regions) {\n            log.infof(\"filtering: %s\", Util.regionFiltering(region, fields));\n        }\n    }\n\n}\n"
  },
  {
    "path": "maker/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java",
    "content": "package org.lionsoul.ip2region.xdb;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\npublic class VersionTest {\n\n    private static final Log log = Log.getLogger(VersionTest.class).setLevel(Log.DEBUG);\n\n    @Test\n    public void testFromName() throws Exception {\n        final String[] vers = new String[]{\"IPv4\", \"IPv6\"};\n        final Version v4 = Version.fromName(vers[0]);\n        assertEquals(v4.name, vers[0]);\n        final Version v6 = Version.fromName(vers[1]);\n        assertEquals(v6.name, vers[1]);\n\n        log.debugf(\"v4: %s\", v4);\n        log.debugf(\"v6: %s\", v6);\n    }\n}\n"
  },
  {
    "path": "maker/python/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb python generation implementation\n\n# Cli Command\n\n```\n# cd to the python maker root directory\n> python main.py\nip2region xdb maker\nmain.py [command] [command options]\nCommand:\n  gen      generate the binary db file\n```\n\n# `xdb` Data Generation\n\nGenerate the xdb binary file via the `python main.py gen` command:\n\n```\n➜  python git:(v2.0_xdb) ✗ python main.py gen\nmain.py gen [command options]\noptions:\n --src string    source ip text file path\n --dst string    destination binary xdb file path\n```\n\nFor example, using the default data/ipv4_source.txt as the source data to generate an ip2region_v4.xdb in the current directory:\n\n```\n➜  python git:(v2.0_xdb) ✗ python main.py gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb\n# You will see a lot of output; eventually, you will see something like the following output indicating the end of the execution\n...\n2022-07-13 19:58:00,540-root-238-INFO - write done, dataBlocks: 13804, indexBlocks: (683591, 720221), indexPtr: (982904, 11065984)\n2022-07-13 19:58:00,540-root-63-INFO - Done, elapsed: 3m3s\n```\n\n# `xdb` Data Query and bench Test\n\nFor query functions and testing based on the xdb format, see ip2region [bindings](../../binding)\n"
  },
  {
    "path": "maker/python/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb python 生成实现\n\n\n# cli 命令\n\n```\n# 切换到python maker 根目录\n> python main.py\nip2region xdb maker\nmain.py [command] [command options]\nCommand:\n  gen      generate the binary db file\n```\n\n# `xdb` 数据生成\n\n通过 `python main.py gen` 命令生成 xdb 二进制文件:\n```\n➜  python git:(v2.0_xdb) ✗ python main.py gen\nmain.py gen [command options]\noptions:\n --src string    source ip text file path\n --dst string    destination binary xdb file path\n```\n\n例如，使用默认的 data/ipv4_source.txt 作为源数据，生成一个 ip2region_v4.xdb 到当前目录：\n```\n➜  python git:(v2.0_xdb) ✗ python main.py gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb\n# 会看到一堆输出，最终会看到类似如下输出表示运行结束\n...\n2022-07-13 19:58:00,540-root-238-INFO - write done, dataBlocks: 13804, indexBlocks: (683591, 720221), indexPtr: (982904, 11065984)\n2022-07-13 19:58:00,540-root-63-INFO - Done, elapsed: 3m3s\n```\n\n\n# `xdb` 数据查询 和 bench 测试\n\n基于xdb 格式的查询功能和测试见 ip2region [binding](../../binding)\n"
  },
  {
    "path": "maker/python/main.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\nimport logging\nimport sys\nimport time\n\nimport xdb.maker as mk\nimport xdb.index as idx\n\n# Format log\nlogging.basicConfig(\n    level=logging.INFO,\n    format=\"%(asctime)s-%(name)s-%(lineno)s-%(levelname)s - %(message)s\",\n)\nlog = logging.getLogger(__name__)\n\n\ndef print_help():\n    print(\"ip2region xdb python maker\")\n    print(\"{} [command] [command options]\".format(sys.argv[0]))\n    print(\"Command: \")\n    print(\"  gen      generate the binary db file\")\n\n\ndef gen_db():\n    src_file, dst_file = \"\", \"\"\n    index_policy = idx.Vector_Index_Policy\n    # Check input parameters\n    for i in range(2, len(sys.argv)):\n        r = sys.argv[i]\n        if len(r) < 5:\n            continue\n        if not r.startswith(\"--\"):\n            continue\n        s_idx = r.index(\"=\")\n        if s_idx < 0:\n            print(\"missing = for args pair '{}'\".format(r))\n            return\n        if r[2:s_idx] == \"src\":\n            src_file = r[s_idx + 1:]\n        elif r[2:s_idx] == \"dst\":\n            dst_file = r[s_idx + 1:]\n        elif r[2:s_idx] == \"index\":\n            index_policy = idx.index_policy_from_string(r[s_idx + 1:])\n        else:\n            print(\"undefined option `{}`\".format(r))\n            return\n    if src_file == \"\" or dst_file == \"\":\n        print(\"{} gen [command options]\".format(sys.argv[0]))\n        print(\"options:\")\n        print(\" --src string    source ip text file path\")\n        print(\" --dst string    destination binary xdb file path\")\n        return\n\n    start_time = time.time()\n    # Make the binary file\n    maker = mk.new_maker(index_policy, src_file, dst_file)\n    maker.init()\n    maker.start()\n    maker.end()\n\n    logging.info(\n        \"Done, elapsed: {:.0f}m{:.0f}s\".format(\n            (time.time() - start_time) / 60, (time.time() - start_time) % 60\n        )\n    )\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print_help()\n        return\n\n    cmd = sys.argv[1].lower()\n    if cmd == \"gen\":\n        gen_db()\n    else:\n        print_help()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "maker/python/xdb/__init__.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\n"
  },
  {
    "path": "maker/python/xdb/index.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\nimport struct\n\nVector_Index_Policy = 1\nBTree_Index_Policy = 2\n\n\ndef index_policy_from_string(s: str) -> int:\n    sl = s.lower()\n    if sl == \"vector\":\n        return Vector_Index_Policy\n    elif sl == \"btree\":\n        return BTree_Index_Policy\n    else:\n        print(\"invalid policy `{}`, used default vector index\".format(s))\n        return Vector_Index_Policy\n\n\nclass VectorIndexBlock:\n    first_ptr = 0\n    last_ptr = 0\n\n    def __init__(self, fp=0, lp=0):\n        self.first_ptr = fp\n        self.last_ptr = lp\n\n    def __str__(self):\n        return \"FirstPtr: {}, LastPrt: {}\".format(self.first_ptr, self.last_ptr)\n\n    def encode(self) -> bytes:\n        return struct.pack(\"<II\", self.first_ptr, self.last_ptr)\n\n\nSegment_Index_Block_Size = 14\n\n\nclass SegmentIndexBlock:\n    start_ip = 0\n    end_ip = 0\n    data_len = 0\n    data_ptr = 0\n\n    def __init__(self, sip, eip, dl, dp):\n        self.start_ip = sip\n        self.end_ip = eip\n        self.data_len = dl\n        self.data_ptr = dp\n\n    def __str__(self):\n        return \"{sip: {}, eip: {}, len: {}, ptr: {}}\".format(\n            self.start_ip, self.end_ip, self.data_len, self.data_ptr\n        )\n\n    def encode(self) -> bytes:\n        return struct.pack(\n            \"<IIHI\", self.start_ip, self.end_ip, self.data_len, self.data_ptr\n        )\n"
  },
  {
    "path": "maker/python/xdb/maker.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\n# ----\n# ip2region database v2.0 structure\n#\n# +----------------+-------------------+---------------+--------------+\n# | header space   | speed up index    |  data payload | block index  |\n# +----------------+-------------------+---------------+--------------+\n# | 256 bytes      | 512 KiB (fixed)   | dynamic size  | dynamic size |\n# +----------------+-------------------+---------------+--------------+\n#\n# 1. padding space : for header info like block index ptr, version, release date eg ... or any other temporary needs.\n# -- 2bytes: version number, different version means structure update, it fixed to 2 for now\n# -- 2bytes: index algorithm code.\n# -- 4bytes: generate unix timestamp (version)\n# -- 4bytes: index block start ptr\n# -- 4bytes: index block end ptr\n#\n#\n# 2. data block : region or whatever data info.\n# 3. segment index block : binary index block.\n# 4. vector index block  : fixed index info for block index search speed up.\n# space structure table:\n# -- 0   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n# -- 1   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n# -- 2   -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n# -- ...\n# -- 255 -> | 1rt super block | 2nd super block | 3rd super block | ... | 255th super block\n#\n#\n# super block structure:\n# +-----------------------+----------------------+\n# | first index block ptr | last index block ptr |\n# +-----------------------+----------------------+\n#\n# data entry structure:\n# +--------------------+-----------------------+\n# | 2bytes (for desc)  |  dynamic length\t   |\n# +--------------------+-----------------------+\n#  data length   whatever in bytes\n#\n# index entry structure\n# +------------+-----------+---------------+------------+\n# | 4bytes\t   | 4bytes\t   | 2bytes\t\t   | 4 bytes    |\n# +------------+-----------+---------------+------------+\n#  start ip \t  end ip\t  data length     data ptr\nimport logging\nimport struct\nimport time\nimport sys\n\nimport xdb.segment as seg\nimport xdb.index as idx\nimport xdb.util as util\n\n\nVersion_No = 2\nHeader_Info_Length = 256\nVector_Index_Rows = 256\nVector_Index_Cols = 256\nVector_Index_Size = 8\nVector_Index_Length = Vector_Index_Rows * Vector_Index_Cols * Vector_Index_Size\n\n\nclass Maker:\n    src_handle = None\n    dst_handle = None\n    index_policy = idx.Vector_Index_Policy\n    segments = None\n    region_pool = None\n    vector_index = None\n\n    def __init__(self, sh, dh, ip, sg, rp, vi):\n        self.src_handle = sh\n        self.dst_handle = dh\n        self.index_policy = ip\n        self.segments = sg\n        self.region_pool = rp\n        self.vector_index = vi\n\n    def init(self):\n        \"\"\"\n        Init the `xdb` binary file.\n        1. Init the file header\n        2. Load all the segments\n        \"\"\"\n        self.init_db_header()\n        self.load_segments()\n\n    def init_db_header(self):\n        \"\"\"\n        Init and write the file header to the destination xdb file.\n        \"\"\"\n        logging.info(\"try to init the db header ... \")\n        self.src_handle.seek(0, 0)\n\n        # Make and write the header space\n        header = bytearray([0] * 256)\n        # 1. Version number\n        header[0:2] = Version_No.to_bytes(2, byteorder=\"little\")\n        # 2. Index policy code\n        header[2:4] = int(self.index_policy).to_bytes(2, byteorder=\"little\")\n        # 3. Generate unix timestamp\n        header[4:8] = int(time.time()).to_bytes(4, byteorder=\"little\")\n        # 4. Index block start ptr\n        header[8:12] = int(0).to_bytes(4, byteorder=\"little\")\n        # 5. Index block end ptr\n        header[12:16] = int(0).to_bytes(4, byteorder=\"little\")\n        # Write header buffer to file\n        self.dst_handle.write(header)\n\n    def load_segments(self):\n        \"\"\"\n        Load the segments [start ip|end ip|region] from source ip text file.\n        :return: the list of Segment\n        \"\"\"\n        logging.info(\"try to load the segments ... \")\n        last = None\n        s_tm = time.time()\n\n        lines = self.src_handle.read().splitlines()\n        for line in lines:\n            logging.info(\"load segment: `{}`\".format(line))\n            ps = line.split(\"|\", maxsplit=2)\n            if len(ps) != 3:\n                raise Exception(\"invalid ip segment line `{}`\".format(line))\n\n            sip = util.check_ip(ps[0])\n            if sip == -1:\n                raise Exception(\n                    \"invalid ip address `{}` in line `{}`\".format(ps[0], line)\n                )\n            eip = util.check_ip(ps[1])\n            if eip == -1:\n                raise Exception(\n                    \"invalid ip address `{}` in line `{}`\".format(ps[1], line)\n                )\n\n            if sip > eip:\n                raise Exception(\n                    \"start ip({}) should not be greater than end ip({})\".format(\n                        ps[0], ps[1]\n                    )\n                )\n            if len(ps[2]) < 1:\n                raise Exception(\"empty region info in segment line `{}`\".format(line))\n\n            segment = seg.Segment(sip=sip, eip=eip, reg=ps[2])\n            # Check the continuity of data segment\n            if last is not None:\n                if last.end_ip + 1 != segment.start_ip:\n                    raise Exception(\n                        \"discontinuous data segment: last.eip+1({})!=seg.sip({}, {})\".format(\n                            sip, eip, ps[0]\n                        )\n                    )\n            self.segments.append(segment)\n            last = segment\n        logging.info(\n            \"all segments loaded, length: {}, elapsed: {}\".format(\n                len(self.segments), time.time() - s_tm\n            )\n        )\n\n    def set_vector_index(self, ip, ptr):\n        \"\"\"\n        Init and refresh the vector index based on the IP pre-two bytes.\n        \"\"\"\n        row, col = (ip >> 24) & 0xFF, (ip >> 16) & 0xFF\n        vi_block = self.vector_index[row][col]\n        if vi_block.first_ptr == 0:\n            vi_block.first_ptr = ptr\n            vi_block.last_ptr = ptr + idx.Segment_Index_Block_Size\n        else:\n            vi_block.last_ptr = ptr + idx.Segment_Index_Block_Size\n        self.vector_index[row][col] = vi_block\n\n    def start(self):\n        \"\"\"\n        Start to make the 'xdb' binary file.\n        \"\"\"\n        if len(self.segments) < 1:\n            raise Exception(\"empty segment list\")\n\n        # 1. Write all the region/data to the binary file\n        self.dst_handle.seek(Header_Info_Length + Vector_Index_Length, 0)\n\n        logging.info(\"try to write the data block ... \")\n        for s in self.segments:\n            logging.info(\"try to write region '{}'...\".format(s.region))\n            if s.region in self.region_pool:\n                logging.info(\n                    \" --[Cached] with ptr={}\".format(self.region_pool[s.region])\n                )\n                continue\n            region = bytes(s.region, encoding=\"utf-8\")\n            if len(region) > 0xFFFF:\n                raise Exception(\n                    \"too long region info `{}`: should be less than {} bytes\".format(\n                        s.region, 0xFFFF\n                    )\n                )\n            # Get the first ptr of the next region\n            pos = self.dst_handle.seek(0, 1)\n            logging.info(\"{} {} {}\".format(pos, region, s.region))\n            self.dst_handle.write(region)\n            self.region_pool[s.region] = pos\n            logging.info(\" --[Added] with ptr={}\".format(pos))\n        # 2. Write the index block and cache the super index block\n        logging.info(\"try to write the segment index block ... \")\n        counter, start_index_ptr, end_index_ptr = 0, -1, -1\n        for sg in self.segments:\n            if sg.region not in self.region_pool:\n                raise Exception(\"missing ptr cache for region `{}`\".format(sg.region))\n            data_len = len(bytes(sg.region, encoding=\"utf-8\"))\n            if data_len < 1:\n                raise Exception(\"empty region info for segment '{}'\".format(sg.region))\n\n            seg_list = sg.split()\n            logging.info(\n                \"try to index segment({} split) {} ...\".format(len(seg_list), sg)\n            )\n            for s in seg_list:\n                pos = self.dst_handle.seek(0, 1)\n\n                s_index = idx.SegmentIndexBlock(\n                    sip=s.start_ip,\n                    eip=s.end_ip,\n                    dl=data_len,\n                    dp=self.region_pool[sg.region],\n                )\n                self.dst_handle.write(s_index.encode())\n                logging.info(\n                    \"|-segment index: {}, ptr: {}, segment: {}\".format(counter, pos, s)\n                )\n                self.set_vector_index(s.start_ip, pos)\n                counter += 1\n\n                # Check and record the start index ptr\n                if start_index_ptr == -1:\n                    start_index_ptr = pos\n                end_index_ptr = pos\n\n        # 3. Synchronized the vector index block\n        logging.info(\"try to write the vector index block ... \")\n        self.dst_handle.seek(Header_Info_Length, 0)\n        for i in range(0, len(self.vector_index)):\n            for j in range(0, len(self.vector_index[i])):\n                vi = self.vector_index[i][j]\n                self.dst_handle.write(vi.encode())\n\n        # 4. Synchronized the segment index info\n        logging.info(\"try to write the segment index ptr ... \")\n        buff = struct.pack(\"<II\", start_index_ptr, end_index_ptr)\n        self.dst_handle.seek(8, 0)\n        self.dst_handle.write(buff)\n\n        logging.info(\n            \"write done, dataBlocks: {}, indexBlocks: ({}, {}), indexPtr: ({}, {})\".format(\n                len(self.region_pool),\n                len(self.segments),\n                counter,\n                start_index_ptr,\n                end_index_ptr,\n            )\n        )\n\n    def end(self):\n        \"\"\"\n        End of make the 'xdb' binary file.\n        \"\"\"\n        try:\n            self.src_handle.close()\n            self.dst_handle.close()\n        except IOError as e:\n            logging.error(e)\n            sys.exit()\n\n\ndef new_maker(policy: int, srcfile: str, dstfile: str) -> Maker:\n    \"\"\"\n    Create a xdb Maker to make the xdb binary file\n    :param policy: index algorithm code 1:vector, 2:b-tree\n    :param srcfile: source ip text file path\n    :param dstfile: destination binary xdb file path\n    :return: the 'xdb' Maker\n    \"\"\"\n    try:\n        sh = open(srcfile, mode=\"r\", encoding=\"utf-8\")\n        dh = open(dstfile, mode=\"wb\")\n        return Maker(\n            sh=sh,\n            dh=dh,\n            ip=policy,\n            sg=[],\n            rp={},\n            vi=[\n                [idx.VectorIndexBlock() for _ in range(Vector_Index_Rows)]\n                for _ in range(Vector_Index_Cols)\n            ],\n        )\n    except IOError as e:\n        logging.error(e)\n        sys.exit()\n"
  },
  {
    "path": "maker/python/xdb/segment.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\nimport xdb.util as util\n\n\nclass Segment:\n    start_ip = 0\n    end_ip = 0\n    region = \"\"\n\n    def __init__(self, sip=0, eip=0, reg=\"\"):\n        self.start_ip, self.end_ip = sip, eip\n        self.region = reg\n\n    def __str__(self):\n        return \"{}|{}|{}\".format(\n            util.long2ip(self.start_ip), util.long2ip(self.end_ip), self.region\n        )\n\n    def split(self) -> list:\n        \"\"\"\n        Split the segment based on the pre-two bytes.\n        :return: the list of segment ofter split\n        \"\"\"\n        # Example:\n        # split the segment \"116.31.76.0|117.21.79.49|region\"\n        #\n        # Return the list with segments:\n        # 116.31.76.0 | 116.31.255.255  | region\n        # 116.32.0.0  | 116.32.255.255  | region\n        # ...         | ...             | region\n        # 116.255.0.0 | 116.255.255.255 | region\n        # 117.0.0.0   | 117.0.255.255   | region\n        # 117.1.0.0   | 117.1.255.255   | region\n        # ...         | ...             | region\n        # 117.21.0.0  | 117.21.79.49    | region\n\n        # 1. Split the segment with the first byte\n        t_list_1 = []\n        s_byte_1, e_byte_1 = (self.start_ip >> 24) & 0xFF, (self.end_ip >> 24) & 0xFF\n        n_sip = self.start_ip\n        for i in range(s_byte_1, e_byte_1 + 1):\n            sip = (i << 24) | (n_sip & 0xFFFFFF)\n            eip = (i << 24) | 0xFFFFFF\n            if eip < self.end_ip:\n                n_sip = (i + 1) << 24\n            else:\n                eip = self.end_ip\n            # Append the new segment (maybe)\n            t_list_1.append(Segment(sip, eip))\n\n        # 2. Split the segments with the second byte\n        t_list_2 = []\n        for s in t_list_1:\n            base = s.start_ip & 0xFF000000\n            n_sip = s.start_ip\n            s_byte_2, e_byte_2 = (s.start_ip >> 16) & 0xFF, (s.end_ip >> 16) & 0xFF\n            for i in range(s_byte_2, e_byte_2 + 1):\n                sip = base | (i << 16) | (n_sip & 0xFFFF)\n                eip = base | (i << 16) | 0xFFFF\n                if eip < self.end_ip:\n                    n_sip = 0\n                else:\n                    eip = self.end_ip\n                t_list_2.append(Segment(sip, eip, self.region))\n        return t_list_2\n"
  },
  {
    "path": "maker/python/xdb/util.py",
    "content": "# Copyright 2022 The Ip2Region Authors. All rights reserved.\n# Use of this source code is governed by a Apache2.0-style\n# license that can be found in the LICENSE file.\n#\n# Author: linyufeng <leolin49@foxmail.com>\n# Date  : 2022/7/14 17:00\n#\n_SHIFT_INDEX = (24, 16, 8, 0)\n\n\ndef check_ip(ip: str) -> int:\n    \"\"\"\n    Convert ip string to integer.\n    Return -1 if ip is not the correct ipv4 address.\n    \"\"\"\n    if not is_ipv4(ip):\n        return -1\n    ps = ip.split(\".\")\n    val = 0\n    for i in range(len(ps)):\n        d = int(ps[i])\n        val |= d << _SHIFT_INDEX[i]\n    return val\n\n\ndef long2ip(num: int) -> str:\n    \"\"\"\n    Convert integer to ip string.\n    Return empty string if the num greater than UINT32_MAX or less than 0.\n    \"\"\"\n    if num < 0 or num > 0xFFFFFFFF:\n        return \"\"\n    return \"{}.{}.{}.{}\".format(\n        (num >> 24) & 0xFF, (num >> 16) & 0xFF, (num >> 8) & 0xFF, num & 0xFF\n    )\n\n\ndef is_ipv4(ip: str) -> bool:\n    \"\"\"\n    Determine whether it is an ipv4 address.\n    \"\"\"\n    ps = ip.split(\".\")\n    if len(ps) != 4:\n        return False\n    for p in ps:\n        if not p.isdigit() or len(p) > 3 or (int(p) < 0 or int(p) > 255):\n            return False\n    return True\n"
  },
  {
    "path": "maker/rust/README.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb rust generation implementation\n\n## Program Compilation\n\n```bash\n$ cd maker/rust/maker\n$ cargo build -r\n```\n\nAfter successful compilation, the executable file is located at `./target/release/maker`\n\n## `xdb` Data Generation\n\n```bash\n# CWD ip2region/maker/rust/maker\n$ ./target/release/maker --help\nUsage: maker [OPTIONS] --src <SRC> --dst <DST> --ip-version <IP_VERSION>\n\nOptions:\n      --src <SRC>\n          ip source region txt filepath\n\n      --dst <DST>\n          generated xdb filepath\n\n      --ip-version <IP_VERSION>\n          Possible values:\n          - v4: IPv4\n          - v6: Ipv6\n\n      --index-policy <INDEX_POLICY>\n          index cache policy\n\n          [default: vector-index]\n          [possible values: vector-index, b-tree-index]\n\n      --filter-fields <FILTER_FIELDS>\n          region filter fields, the index of the fields, e.g. `1,2,3,5`\n\n  -h, --help\n          Print help (see a summary with '-h')\n```\n\nFor example, use the default raw data under the repository's data/ directory to generate the xdb file to the current directory (`ip2region/maker/rust/maker`):\n\n```bash\n# ipv6\n./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6\n# ipv4\n./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4\n```\n\n## `xdb` Data Search and bench Test\n\nFor search functions and testing based on the xdb format, see [ip2region bindings](../../binding)\n\n## Comparison with xdb files generated by other makers\n\nIt is recommended to use `vbindiff`. The only difference from other files should be the create time information; all other data must be identical.\n\nBuild xdb using the golang version maker\n\n```bash\n$ cd maker golang\n$ make\n$ ./xdb_maker gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n$ ./xdb_maker gen --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n```\n\nCompare xdb differences\n\n```bash\n$ cd maker/rust/maker\n# Generate xdb files using rust maker\n$ ./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4\n$ ./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6\n# Compare with golang generated files\n$ vbindiff ./ipv4.xdb ../../golang/ip2region_v4.xdb\n$ vbindiff ./ipv6.xdb ../../golang/ip2region_v6.xdb\n```\n"
  },
  {
    "path": "maker/rust/README_zh.md",
    "content": ":globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)\n\n# ip2region xdb rust 生成实现\n\n## 程序编译\n```bash\n$ cd maker/rust/maker\n$ cargo build -r\n```\n编译成功以后，执行文件位置 `./target/release/maker`\n\n## `xdb` 数据生成\n```bash\n# CWD ip2region/maker/rust/maker\n$ ./target/release/maker --help\nUsage: maker [OPTIONS] --src <SRC> --dst <DST> --ip-version <IP_VERSION>\n\nOptions:\n      --src <SRC>\n          ip source region txt filepath\n\n      --dst <DST>\n          generated xdb filepath\n\n      --ip-version <IP_VERSION>\n          Possible values:\n          - v4: IPv4\n          - v6: Ipv6\n\n      --index-policy <INDEX_POLICY>\n          index cache policy\n\n          [default: vector-index]\n          [possible values: vector-index, b-tree-index]\n\n      --filter-fields <FILTER_FIELDS>\n          region filter fields, the index of the fields, e.g. `1,2,3,5`\n\n  -h, --help\n          Print help (see a summary with '-h')\n```\n\n例如，使用默认的仓库 data/ 下默认的原始数据生成生成 xdb 文件到当前目录(`ip2region/maker/rust/maker`)：\n```bash\n# ipv6\n./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6\n# ipv4\n./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4\n```\n\n## `xdb` 数据查询 和 bench 测试\n\n基于xdb 格式的查询功能和测试见 [ip2region binding](../../binding)\n\n## 对比其他 maker 生成的 xdb 文件 \n推荐使用 `vbindiff`, 与其他文件的差异只有 create time 信息上有差异，其他数据都需要是一样的\n\ngolang 版本 maker 构建 xdb\n```bash\n$ cd maker golang\n$ make\n$ ./xdb_maker gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4\n$ ./xdb_maker gen --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6\n```\n\n对比 xdb 差异\n```bash\n$ cd maker/rust/maker\n$ ./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4\n$ ./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6\n$ vbindiff ./ipv4.xdb ../../golang/ip2region_v4.xdb\n$ vbindiff ./ipv6.xdb ../../golang/ip2region_v6.xdb\n```\n"
  },
  {
    "path": "maker/rust/maker/Cargo.toml",
    "content": "[package]\nname = \"maker\"\nversion = \"0.2.0\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nclap = { version = \"4.5\", features = [\"derive\"] }\nthiserror = \"2\"\nbytes = \"1\"\nnum-derive = \"0.4.2\"\nnum-traits = \"0.2.19\"\nchrono = \"0.4\"\nitertools = \"0.14\"\ntracing = \"0.1\"\ntracing-subscriber = \"0.3\"\n"
  },
  {
    "path": "maker/rust/maker/src/command.rs",
    "content": "use clap::Parser;\n\nuse crate::IpVersion;\nuse crate::header::IndexPolicy;\n\n#[derive(Parser, Debug)]\npub struct Command {\n    /// ip source region txt filepath\n    #[arg(long)]\n    pub src: String,\n    /// generated xdb filepath\n    #[clap(long)]\n    pub dst: String,\n    #[clap(long, value_enum)]\n    pub ip_version: IpVersion,\n    /// index cache policy\n    #[clap(long, value_enum, default_value_t = IndexPolicy::VectorIndex)]\n    pub index_policy: IndexPolicy,\n    /// region filter fields, the index of the fields, e.g. `1,2,3,5`\n    #[clap(long, value_delimiter = ',')]\n    pub filter_fields: Vec<usize>,\n}\n"
  },
  {
    "path": "maker/rust/maker/src/error.rs",
    "content": "#[derive(Debug, thiserror::Error)]\npub enum MakerError {\n    #[error(\"Io error: {0}\")]\n    IoError(#[from] std::io::Error),\n\n    #[error(\"Header parse error: {0}\")]\n    HeaderParsed(String),\n\n    #[error(\"Parse line src ip, dst ip, region failed for line: {0}\")]\n    ParseIPRegion(String),\n\n    #[error(\"Invalid sip/eip version\")]\n    InvalidIPVersion,\n\n    #[error(\"Ipaddr parse error: {0}\")]\n    IpaddrParseError(#[from] std::net::AddrParseError),\n\n    #[error(\"Region filter fields value too big, limit: {limit}, actual: {actual}\")]\n    RegionFilterFieldsTooBig { limit: usize, actual: usize },\n\n    #[error(\"Empty segments\")]\n    EmptySegments,\n\n    #[error(\"Try from int failed\")]\n    TryFromIntError(#[from] std::num::TryFromIntError),\n\n    #[error(\"Try from slice failed\")]\n    TryFromSliceFailed(#[from] std::array::TryFromSliceError),\n\n    #[error(\"Region could not found\")]\n    RegionNotFound,\n}\n\npub type Result<T> = std::result::Result<T, MakerError>;\n"
  },
  {
    "path": "maker/rust/maker/src/header.rs",
    "content": "use std::fmt::Display;\nuse std::net::IpAddr;\n\nuse bytes::{BufMut, Bytes, BytesMut};\nuse clap::ValueEnum;\nuse itertools::Itertools;\nuse num_derive::FromPrimitive;\nuse num_traits::FromPrimitive;\n\nuse crate::error::{MakerError, Result};\n\npub const VERSION_NO: u16 = 3; // since 2025/09/01 (IPv6 supporting)\npub const HEADER_INFO_LENGTH: usize = 256;\npub const VECTOR_INDEX_COLS: usize = 256;\npub const VECTOR_INDEX_ROWS: usize = 256;\npub const VECTOR_INDEX_SIZE: usize = 8;\npub const VECTOR_INDEX_LENGTH: usize = VECTOR_INDEX_COLS * VECTOR_INDEX_ROWS * VECTOR_INDEX_SIZE;\npub const RUNTIME_PTR_SIZE: u16 = 4;\npub const REGION_START: u64 = (HEADER_INFO_LENGTH + VECTOR_INDEX_LENGTH) as u64;\n\n#[allow(dead_code)]\n#[derive(Debug)]\npub struct Header {\n    version: u16,\n    index_policy: IndexPolicy,\n    create_time: u32,\n    start_index_ptr: u32,\n    end_index_ptr: u32,\n    ip_version: IpVersion,\n    runtime_ptr_bytes: u16,\n}\n\nimpl TryFrom<&[u8; 256]> for Header {\n    type Error = MakerError;\n\n    fn try_from(value: &[u8; 256]) -> Result<Self> {\n        if value.len() < 20 {\n            return Err(MakerError::HeaderParsed(\"Header bytes too short\".into()));\n        }\n\n        let index_policy_value = u16::from_le_bytes([value[2], value[3]]);\n        let ip_version_value = u16::from_le_bytes([value[16], value[17]]);\n\n        Ok(Header {\n            version: u16::from_le_bytes([value[0], value[1]]),\n            index_policy: IndexPolicy::from_u16(index_policy_value).ok_or_else(|| {\n                MakerError::HeaderParsed(format!(\n                    \"Header index policy invalid: {index_policy_value}\"\n                ))\n            })?,\n            create_time: u32::from_le_bytes([value[4], value[5], value[6], value[7]]),\n            start_index_ptr: u32::from_le_bytes([value[8], value[9], value[10], value[11]]),\n            end_index_ptr: u32::from_le_bytes([value[12], value[13], value[14], value[15]]),\n\n            ip_version: IpVersion::from_u16(ip_version_value).ok_or_else(|| {\n                MakerError::HeaderParsed(format!(\"Header ip version invalid: {ip_version_value}\"))\n            })?,\n            runtime_ptr_bytes: u16::from_le_bytes([value[18], value[19]]),\n        })\n    }\n}\n\nimpl Header {\n    pub fn new(index_policy: IndexPolicy, ip_version: IpVersion) -> Header {\n        Header {\n            version: VERSION_NO,\n            index_policy,\n            create_time: chrono::Utc::now().timestamp() as u32,\n            start_index_ptr: 0,\n            end_index_ptr: 0,\n            ip_version,\n            runtime_ptr_bytes: RUNTIME_PTR_SIZE,\n        }\n    }\n\n    pub fn encode_bytes(&self, start_index_ptr: u32, end_index_ptr: u32) -> Bytes {\n        let mut buf = BytesMut::with_capacity(HEADER_INFO_LENGTH);\n        buf.put_u16_le(VERSION_NO);\n        buf.put_u16_le(self.index_policy as u16);\n        buf.put_u32_le(self.create_time);\n        buf.put_u32_le(start_index_ptr);\n        // index block end ptr\n        buf.put_u32_le(end_index_ptr);\n        buf.put_u16_le(self.ip_version as u16);\n        buf.put_u16_le(self.runtime_ptr_bytes);\n        buf.freeze()\n    }\n}\n\n#[derive(FromPrimitive, Debug, Copy, Clone, ValueEnum)]\n#[repr(u16)]\npub enum IndexPolicy {\n    VectorIndex = 1,\n    BTreeIndex = 2,\n}\n\nimpl Display for IndexPolicy {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            IndexPolicy::VectorIndex => write!(f, \"VectorIndex\"),\n            IndexPolicy::BTreeIndex => write!(f, \"BTreeIndex\"),\n        }\n    }\n}\n\n#[derive(FromPrimitive, Debug, Copy, Clone, ValueEnum, PartialEq)]\n#[repr(u16)]\npub enum IpVersion {\n    /// IPv4\n    V4 = 4,\n    /// Ipv6\n    V6 = 6,\n}\n\nimpl IpVersion {\n    pub fn ip_bytes_len(&self) -> usize {\n        match &self {\n            IpVersion::V4 => 4,\n            IpVersion::V6 => 16,\n        }\n    }\n\n    pub fn segment_index_size(&self) -> usize {\n        match &self {\n            IpVersion::V4 => 14,\n            IpVersion::V6 => 38,\n        }\n    }\n}\n\nimpl Header {\n    pub fn ip_bytes_len(&self) -> usize {\n        self.ip_version.ip_bytes_len()\n    }\n\n    pub fn segment_index_size(&self) -> usize {\n        self.ip_version.segment_index_size()\n    }\n\n    pub fn ip_version(&self) -> &IpVersion {\n        &self.ip_version\n    }\n}\n\npub trait IPAddrExt {\n    fn ipaddr_bytes(&self) -> Vec<u8>;\n\n    fn encode_ipaddr_bytes(&self) -> Vec<u8>;\n}\n\nimpl IPAddrExt for IpAddr {\n    fn ipaddr_bytes(&self) -> Vec<u8> {\n        match self {\n            IpAddr::V4(addr) => addr.octets().to_vec(),\n            IpAddr::V6(addr) => addr.octets().to_vec(),\n        }\n    }\n\n    fn encode_ipaddr_bytes(&self) -> Vec<u8> {\n        match self {\n            IpAddr::V4(addr) => addr.octets().into_iter().rev().collect_vec(),\n            IpAddr::V6(addr) => addr.octets().to_vec(),\n        }\n    }\n}\n"
  },
  {
    "path": "maker/rust/maker/src/lib.rs",
    "content": "mod command;\nmod error;\nmod header;\nmod maker;\nmod segment;\n\npub use command::Command;\npub use error::{MakerError, Result};\npub use header::{\n    HEADER_INFO_LENGTH, Header, IpVersion, REGION_START, VECTOR_INDEX_COLS, VECTOR_INDEX_LENGTH,\n    VECTOR_INDEX_SIZE,\n};\npub use maker::Maker;\n"
  },
  {
    "path": "maker/rust/maker/src/main.rs",
    "content": "use std::time::Instant;\n\nuse clap::Parser;\nuse maker::{Command, Maker, Result};\nuse tracing::info;\n\n/// Ip2Region database structure\n/// See https://github.com/lionsoul2014/ip2region/blob/master/maker/golang/xdb/maker.go\nfn main() -> Result<()> {\n    tracing_subscriber::fmt::init();\n    let now = Instant::now();\n    let cmd = Command::parse();\n    info!(?cmd, \"Generate xdb\");\n    let mut maker = Maker::new(\n        cmd.ip_version,\n        cmd.index_policy,\n        &cmd.src,\n        &cmd.dst,\n        cmd.filter_fields,\n    )?;\n    maker.start()?;\n\n    info!(cost_time=?now.elapsed(), \"Make completed\");\n    Ok(())\n}\n"
  },
  {
    "path": "maker/rust/maker/src/maker.rs",
    "content": "use std::collections::HashMap;\nuse std::fs::File;\nuse std::io::{Seek, SeekFrom, Write};\nuse std::sync::Arc;\n\nuse bytes::{BufMut, BytesMut};\nuse itertools::Itertools;\nuse tracing::{info, trace};\n\nuse crate::error::{MakerError, Result};\nuse crate::header::{IPAddrExt, IndexPolicy, IpVersion, VECTOR_INDEX_ROWS};\nuse crate::segment::Segment;\nuse crate::{HEADER_INFO_LENGTH, Header, REGION_START, VECTOR_INDEX_COLS, VECTOR_INDEX_SIZE};\n\npub struct Maker {\n    ip_version: IpVersion,\n    dst_file: File,\n    region_pool: HashMap<Arc<String>, u32>,\n    vector_index: [[[u8; VECTOR_INDEX_SIZE]; VECTOR_INDEX_ROWS]; VECTOR_INDEX_COLS],\n    segments: Vec<Segment>,\n    header: Header,\n}\n\nimpl Maker {\n    pub fn new(\n        ip_version: IpVersion,\n        index_policy: IndexPolicy,\n        src_filepath: &str,\n        end_filepath: &str,\n        filter_fields: Vec<usize>,\n    ) -> Result<Self> {\n        let header = Header::new(index_policy, ip_version);\n\n        let segments = Segment::from_file(src_filepath, ip_version, &filter_fields)?;\n        if segments.is_empty() {\n            return Err(MakerError::EmptySegments);\n        }\n\n        let mut region_pool = HashMap::with_capacity(segments.len());\n        let mut dst_file = File::create(end_filepath)?;\n        let mut region_buf = BytesMut::new();\n        let mut current = u32::try_from(REGION_START)?;\n        for region in segments.iter().map(|s| s.region.clone()).unique() {\n            region_buf.extend_from_slice(region.as_bytes());\n            let region_len = region.len() as u32;\n            region_pool.insert(region, current);\n            current += region_len;\n        }\n        dst_file.seek(SeekFrom::Start(REGION_START))?;\n        dst_file.write_all(region_buf.as_ref())?;\n        info!(\"Load region pool successfully\");\n\n        Ok(Self {\n            ip_version,\n            dst_file,\n            region_pool,\n            vector_index: [[[0; VECTOR_INDEX_SIZE]; VECTOR_INDEX_ROWS]; VECTOR_INDEX_COLS],\n            segments,\n            header,\n        })\n    }\n\n    fn set_vector_index(&mut self, ip: &[u8], ptr: u32) -> Result<()> {\n        let (l0, l1) = (ip[0] as usize, ip[1] as usize);\n        let block = &mut self.vector_index[l0][l1];\n\n        if block[0..4].eq(&[0; 4]) {\n            block[0..4].copy_from_slice(&ptr.to_le_bytes());\n        }\n        let end_value = ptr + self.ip_version.segment_index_size() as u32;\n        block[4..].copy_from_slice(&end_value.to_le_bytes());\n        Ok(())\n    }\n\n    pub fn start(&mut self) -> Result<()> {\n        let start_index_ptr = u32::try_from(self.dst_file.stream_position()?)?;\n        let mut segment_count = 0;\n        let mut buf =\n            BytesMut::with_capacity(self.ip_version.segment_index_size() * self.segments.len());\n\n        for segment in std::mem::take(&mut self.segments) {\n            let region_ptr = *self\n                .region_pool\n                .get(&segment.region)\n                .ok_or(MakerError::RegionNotFound)?;\n            let region_len = u16::try_from(segment.region.len())?;\n\n            trace!(?segment, \"before segment split\");\n            for seg in segment.split()? {\n                self.set_vector_index(\n                    &seg.start_ip.ipaddr_bytes(),\n                    start_index_ptr + buf.len() as u32,\n                )?;\n\n                buf.put_slice(&seg.start_ip.encode_ipaddr_bytes());\n                buf.put_slice(&seg.end_ip.encode_ipaddr_bytes());\n                buf.put_u16_le(region_len);\n                buf.put_u32_le(region_ptr);\n                segment_count += 1;\n            }\n        }\n\n        info!(\n            region_pool_len = self.region_pool.len(),\n            segment_count, \"Write segment index buffer\"\n        );\n        self.dst_file\n            .seek(SeekFrom::Start(start_index_ptr as u64))?;\n        self.dst_file.write_all(buf.as_ref())?;\n\n        info!(\"Write header buffer\");\n        let header_buf = self.header.encode_bytes(\n            start_index_ptr,\n            start_index_ptr + (buf.len() as u32) - (self.ip_version.segment_index_size() as u32),\n        );\n        self.dst_file.seek(SeekFrom::Start(0))?;\n        self.dst_file.write_all(header_buf.as_ref())?;\n\n        info!(\"Write vector index buffer\");\n        self.dst_file\n            .seek(SeekFrom::Start(HEADER_INFO_LENGTH as u64))?;\n        self.dst_file\n            .write_all(self.vector_index.as_flattened().as_flattened())?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "maker/rust/maker/src/segment.rs",
    "content": "use std::fs::File;\nuse std::io::{BufRead, BufReader};\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr};\nuse std::str::FromStr;\nuse std::sync::Arc;\n\nuse itertools::Itertools;\nuse tracing::{debug, info, trace};\n\nuse crate::IpVersion;\nuse crate::error::{MakerError, Result};\nuse crate::header::IPAddrExt;\n\npub trait IpPlusEq {\n    fn ip_plus_eq(&self, other: &Self) -> bool;\n}\n\nimpl IpPlusEq for IpAddr {\n    fn ip_plus_eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (IpAddr::V4(start), IpAddr::V4(end)) => Ipv4Addr::from(u32::from(*start) + 1).eq(end),\n            (IpAddr::V6(start), IpAddr::V6(end)) => Ipv6Addr::from(u128::from(*start) + 1).eq(end),\n            _ => false,\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct Segment {\n    pub start_ip: IpAddr,\n    pub end_ip: IpAddr,\n    pub region: Arc<String>,\n}\n\nfn region_filter(region: &str, filter_fields: &[usize]) -> Result<String> {\n    if filter_fields.is_empty() {\n        return Ok(region.to_owned());\n    }\n    let fields = region.split('|').collect::<Vec<_>>();\n    let filtered = filter_fields\n        .iter()\n        .map(|idx| {\n            fields\n                .get(*idx)\n                .ok_or(MakerError::RegionFilterFieldsTooBig {\n                    limit: fields.len(),\n                    actual: *idx,\n                })\n        })\n        .collect::<Result<Vec<_>>>()?;\n    Ok(filtered.into_iter().join(\"|\"))\n}\n\nimpl Segment {\n    pub fn from_file(\n        src_filepath: &str,\n        ip_version: IpVersion,\n        filter_fields: &[usize],\n    ) -> Result<Vec<Segment>> {\n        info!(\"Read src file\");\n\n        let mut last = None;\n        let mut segments = vec![];\n\n        let reader = BufReader::new(File::open(src_filepath)?);\n        for original_line in reader.lines() {\n            let original_line = original_line?;\n            let line = original_line.trim();\n            if line.is_empty() || line.starts_with('#') {\n                continue;\n            }\n\n            trace!(?line, \"Processing line\");\n            let v = line.splitn(3, '|').collect::<Vec<_>>();\n            if v.len() != 3 {\n                return Err(MakerError::ParseIPRegion(line.to_owned()));\n            }\n            let (start_ip, end_ip, region) = (v[0], v[1], v[2]);\n\n            let (start_ip, end_ip) = if ip_version.eq(&IpVersion::V4) {\n                (\n                    IpAddr::V4(Ipv4Addr::from_str(start_ip)?),\n                    IpAddr::V4(Ipv4Addr::from_str(end_ip)?),\n                )\n            } else {\n                (\n                    IpAddr::V6(Ipv6Addr::from_str(start_ip)?),\n                    IpAddr::V6(Ipv6Addr::from_str(end_ip)?),\n                )\n            };\n            if start_ip.gt(&end_ip) {\n                return Err(MakerError::ParseIPRegion(line.to_owned()));\n            }\n\n            let segment = Segment {\n                start_ip,\n                end_ip,\n                region: Arc::new(region_filter(region, filter_fields)?),\n            };\n            match last.take() {\n                None => {\n                    last = Some(segment);\n                }\n                Some(mut l)\n                    if segment.region.eq(&l.region) && l.end_ip.ip_plus_eq(&segment.start_ip) =>\n                {\n                    l.end_ip = segment.end_ip;\n                    last = Some(l);\n                }\n                Some(seg) => {\n                    segments.push(seg);\n                    last = Some(segment);\n                }\n            }\n        }\n\n        if let Some(last) = last {\n            segments.push(last);\n        }\n\n        info!(length = segments.len(), \"load segments\");\n        Ok(segments)\n    }\n\n    pub fn split(self) -> Result<Vec<Segment>> {\n        let start_bytes = self.start_ip.ipaddr_bytes();\n        let end_bytes = self.end_ip.ipaddr_bytes();\n\n        let start_byte = u16::from_be_bytes([start_bytes[0], start_bytes[1]]);\n        let end_byte = u16::from_be_bytes([end_bytes[0], end_bytes[1]]);\n\n        let segments = (start_byte..=end_byte)\n            .map(|index| {\n                let sip = if index == start_byte {\n                    self.start_ip\n                } else if self.start_ip.is_ipv4() {\n                    IpAddr::from(Ipv4Addr::from((index as u32) << 16))\n                } else {\n                    IpAddr::from(Ipv6Addr::from((index as u128) << 112))\n                };\n\n                let eip = if index == end_byte {\n                    self.end_ip\n                } else if self.start_ip.is_ipv4() {\n                    let mask = (1 << 16) - 1;\n                    let v = (index as u32) << 16;\n                    IpAddr::from(Ipv4Addr::from(v | mask))\n                } else {\n                    let mask = (1 << 112) - 1;\n                    let v = (index as u128) << 112;\n                    IpAddr::from(Ipv6Addr::from(v | mask))\n                };\n\n                trace!(?index, ?sip, ?eip, ?self.region, \"in split segment\");\n                Segment {\n                    start_ip: sip,\n                    end_ip: eip,\n                    region: self.region.clone(),\n                }\n            })\n            .collect_vec();\n        debug!(?self, length = segments.len(), \"Try to index segment\");\n\n        Ok(segments)\n    }\n}\n"
  }
]