├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── test.yml │ └── zizmor.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.dev.md ├── README.md ├── checkstyle-suppressions.xml ├── checkstyle.xml ├── dev-bin └── release.sh ├── pom.xml ├── sample └── Benchmark.java └── src ├── main └── java │ ├── com │ └── maxmind │ │ └── db │ │ ├── BufferHolder.java │ │ ├── CHMCache.java │ │ ├── CacheKey.java │ │ ├── CachedConstructor.java │ │ ├── ClosedDatabaseException.java │ │ ├── ConstructorNotFoundException.java │ │ ├── CtrlData.java │ │ ├── DatabaseRecord.java │ │ ├── DecodedValue.java │ │ ├── Decoder.java │ │ ├── DeserializationException.java │ │ ├── InvalidDatabaseException.java │ │ ├── InvalidNetworkException.java │ │ ├── MaxMindDbConstructor.java │ │ ├── MaxMindDbParameter.java │ │ ├── Metadata.java │ │ ├── Network.java │ │ ├── Networks.java │ │ ├── NetworksIterationException.java │ │ ├── NoCache.java │ │ ├── NodeCache.java │ │ ├── ParameterNotFoundException.java │ │ ├── Reader.java │ │ ├── Type.java │ │ └── package-info.java │ └── module-info.java └── test └── java └── com └── maxmind └── db ├── DecoderTest.java ├── MultiThreadedTest.java ├── NetworkTest.java ├── PointerTest.java ├── ReaderTest.java └── TestDecoder.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | pull_request: 8 | schedule: 9 | - cron: '0 6 * * 3' 10 | 11 | jobs: 12 | CodeQL-Build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | security-events: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | # We must fetch at least the immediate parents so that if this is 24 | # a pull request then we can checkout the head. 25 | fetch-depth: 2 26 | persist-credentials: false 27 | 28 | # If this run was triggered by a pull request event, then checkout 29 | # the head of the pull request instead of the merge commit. 30 | - run: git checkout HEAD^2 31 | if: ${{ github.event_name == 'pull_request' }} 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | # Override language selection by uncommenting this and choosing your languages 37 | # with: 38 | # languages: go, javascript, csharp, python, cpp, java 39 | 40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 41 | # If this step fails, then you should remove it and run the build manually (see below) 42 | - name: Autobuild 43 | uses: github/codeql-action/autobuild@v3 44 | 45 | # ℹ️ Command-line programs to run using the OS shell. 46 | # 📚 https://git.io/JvXDl 47 | 48 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 49 | # and modify them (or add more) to build your code if your project 50 | # uses a compiled language 51 | 52 | #- run: | 53 | # make bootstrap 54 | # make release 55 | 56 | - name: Perform CodeQL Analysis 57 | uses: github/codeql-action/analyze@v3 58 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [push, pull_request] 3 | permissions: {} 4 | jobs: 5 | test: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | distribution: ['zulu'] 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | version: [ 11, 17, 21, 22 ] 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: true 17 | persist-credentials: false 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: ${{ matrix.distribution }} 21 | java-version: ${{ matrix.version }} 22 | - run: mvn test -B 23 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | jobs: 10 | zizmor: 11 | name: zizmor latest via PyPI 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | # required for workflows in private repositories 16 | contents: read 17 | actions: read 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Install the latest version of uv 25 | uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # 6.1.0 26 | with: 27 | enable-cache: false 28 | 29 | - name: Run zizmor 30 | run: uvx zizmor@1.7.0 --format plain . 31 | env: 32 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *~ 3 | *.iml 4 | *.jar 5 | *.war 6 | *.ear 7 | *.sw? 8 | *.classpath 9 | .gh-pages 10 | .idea 11 | .pmd 12 | .project 13 | .settings 14 | bin 15 | doc 16 | hs_err*.log 17 | target 18 | /sample/GeoLite2-City.mmdb 19 | /sample/run.sh 20 | reports 21 | Test.java 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/test/resources/maxmind-db"] 2 | path = src/test/resources/maxmind-db 3 | url = https://github.com/maxmind/MaxMind-DB 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 3.2.0 (2025-05-28) 5 | ------------------ 6 | 7 | * First release using Central Portal instead of Legacy OSSRH. 8 | * Improve internal uses of types and other code cleanups. Pull requests 9 | by Philippe Marschall. GitHub #246, #247, #248, and #249. 10 | 11 | 3.1.1 (2024-09-19) 12 | ------------------ 13 | 14 | * When handling a deserialization exception, the decoder now avoids 15 | throwing a `NullPointerException` when one of the constructor arguments 16 | is `null`. Reported by Keith Massey. GitHub #164. 17 | 18 | 3.1.0 (2023-12-05) 19 | ------------------ 20 | 21 | * Reader supports iterating over the whole database or within a network. 22 | 23 | 3.0.0 (2022-12-12) 24 | ------------------ 25 | 26 | * Java 11 or greater is now required. 27 | * This library is now a Java module. 28 | 29 | 2.1.0 (2022-10-31) 30 | ------------------ 31 | 32 | * Messages for `DeserializationException` have been improved, and the cause 33 | is included, if any. Moreover, the message provides detail about the involved 34 | types, if the exception is caused by an `IllegalArgumentException`. 35 | 36 | 2.0.0 (2020-10-13) 37 | ------------------ 38 | 39 | * No changes since 2.0.0-rc2. 40 | 41 | 2.0.0-rc2 (2020-09-29) 42 | ---------------------- 43 | 44 | * Build using the `--release` command-line option so linking when using 45 | Java 8 works. 46 | 47 | 2.0.0-rc1 (2020-09-24) 48 | ---------------------- 49 | 50 | * Significant API changes. The `get()` and `getRecord()` methods now take a 51 | class parameter specifying the type of object to deserialize into. You 52 | can either deserialize into a `Map` or to model classes that use the 53 | `MaxMindDbConstructor` and `MaxMindDbParameter` annotations to identify 54 | the constructors and parameters to deserialize into. 55 | * `jackson-databind` is no longer a dependency. 56 | * The `Record` class is now named `DatabaseRecord`. This is to avoid a 57 | conflict with `java.lang.Record` in Java 14. 58 | 59 | 1.4.0 (2020-06-12) 60 | ------------------ 61 | 62 | * IMPORTANT: Java 8 is now required. If you need Java 7 support, please 63 | continue using 1.3.1 or earlier. 64 | * The decoder will now throw an `InvalidDatabaseException` on an invalid 65 | control byte in the data section rather than an 66 | `ArrayIndexOutOfBoundsException`. Reported by Edwin Delgado H. GitHub 67 | #68. 68 | * In order to improve performance when lookups are done from multiple 69 | threads, a use of `synchronized` has been removed. GitHub #65 & #69. 70 | * `jackson-databind` has been upgraded to 2.11.0. 71 | 72 | 1.3.1 (2020-03-03) 73 | ------------------ 74 | 75 | * Correctly decode strings that are between 157 and 288 bytes long. 1.3.0 76 | introduced a regression when decoding these due to using a signed `byte` 77 | as an unsigned value. Reported by Dongmin Yu. GitHub #181 in 78 | maxmind/GeoIP2-java. 79 | * Update `jackson-databind` dependency. 80 | 81 | 1.3.0 (2019-12-13) 82 | ------------------ 83 | 84 | * IMPORTANT: Java 7 is now required. If you need Java 6 support, please 85 | continue using 1.2.2 or earlier. 86 | * The method `getRecord` was added to `com.maxmind.db.Reader`. This method 87 | returns a `com.maxmind.db.Record` object that includes the data for the 88 | record as well as the network associated with the record. 89 | 90 | 1.2.2 (2017-02-22) 91 | ------------------ 92 | 93 | * Remove the version range. As of today, `jackson-databind` is no longer 94 | resolved correctly when a range is used. GitHub #28. 95 | 96 | 1.2.1 (2016-04-15) 97 | ------------------ 98 | 99 | * Specify a hard minimum dependency for `jackson-databind`. This API will not 100 | work with versions earlier than 2.7.0, and Maven's nearest-first resolution 101 | rule often pulled in older versions. 102 | 103 | 1.2.0 (2016-01-13) 104 | ------------------ 105 | 106 | * `JsonNode` containers returned by the `get(ip)` are now backed by 107 | unmodifiable collections. Any mutation done to them will fail with an 108 | `UnsupportedOperationException` exception. This allows safe caching of the 109 | nodes to be done without doing a deep copy of the cached data. Pull request 110 | by Viktor Szathmáry. GitHub #24. 111 | 112 | 1.1.0 (2016-01-04) 113 | ------------------ 114 | 115 | * The reader now supports pluggable caching of the decoded data. By default, 116 | no caching is performed. Please see the `README.md` file or the API docs 117 | for information on how to enable caching. Pull requests by Viktor Szathmáry. 118 | GitHub #21. 119 | * This release also includes several additional performance enhancements as 120 | well as code cleanup from Viktor Szathmáry. GitHub #18, #19, #20, #22,and 121 | #23. 122 | 123 | 1.0.1 (2015-12-17) 124 | ------------------ 125 | 126 | * Several optimizations have been made to reduce allocations when decoding a 127 | record. Pull requests by Viktor Szathmáry. GitHub #16 & #17. 128 | 129 | 1.0.0 (2014-09-29) 130 | ------------------ 131 | 132 | * First production release. 133 | 134 | 0.4.0 (2014-09-23) 135 | ------------------ 136 | 137 | * Made `com.maxmind.db.Metadata` public and added public getters for most 138 | of the interesting metadata. This is accessible through the `getMetadata()` 139 | method on a `Reader` object. 140 | 141 | 0.3.4 (2014-08-27) 142 | ------------------ 143 | 144 | * Previously the Reader would hold onto the underlying file and FileChannel, 145 | not closing them until the Reader was closed. This was unnecessary; they 146 | are now closed immediately after they are used. Fix by Andrew Snare; GitHub 147 | issue #7. 148 | * The Reader now discards the reference to the underlying buffer when 149 | `close()` is called. This is done to help ensure that the buffer is garbage 150 | collected sooner, which may mitigate file locking issues that some users 151 | have experienced on Windows when updating the database. Patch by Andrew 152 | Snare; GitHub issue #8. 153 | 154 | 0.3.3 (2014-06-02) 155 | ------------------ 156 | 157 | * A potential (small) resource leak when using this library with a thread 158 | pool was fixed. 159 | 160 | 0.3.2 (2014-04-02) 161 | ------------------ 162 | 163 | * Added tests and documentation for multi-threaded use. 164 | 165 | 0.3.1 (2013-11-05) 166 | ------------------ 167 | 168 | * An `InputStream` constructor was added to the `Reader` class. This reads the 169 | stream into memory as if it was using `FileMode.MEMORY`. Patch by Matthew 170 | Daniel. 171 | * The source code is now attached during packaging. Patch by Matthew Daniel. 172 | * The artifact ID was changed to `maxmind-db` in order to increase naming 173 | consistency. 174 | 175 | 0.3.0 (2013-10-17) 176 | ------------------ 177 | 178 | * IMPORTANT: The package name was changed to `com.maxmind.db`. The 179 | `MaxMindDbReader` class was renamed to `Reader`. 180 | * Improved error handling and test coverage. 181 | * Performance improvements. 182 | 183 | 0.2.0 (2013-07-08) 184 | ------------------ 185 | 186 | * The reader and database format now uses IEEE 754 doubles and floats. 187 | * FileMode.IN_MEMORY was renamed to FileMode.MEMORY. 188 | * Cache Type enum values array. 189 | 190 | 0.1.0 (2013-06-14) 191 | ------------------ 192 | 193 | * Initial release 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.dev.md: -------------------------------------------------------------------------------- 1 | See the [`README.dev.md` in `minfraud-api-java`](https://github.com/maxmind/minfraud-api-java/blob/main/README.dev.md). 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaxMind DB Reader # 2 | 3 | ## Description ## 4 | 5 | This is the Java API for reading MaxMind DB files. MaxMind DB is a binary file 6 | format that stores data indexed by IP address subnets (IPv4 or IPv6). 7 | 8 | ## Installation ## 9 | 10 | ### Maven ### 11 | 12 | We recommend installing this package with [Maven](https://maven.apache.org/). 13 | To do this, add the dependency to your pom.xml: 14 | 15 | ```xml 16 | 17 | com.maxmind.db 18 | maxmind-db 19 | 3.2.0 20 | 21 | ``` 22 | 23 | ### Gradle ### 24 | 25 | Add the following to your `build.gradle` file: 26 | 27 | ``` 28 | repositories { 29 | mavenCentral() 30 | } 31 | dependencies { 32 | compile 'com.maxmind.db:maxmind-db:3.2.0' 33 | } 34 | ``` 35 | 36 | ## Usage ## 37 | 38 | *Note:* For accessing MaxMind GeoIP2 databases, we generally recommend using 39 | the [GeoIP2 Java API](https://maxmind.github.io/GeoIP2-java/) rather than using 40 | this package directly. 41 | 42 | To use the API, you must first create a `Reader` object. The constructor for 43 | the reader object takes a `File` representing your MaxMind DB. Optionally you 44 | may pass a second parameter with a `FileMode` with a value of `MEMORY_MAP` or 45 | `MEMORY`. The default mode is `MEMORY_MAP`, which maps the file to virtual 46 | memory. This often provides performance comparable to loading the file into 47 | real memory with `MEMORY`. 48 | 49 | To look up an IP address, pass the address as an `InetAddress` to the `get` 50 | method on `Reader`, along with the class of the object you want to 51 | deserialize into. This method will create an instance of the class and 52 | populate it. See examples below. 53 | 54 | We recommend reusing the `Reader` object rather than creating a new one for 55 | each lookup. The creation of this object is relatively expensive as it must 56 | read in metadata for the file. 57 | 58 | ## Example ## 59 | 60 | ```java 61 | import com.maxmind.db.MaxMindDbConstructor; 62 | import com.maxmind.db.MaxMindDbParameter; 63 | import com.maxmind.db.Reader; 64 | import com.maxmind.db.DatabaseRecord; 65 | 66 | import java.io.File; 67 | import java.io.IOException; 68 | import java.net.InetAddress; 69 | 70 | public class Lookup { 71 | public static void main(String[] args) throws IOException { 72 | File database = new File("/path/to/database/GeoIP2-City.mmdb"); 73 | try (Reader reader = new Reader(database)) { 74 | InetAddress address = InetAddress.getByName("24.24.24.24"); 75 | 76 | // get() returns just the data for the associated record 77 | LookupResult result = reader.get(address, LookupResult.class); 78 | 79 | System.out.println(result.getCountry().getIsoCode()); 80 | 81 | // getRecord() returns a DatabaseRecord class that contains both 82 | // the data for the record and associated metadata. 83 | DatabaseRecord record 84 | = reader.getRecord(address, LookupResult.class); 85 | 86 | System.out.println(record.getData().getCountry().getIsoCode()); 87 | System.out.println(record.getNetwork()); 88 | } 89 | } 90 | 91 | public static class LookupResult { 92 | private final Country country; 93 | 94 | @MaxMindDbConstructor 95 | public LookupResult ( 96 | @MaxMindDbParameter(name="country") Country country 97 | ) { 98 | this.country = country; 99 | } 100 | 101 | public Country getCountry() { 102 | return this.country; 103 | } 104 | } 105 | 106 | public static class Country { 107 | private final String isoCode; 108 | 109 | @MaxMindDbConstructor 110 | public Country ( 111 | @MaxMindDbParameter(name="iso_code") String isoCode 112 | ) { 113 | this.isoCode = isoCode; 114 | } 115 | 116 | public String getIsoCode() { 117 | return this.isoCode; 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | You can also use the reader object to iterate over the database. 124 | The `reader.networks()` and `reader.networksWithin()` methods can 125 | be used for this purpose. 126 | 127 | ```java 128 | Reader reader = new Reader(file); 129 | Networks networks = reader.networks(Map.class); 130 | 131 | while(networks.hasNext()) { 132 | DatabaseRecord> iteration = networks.next(); 133 | 134 | // Get the data. 135 | Map data = iteration.getData(); 136 | 137 | // The IP Address 138 | InetAddress ipAddress = InetAddress.getByName(data.get("ip")); 139 | 140 | // ... 141 | } 142 | ``` 143 | 144 | 145 | ## Caching ## 146 | 147 | The database API supports pluggable caching (by default, no caching is 148 | performed). A simple implementation is provided by `com.maxmind.db.CHMCache`. 149 | Using this cache, lookup performance is significantly improved at the cost of 150 | a small (~2MB) memory overhead. 151 | 152 | Usage: 153 | 154 | ```java 155 | Reader reader = new Reader(database, new CHMCache()); 156 | ``` 157 | 158 | Please note that the cache will hold references to the objects created 159 | during the lookup. If you mutate the objects, the mutated objects will be 160 | returned from the cache on subsequent lookups. 161 | 162 | ## Multi-Threaded Use ## 163 | 164 | This API fully supports use in multi-threaded applications. In such 165 | applications, we suggest creating one `Reader` object and sharing that among 166 | threads. 167 | 168 | ## Common Problems ## 169 | 170 | ### File Lock on Windows ### 171 | 172 | By default, this API uses the `MEMORY_MAP` mode, which memory maps the file. 173 | On Windows, this may create an exclusive lock on the file that prevents it 174 | from being renamed or deleted. Due to the implementation of memory mapping in 175 | Java, this lock will not be released when the `DatabaseReader` is closed; it 176 | will only be released when the object and the `MappedByteBuffer` it uses are 177 | garbage collected. Older JVM versions may also not release the lock on exit. 178 | 179 | To work around this problem, use the `MEMORY` mode or try upgrading your JVM 180 | version. You may also call `System.gc()` after dereferencing the 181 | `DatabaseReader` object to encourage the JVM to garbage collect sooner. 182 | 183 | ### Packaging Database in a JAR ### 184 | 185 | If you are packaging the database file as a resource in a JAR file using 186 | Maven, you must 187 | [disable binary file filtering](https://maven.apache.org/plugins/maven-resources-plugin/examples/binaries-filtering.html). 188 | Failure to do so will result in `InvalidDatabaseException` exceptions being 189 | thrown when querying the database. 190 | 191 | ## Format ## 192 | 193 | The MaxMind DB format is an open format for quickly mapping IP addresses to 194 | records. The 195 | [specification](https://github.com/maxmind/MaxMind-DB/blob/main/MaxMind-DB-spec.md) 196 | is available, as is our 197 | [Perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) for the 198 | format. 199 | 200 | ## Bug Tracker ## 201 | 202 | Please report all issues with this code using the [GitHub issue 203 | tracker](https://github.com/maxmind/MaxMind-DB-Reader-java/issues). 204 | 205 | If you are having an issue with a MaxMind database or service that is not 206 | specific to this reader, please [contact MaxMind support](https://www.maxmind.com/en/support). 207 | 208 | ## Requirements ## 209 | 210 | This API requires Java 11 or greater. 211 | 212 | ## Contributing ## 213 | 214 | Patches and pull requests are encouraged. Please include unit tests whenever 215 | possible. 216 | 217 | ## Versioning ## 218 | 219 | The MaxMind DB Reader API uses [Semantic Versioning](https://semver.org/). 220 | 221 | ## Copyright and License ## 222 | 223 | This software is Copyright (c) 2014-2025 by MaxMind, Inc. 224 | 225 | This is free software, licensed under the Apache License, Version 2.0. 226 | -------------------------------------------------------------------------------- /checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 129 | 132 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | 181 | 182 | 184 | 186 | 187 | 188 | 189 | 191 | 192 | 193 | 194 | 196 | 197 | 198 | 199 | 201 | 202 | 203 | 204 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 216 | 217 | 218 | 219 | 221 | 222 | 223 | 224 | 226 | 227 | 228 | 229 | 231 | 232 | 233 | 234 | 236 | 237 | 238 | 239 | 241 | 242 | 243 | 244 | 246 | 248 | 250 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 282 | 283 | 284 | 287 | 288 | 289 | 290 | 296 | 297 | 298 | 299 | 303 | 304 | 305 | 306 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 322 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 352 | 353 | 354 | 355 | 356 | 358 | 359 | 360 | 361 | 362 | 363 | 366 | 367 | 368 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | -------------------------------------------------------------------------------- /dev-bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu -o pipefail 4 | 5 | changelog=$(cat CHANGELOG.md) 6 | 7 | regex=' 8 | ([0-9]+\.[0-9]+\.[0-9]+[a-zA-Z0-9\-]*) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\) 9 | -* 10 | 11 | ((.| 12 | )*) 13 | ' 14 | 15 | if [[ ! $changelog =~ $regex ]]; then 16 | echo "Could not find date line in change log!" 17 | exit 1 18 | fi 19 | 20 | version="${BASH_REMATCH[1]}" 21 | date="${BASH_REMATCH[2]}" 22 | notes="$(echo "${BASH_REMATCH[3]}" | sed -n -e '/^[0-9]\+\.[0-9]\+\.[0-9]\+/,$!p')" 23 | 24 | if [[ "$date" != $(date +"%Y-%m-%d") ]]; then 25 | echo "$date is not today!" 26 | exit 1 27 | fi 28 | 29 | tag="v$version" 30 | 31 | if [ -n "$(git status --porcelain)" ]; then 32 | echo ". is not clean." >&2 33 | exit 1 34 | fi 35 | 36 | if [ ! -d .gh-pages ]; then 37 | echo "Checking out gh-pages in .gh-pages" 38 | git clone -b gh-pages git@github.com:maxmind/MaxMind-DB-Reader-java.git .gh-pages 39 | pushd .gh-pages 40 | else 41 | echo "Updating .gh-pages" 42 | pushd .gh-pages 43 | git pull 44 | fi 45 | 46 | if [ -n "$(git status --porcelain)" ]; then 47 | echo ".gh-pages is not clean" >&2 48 | exit 1 49 | fi 50 | 51 | popd 52 | 53 | mvn versions:display-plugin-updates 54 | mvn versions:display-dependency-updates 55 | 56 | read -r -n 1 -p "Continue given above dependencies? (y/n) " should_continue 57 | 58 | if [ "$should_continue" != "y" ]; then 59 | echo "Aborting" 60 | exit 1 61 | fi 62 | 63 | mvn test 64 | 65 | read -r -n 1 -p "Continue given above tests? (y/n) " should_continue 66 | 67 | if [ "$should_continue" != "y" ]; then 68 | echo "Aborting" 69 | exit 1 70 | fi 71 | 72 | page=.gh-pages/index.md 73 | cat < $page 74 | --- 75 | layout: default 76 | title: MaxMind DB Java API 77 | language: java 78 | version: $tag 79 | --- 80 | 81 | EOF 82 | 83 | mvn versions:set -DnewVersion="$version" 84 | 85 | perl -pi -e "s/(?<=)[^<]*/$version/" README.md 86 | perl -pi -e "s/(?<=com\.maxmind\.db\:maxmind-db\:)\d+\.\d+\.\d+([\w\-]+)?/$version/" README.md 87 | 88 | cat README.md >> $page 89 | 90 | git diff 91 | 92 | read -r -n 1 -p "Commit changes? " should_commit 93 | if [ "$should_commit" != "y" ]; then 94 | echo "Aborting" 95 | exit 1 96 | fi 97 | git add README.md pom.xml 98 | git commit -m "Preparing for $version" 99 | 100 | mvn clean deploy 101 | 102 | rm -fr ".gh-pages/doc/$tag" 103 | cp -r target/reports/apidocs ".gh-pages/doc/$tag" 104 | rm .gh-pages/doc/latest 105 | ln -fs "$tag" .gh-pages/doc/latest 106 | 107 | pushd .gh-pages 108 | 109 | git add doc/ 110 | git commit -m "Updated for $tag" -a 111 | 112 | echo "Release notes for $version: 113 | 114 | $notes 115 | 116 | " 117 | read -r -n 1 -p "Push to origin? " should_push 118 | 119 | if [ "$should_push" != "y" ]; then 120 | echo "Aborting" 121 | exit 1 122 | fi 123 | 124 | git push 125 | 126 | popd 127 | 128 | git push 129 | 130 | gh release create --target "$(git branch --show-current)" -t "$version" -n "$notes" "$tag" 131 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.maxmind.db 5 | maxmind-db 6 | 3.2.0 7 | jar 8 | MaxMind DB Reader 9 | Reader for MaxMind DB 10 | http://dev.maxmind.com/ 11 | 12 | 13 | Apache License 2.0 14 | http://www.apache.org/licenses/LICENSE-2.0.html 15 | repo 16 | 17 | 18 | 19 | MaxMind, Inc. 20 | http://www.maxmind.com/ 21 | 22 | 23 | https://github.com/maxmind/MaxMind-DB-Reader-java 24 | scm:git:git://github.com:maxmind/MaxMind-DB-Reader-java.git 25 | scm:git:git@github.com:maxmind/MaxMind-DB-Reader-java.git 26 | HEAD 27 | 28 | 29 | https://github.com/maxmind/MaxMind-DB-Reader-java/issues 30 | GitHub 31 | 32 | 33 | 34 | oschwald 35 | Gregory J. Oschwald 36 | goschwald@maxmind.com 37 | 38 | 39 | 40 | 41 | org.junit.jupiter 42 | junit-jupiter 43 | 5.13.0 44 | test 45 | 46 | 47 | org.hamcrest 48 | java-hamcrest 49 | 2.0.0.0 50 | test 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-enforcer-plugin 58 | 3.5.0 59 | 60 | 61 | enforce-maven 62 | 63 | enforce 64 | 65 | 66 | 67 | 68 | 3.6.3 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-checkstyle-plugin 78 | 3.6.0 79 | 80 | true 81 | checkstyle.xml 82 | checkstyle-suppressions.xml 83 | warning 84 | 85 | 86 | 87 | com.puppycrawl.tools 88 | checkstyle 89 | 10.25.0 90 | 91 | 92 | 93 | 94 | test 95 | test 96 | 97 | check 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-gpg-plugin 105 | 3.2.7 106 | 107 | 108 | sign-artifacts 109 | verify 110 | 111 | sign 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-source-plugin 119 | 3.3.1 120 | 121 | 122 | attach-sources 123 | package 124 | 125 | jar-no-fork 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-javadoc-plugin 133 | 3.11.2 134 | 135 | -missing 136 | 137 | 138 | 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-compiler-plugin 148 | 3.14.0 149 | 150 | 11 151 | 11 152 | 11 153 | 154 | 155 | 156 | org.apache.maven.plugins 157 | maven-surefire-plugin 158 | 3.5.3 159 | 160 | 161 | org.codehaus.mojo 162 | versions-maven-plugin 163 | 2.18.0 164 | 165 | 166 | org.sonatype.central 167 | central-publishing-maven-plugin 168 | 0.7.0 169 | true 170 | 171 | central 172 | true 173 | 174 | 175 | 176 | 177 | 178 | UTF-8 179 | 180 | 181 | 182 | not-windows 183 | 189 | 190 | !Windows 191 | 192 | 193 | 194 | 195 | 200 | org.codehaus.mojo 201 | exec-maven-plugin 202 | 3.5.1 203 | 204 | 205 | initialize 206 | invoke build 207 | 208 | exec 209 | 210 | 211 | 212 | 213 | git 214 | submodule update --init --recursive 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | sonatype-nexus-staging 224 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /sample/Benchmark.java: -------------------------------------------------------------------------------- 1 | import java.io.File; 2 | import java.io.IOException; 3 | import java.net.InetAddress; 4 | import java.util.Map; 5 | import java.util.Random; 6 | 7 | import com.maxmind.db.CHMCache; 8 | import com.maxmind.db.InvalidDatabaseException; 9 | import com.maxmind.db.NoCache; 10 | import com.maxmind.db.NodeCache; 11 | import com.maxmind.db.Reader; 12 | import com.maxmind.db.Reader.FileMode; 13 | 14 | public class Benchmark { 15 | 16 | private final static int COUNT = 1000000; 17 | private final static int WARMUPS = 3; 18 | private final static int BENCHMARKS = 5; 19 | private final static boolean TRACE = false; 20 | 21 | public static void main(String[] args) throws IOException, InvalidDatabaseException { 22 | File file = new File(args.length > 0 ? args[0] : "GeoLite2-City.mmdb"); 23 | System.out.println("No caching"); 24 | loop("Warming up", file, WARMUPS, NoCache.getInstance()); 25 | loop("Benchmarking", file, BENCHMARKS, NoCache.getInstance()); 26 | 27 | System.out.println("With caching"); 28 | loop("Warming up", file, WARMUPS, new CHMCache()); 29 | loop("Benchmarking", file, BENCHMARKS, new CHMCache()); 30 | } 31 | 32 | private static void loop(String msg, File file, int loops, NodeCache cache) throws IOException { 33 | System.out.println(msg); 34 | for (int i = 0; i < loops; i++) { 35 | Reader r = new Reader(file, FileMode.MEMORY_MAPPED, cache); 36 | bench(r, COUNT, i); 37 | } 38 | System.out.println(); 39 | } 40 | 41 | private static void bench(Reader r, int count, int seed) throws IOException { 42 | Random random = new Random(seed); 43 | long startTime = System.nanoTime(); 44 | byte[] address = new byte[4]; 45 | for (int i = 0; i < count; i++) { 46 | random.nextBytes(address); 47 | InetAddress ip = InetAddress.getByAddress(address); 48 | Map t = r.get(ip, Map.class); 49 | if (TRACE) { 50 | if (i % 50000 == 0) { 51 | System.out.println(i + " " + ip); 52 | System.out.println(t); 53 | } 54 | } 55 | } 56 | long endTime = System.nanoTime(); 57 | 58 | long duration = endTime - startTime; 59 | long qps = count * 1000000000L / duration; 60 | System.out.println("Requests per second: " + qps); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/BufferHolder.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import com.maxmind.db.Reader.FileMode; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.RandomAccessFile; 9 | import java.nio.ByteBuffer; 10 | import java.nio.channels.FileChannel; 11 | import java.nio.channels.FileChannel.MapMode; 12 | 13 | final class BufferHolder { 14 | // DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety. 15 | private final ByteBuffer buffer; 16 | 17 | BufferHolder(File database, FileMode mode) throws IOException { 18 | try ( 19 | final RandomAccessFile file = new RandomAccessFile(database, "r"); 20 | final FileChannel channel = file.getChannel() 21 | ) { 22 | if (mode == FileMode.MEMORY) { 23 | final ByteBuffer buf = ByteBuffer.wrap(new byte[(int) channel.size()]); 24 | if (channel.read(buf) != buf.capacity()) { 25 | throw new IOException("Unable to read " 26 | + database.getName() 27 | + " into memory. Unexpected end of stream."); 28 | } 29 | this.buffer = buf.asReadOnlyBuffer(); 30 | } else { 31 | this.buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()).asReadOnlyBuffer(); 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Construct a ThreadBuffer from the provided URL. 38 | * 39 | * @param stream the source of my bytes. 40 | * @throws IOException if unable to read from your source. 41 | * @throws NullPointerException if you provide a NULL InputStream 42 | */ 43 | BufferHolder(InputStream stream) throws IOException { 44 | if (null == stream) { 45 | throw new NullPointerException("Unable to use a NULL InputStream"); 46 | } 47 | final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 48 | final byte[] bytes = new byte[16 * 1024]; 49 | int br; 50 | while (-1 != (br = stream.read(bytes))) { 51 | baos.write(bytes, 0, br); 52 | } 53 | this.buffer = ByteBuffer.wrap(baos.toByteArray()).asReadOnlyBuffer(); 54 | } 55 | 56 | /* 57 | * Returns a duplicate of the underlying ByteBuffer. The returned ByteBuffer 58 | * should not be shared between threads. 59 | */ 60 | ByteBuffer get() { 61 | // The Java API docs for buffer state: 62 | // 63 | // Buffers are not safe for use by multiple concurrent threads. If a buffer is to be 64 | // used by more than one thread then access to the buffer should be controlled by 65 | // appropriate synchronization. 66 | // 67 | // As such, you may think that this should be synchronized. This used to be the case, but 68 | // we had several complaints about the synchronization causing contention, e.g.: 69 | // 70 | // * https://github.com/maxmind/MaxMind-DB-Reader-java/issues/65 71 | // * https://github.com/maxmind/MaxMind-DB-Reader-java/pull/69 72 | // 73 | // Given that we are not modifying the original ByteBuffer in any way and all currently 74 | // known and most reasonably imaginable implementations of duplicate() only do read 75 | // operations on the original buffer object, the risk of not synchronizing this call seems 76 | // relatively low and worth taking for the performance benefit when lookups are being done 77 | // from many threads. 78 | return this.buffer.duplicate(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/CHMCache.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | /** 7 | * A simplistic cache using a {@link ConcurrentHashMap}. There's no eviction 8 | * policy, it just fills up until reaching the specified capacity (or 9 | * close enough at least, bounds check is not atomic :) 10 | */ 11 | public class CHMCache implements NodeCache { 12 | 13 | private static final int DEFAULT_CAPACITY = 4096; 14 | 15 | private final int capacity; 16 | private final ConcurrentHashMap cache; 17 | private boolean cacheFull = false; 18 | 19 | /** 20 | * Creates a new cache with the default capacity. 21 | */ 22 | public CHMCache() { 23 | this(DEFAULT_CAPACITY); 24 | } 25 | 26 | /** 27 | * Creates a new cache with the specified capacity. 28 | * 29 | * @param capacity 30 | * the maximum number of elements the cache can hold before 31 | * starting to evict them 32 | */ 33 | public CHMCache(int capacity) { 34 | this.capacity = capacity; 35 | this.cache = new ConcurrentHashMap<>(capacity); 36 | } 37 | 38 | @Override 39 | public DecodedValue get(CacheKey key, Loader loader) throws IOException { 40 | DecodedValue value = cache.get(key); 41 | if (value == null) { 42 | value = loader.load(key); 43 | if (!cacheFull) { 44 | if (cache.size() < capacity) { 45 | cache.put(key, value); 46 | } else { 47 | cacheFull = true; 48 | } 49 | } 50 | } 51 | return value; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/CacheKey.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | * {@code CacheKey} is used as a key in the data-section cache. It contains the offset of the 5 | * value in the database file, the class of the value, and the type 6 | * of the value. 7 | * 8 | * @param the type of value 9 | */ 10 | public final class CacheKey { 11 | private final int offset; 12 | private final Class cls; 13 | private final java.lang.reflect.Type type; 14 | 15 | CacheKey(int offset, Class cls, java.lang.reflect.Type type) { 16 | this.offset = offset; 17 | this.cls = cls; 18 | this.type = type; 19 | } 20 | 21 | int getOffset() { 22 | return this.offset; 23 | } 24 | 25 | Class getCls() { 26 | return this.cls; 27 | } 28 | 29 | java.lang.reflect.Type getType() { 30 | return this.type; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (o == null) { 36 | return false; 37 | } 38 | 39 | CacheKey other = (CacheKey) o; 40 | 41 | if (this.offset != other.offset) { 42 | return false; 43 | } 44 | 45 | if (this.cls == null) { 46 | if (other.cls != null) { 47 | return false; 48 | } 49 | } else if (!this.cls.equals(other.cls)) { 50 | return false; 51 | } 52 | 53 | if (this.type == null) { 54 | return other.type == null; 55 | } 56 | return this.type.equals(other.type); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | int result = offset; 62 | result = 31 * result + (cls == null ? 0 : cls.hashCode()); 63 | result = 31 * result + (type == null ? 0 : type.hashCode()); 64 | return result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/CachedConstructor.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.util.Map; 5 | 6 | final class CachedConstructor { 7 | private final Constructor constructor; 8 | private final Class[] parameterTypes; 9 | private final java.lang.reflect.Type[] parameterGenericTypes; 10 | private final Map parameterIndexes; 11 | 12 | CachedConstructor( 13 | Constructor constructor, 14 | Class[] parameterTypes, 15 | java.lang.reflect.Type[] parameterGenericTypes, 16 | Map parameterIndexes 17 | ) { 18 | this.constructor = constructor; 19 | this.parameterTypes = parameterTypes; 20 | this.parameterGenericTypes = parameterGenericTypes; 21 | this.parameterIndexes = parameterIndexes; 22 | } 23 | 24 | Constructor getConstructor() { 25 | return this.constructor; 26 | } 27 | 28 | Class[] getParameterTypes() { 29 | return this.parameterTypes; 30 | } 31 | 32 | java.lang.reflect.Type[] getParameterGenericTypes() { 33 | return this.parameterGenericTypes; 34 | } 35 | 36 | Map getParameterIndexes() { 37 | return this.parameterIndexes; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/ClosedDatabaseException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Signals that the underlying database has been closed. 7 | */ 8 | public class ClosedDatabaseException extends IOException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | ClosedDatabaseException() { 13 | super("The MaxMind DB has been closed."); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/ConstructorNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | * Signals that no annotated constructor was found. You should annotate a 5 | * constructor in the class with the MaxMindDbConstructor annotation. 6 | */ 7 | public class ConstructorNotFoundException extends RuntimeException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | ConstructorNotFoundException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/CtrlData.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | final class CtrlData { 4 | private final Type type; 5 | private final int ctrlByte; 6 | private final int offset; 7 | private final int size; 8 | 9 | CtrlData(Type type, int ctrlByte, int offset, int size) { 10 | this.type = type; 11 | this.ctrlByte = ctrlByte; 12 | this.offset = offset; 13 | this.size = size; 14 | } 15 | 16 | public Type getType() { 17 | return this.type; 18 | } 19 | 20 | public int getCtrlByte() { 21 | return this.ctrlByte; 22 | } 23 | 24 | public int getOffset() { 25 | return this.offset; 26 | } 27 | 28 | public int getSize() { 29 | return this.size; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/DatabaseRecord.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.net.InetAddress; 4 | 5 | /** 6 | * DatabaseRecord represents the data and metadata associated with a database 7 | * lookup. 8 | * 9 | * @param the type to deserialize the returned value to 10 | */ 11 | public final class DatabaseRecord { 12 | private final T data; 13 | private final Network network; 14 | 15 | /** 16 | * Create a new record. 17 | * 18 | * @param data the data for the record in the database. 19 | * @param ipAddress the IP address used in the lookup. 20 | * @param prefixLength the network prefix length associated with the record in the database. 21 | */ 22 | public DatabaseRecord(T data, InetAddress ipAddress, int prefixLength) { 23 | this.data = data; 24 | this.network = new Network(ipAddress, prefixLength); 25 | } 26 | 27 | /** 28 | * @return the data for the record in the database. The record will be 29 | * null if there was no data for the address in the 30 | * database. 31 | */ 32 | public T getData() { 33 | return data; 34 | } 35 | 36 | /** 37 | * @return the network associated with the record in the database. This is 38 | * the largest network where all of the IPs in the network have the same 39 | * data. 40 | */ 41 | public Network getNetwork() { 42 | return network; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/DecodedValue.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | * {@code DecodedValue} is a wrapper for the decoded value and the number of bytes used 5 | * to decode it. 6 | */ 7 | public final class DecodedValue { 8 | final Object value; 9 | 10 | DecodedValue(Object value) { 11 | this.value = value; 12 | } 13 | 14 | Object getValue() { 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Decoder.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | import java.lang.annotation.Annotation; 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.ParameterizedType; 8 | import java.math.BigInteger; 9 | import java.nio.ByteBuffer; 10 | import java.nio.charset.CharacterCodingException; 11 | import java.nio.charset.Charset; 12 | import java.nio.charset.CharsetDecoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | /* 21 | * Decoder for MaxMind DB data. 22 | * 23 | * This class CANNOT be shared between threads 24 | */ 25 | class Decoder { 26 | 27 | private static final Charset UTF_8 = StandardCharsets.UTF_8; 28 | 29 | private static final int[] POINTER_VALUE_OFFSETS = {0, 0, 1 << 11, (1 << 19) + (1 << 11), 0}; 30 | 31 | private final NodeCache cache; 32 | 33 | private final long pointerBase; 34 | 35 | private final CharsetDecoder utfDecoder = UTF_8.newDecoder(); 36 | 37 | private final ByteBuffer buffer; 38 | 39 | private final ConcurrentHashMap constructors; 40 | 41 | Decoder(NodeCache cache, ByteBuffer buffer, long pointerBase) { 42 | this( 43 | cache, 44 | buffer, 45 | pointerBase, 46 | new ConcurrentHashMap<>() 47 | ); 48 | } 49 | 50 | Decoder( 51 | NodeCache cache, 52 | ByteBuffer buffer, 53 | long pointerBase, 54 | ConcurrentHashMap constructors 55 | ) { 56 | this.cache = cache; 57 | this.pointerBase = pointerBase; 58 | this.buffer = buffer; 59 | this.constructors = constructors; 60 | } 61 | 62 | private final NodeCache.Loader cacheLoader = this::decode; 63 | 64 | T decode(int offset, Class cls) throws IOException { 65 | if (offset >= this.buffer.capacity()) { 66 | throw new InvalidDatabaseException( 67 | "The MaxMind DB file's data section contains bad data: " 68 | + "pointer larger than the database."); 69 | } 70 | 71 | this.buffer.position(offset); 72 | return cls.cast(decode(cls, null).getValue()); 73 | } 74 | 75 | private DecodedValue decode(CacheKey key) throws IOException { 76 | int offset = key.getOffset(); 77 | if (offset >= this.buffer.capacity()) { 78 | throw new InvalidDatabaseException( 79 | "The MaxMind DB file's data section contains bad data: " 80 | + "pointer larger than the database."); 81 | } 82 | 83 | this.buffer.position(offset); 84 | Class cls = key.getCls(); 85 | return decode(cls, key.getType()); 86 | } 87 | 88 | private DecodedValue decode(Class cls, java.lang.reflect.Type genericType) 89 | throws IOException { 90 | int ctrlByte = 0xFF & this.buffer.get(); 91 | 92 | Type type = Type.fromControlByte(ctrlByte); 93 | 94 | // Pointers are a special case, we don't read the next 'size' bytes, we 95 | // use the size to determine the length of the pointer and then follow 96 | // it. 97 | if (type.equals(Type.POINTER)) { 98 | int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1; 99 | int base = pointerSize == 4 ? (byte) 0 : (byte) (ctrlByte & 0x7); 100 | int packed = this.decodeInteger(base, pointerSize); 101 | long pointer = packed + this.pointerBase + POINTER_VALUE_OFFSETS[pointerSize]; 102 | 103 | return decodePointer(pointer, cls, genericType); 104 | } 105 | 106 | if (type.equals(Type.EXTENDED)) { 107 | int nextByte = this.buffer.get(); 108 | 109 | int typeNum = nextByte + 7; 110 | 111 | if (typeNum < 8) { 112 | throw new InvalidDatabaseException( 113 | "Something went horribly wrong in the decoder. An extended type " 114 | + "resolved to a type number < 8 (" + typeNum 115 | + ")"); 116 | } 117 | 118 | type = Type.get(typeNum); 119 | } 120 | 121 | int size = ctrlByte & 0x1f; 122 | if (size >= 29) { 123 | switch (size) { 124 | case 29: 125 | size = 29 + (0xFF & buffer.get()); 126 | break; 127 | case 30: 128 | size = 285 + decodeInteger(2); 129 | break; 130 | default: 131 | size = 65821 + decodeInteger(3); 132 | } 133 | } 134 | 135 | return new DecodedValue(this.decodeByType(type, size, cls, genericType)); 136 | } 137 | 138 | DecodedValue decodePointer(long pointer, Class cls, java.lang.reflect.Type genericType) 139 | throws IOException { 140 | int targetOffset = (int) pointer; 141 | int position = buffer.position(); 142 | 143 | CacheKey key = new CacheKey(targetOffset, cls, genericType); 144 | DecodedValue o = cache.get(key, cacheLoader); 145 | 146 | buffer.position(position); 147 | return o; 148 | } 149 | 150 | private Object decodeByType( 151 | Type type, 152 | int size, 153 | Class cls, 154 | java.lang.reflect.Type genericType 155 | ) throws IOException { 156 | switch (type) { 157 | case MAP: 158 | return this.decodeMap(size, cls, genericType); 159 | case ARRAY: 160 | Class elementClass = Object.class; 161 | if (genericType instanceof ParameterizedType) { 162 | ParameterizedType ptype = (ParameterizedType) genericType; 163 | java.lang.reflect.Type[] actualTypes = ptype.getActualTypeArguments(); 164 | if (actualTypes.length == 1) { 165 | elementClass = (Class) actualTypes[0]; 166 | } 167 | } 168 | return this.decodeArray(size, cls, elementClass); 169 | case BOOLEAN: 170 | return Decoder.decodeBoolean(size); 171 | case UTF8_STRING: 172 | return this.decodeString(size); 173 | case DOUBLE: 174 | return this.decodeDouble(size); 175 | case FLOAT: 176 | return this.decodeFloat(size); 177 | case BYTES: 178 | return this.getByteArray(size); 179 | case UINT16: 180 | return this.decodeUint16(size); 181 | case UINT32: 182 | return this.decodeUint32(size); 183 | case INT32: 184 | return this.decodeInt32(size); 185 | case UINT64: 186 | case UINT128: 187 | return this.decodeBigInteger(size); 188 | default: 189 | throw new InvalidDatabaseException( 190 | "Unknown or unexpected type: " + type.name()); 191 | } 192 | } 193 | 194 | private String decodeString(int size) throws CharacterCodingException { 195 | int oldLimit = buffer.limit(); 196 | buffer.limit(buffer.position() + size); 197 | String s = utfDecoder.decode(buffer).toString(); 198 | buffer.limit(oldLimit); 199 | return s; 200 | } 201 | 202 | private int decodeUint16(int size) { 203 | return this.decodeInteger(size); 204 | } 205 | 206 | private int decodeInt32(int size) { 207 | return this.decodeInteger(size); 208 | } 209 | 210 | private long decodeLong(int size) { 211 | long integer = 0; 212 | for (int i = 0; i < size; i++) { 213 | integer = (integer << 8) | (this.buffer.get() & 0xFF); 214 | } 215 | return integer; 216 | } 217 | 218 | private long decodeUint32(int size) { 219 | return this.decodeLong(size); 220 | } 221 | 222 | private int decodeInteger(int size) { 223 | return this.decodeInteger(0, size); 224 | } 225 | 226 | private int decodeInteger(int base, int size) { 227 | return Decoder.decodeInteger(this.buffer, base, size); 228 | } 229 | 230 | static int decodeInteger(ByteBuffer buffer, int base, int size) { 231 | int integer = base; 232 | for (int i = 0; i < size; i++) { 233 | integer = (integer << 8) | (buffer.get() & 0xFF); 234 | } 235 | return integer; 236 | } 237 | 238 | private BigInteger decodeBigInteger(int size) { 239 | byte[] bytes = this.getByteArray(size); 240 | return new BigInteger(1, bytes); 241 | } 242 | 243 | private double decodeDouble(int size) throws InvalidDatabaseException { 244 | if (size != 8) { 245 | throw new InvalidDatabaseException( 246 | "The MaxMind DB file's data section contains bad data: " 247 | + "invalid size of double."); 248 | } 249 | return this.buffer.getDouble(); 250 | } 251 | 252 | private float decodeFloat(int size) throws InvalidDatabaseException { 253 | if (size != 4) { 254 | throw new InvalidDatabaseException( 255 | "The MaxMind DB file's data section contains bad data: " 256 | + "invalid size of float."); 257 | } 258 | return this.buffer.getFloat(); 259 | } 260 | 261 | private static boolean decodeBoolean(int size) 262 | throws InvalidDatabaseException { 263 | switch (size) { 264 | case 0: 265 | return false; 266 | case 1: 267 | return true; 268 | default: 269 | throw new InvalidDatabaseException( 270 | "The MaxMind DB file's data section contains bad data: " 271 | + "invalid size of boolean."); 272 | } 273 | } 274 | 275 | private List decodeArray( 276 | int size, 277 | Class cls, 278 | Class elementClass 279 | ) throws IOException { 280 | if (!List.class.isAssignableFrom(cls) && !cls.equals(Object.class)) { 281 | throw new DeserializationException("Unable to deserialize an array into an " + cls); 282 | } 283 | 284 | List array; 285 | if (cls.equals(List.class) || cls.equals(Object.class)) { 286 | array = new ArrayList<>(size); 287 | } else { 288 | Constructor constructor; 289 | try { 290 | constructor = cls.getConstructor(Integer.TYPE); 291 | } catch (NoSuchMethodException e) { 292 | throw new DeserializationException( 293 | "No constructor found for the List: " + e.getMessage(), e); 294 | } 295 | Object[] parameters = {size}; 296 | try { 297 | @SuppressWarnings("unchecked") 298 | List array2 = (List) constructor.newInstance(parameters); 299 | array = array2; 300 | } catch (InstantiationException 301 | | IllegalAccessException 302 | | InvocationTargetException e) { 303 | throw new DeserializationException("Error creating list: " + e.getMessage(), e); 304 | } 305 | } 306 | 307 | for (int i = 0; i < size; i++) { 308 | Object e = this.decode(elementClass, null).getValue(); 309 | array.add(elementClass.cast(e)); 310 | } 311 | 312 | return array; 313 | } 314 | 315 | private Object decodeMap( 316 | int size, 317 | Class cls, 318 | java.lang.reflect.Type genericType 319 | ) throws IOException { 320 | if (Map.class.isAssignableFrom(cls) || cls.equals(Object.class)) { 321 | Class valueClass = Object.class; 322 | if (genericType instanceof ParameterizedType) { 323 | ParameterizedType ptype = (ParameterizedType) genericType; 324 | java.lang.reflect.Type[] actualTypes = ptype.getActualTypeArguments(); 325 | if (actualTypes.length == 2) { 326 | Class keyClass = (Class) actualTypes[0]; 327 | if (!keyClass.equals(String.class)) { 328 | throw new DeserializationException("Map keys must be strings."); 329 | } 330 | 331 | valueClass = (Class) actualTypes[1]; 332 | } 333 | } 334 | return this.decodeMapIntoMap(cls, size, valueClass); 335 | } 336 | 337 | return this.decodeMapIntoObject(size, cls); 338 | } 339 | 340 | private Map decodeMapIntoMap( 341 | Class cls, 342 | int size, 343 | Class valueClass 344 | ) throws IOException { 345 | Map map; 346 | if (cls.equals(Map.class) || cls.equals(Object.class)) { 347 | map = new HashMap<>(size); 348 | } else { 349 | Constructor constructor; 350 | try { 351 | constructor = cls.getConstructor(Integer.TYPE); 352 | } catch (NoSuchMethodException e) { 353 | throw new DeserializationException( 354 | "No constructor found for the Map: " + e.getMessage(), e); 355 | } 356 | Object[] parameters = {size}; 357 | try { 358 | @SuppressWarnings("unchecked") 359 | Map map2 = (Map) constructor.newInstance(parameters); 360 | map = map2; 361 | } catch (InstantiationException 362 | | IllegalAccessException 363 | | InvocationTargetException e) { 364 | throw new DeserializationException("Error creating map: " + e.getMessage(), e); 365 | } 366 | } 367 | 368 | for (int i = 0; i < size; i++) { 369 | String key = (String) this.decode(String.class, null).getValue(); 370 | Object value = this.decode(valueClass, null).getValue(); 371 | try { 372 | map.put(key, valueClass.cast(value)); 373 | } catch (ClassCastException e) { 374 | throw new DeserializationException( 375 | "Error creating map entry for '" + key + "': " + e.getMessage(), e); 376 | } 377 | } 378 | 379 | return map; 380 | } 381 | 382 | private Object decodeMapIntoObject(int size, Class cls) 383 | throws IOException { 384 | CachedConstructor cachedConstructor = this.constructors.get(cls); 385 | Constructor constructor; 386 | Class[] parameterTypes; 387 | java.lang.reflect.Type[] parameterGenericTypes; 388 | Map parameterIndexes; 389 | if (cachedConstructor == null) { 390 | constructor = findConstructor(cls); 391 | 392 | parameterTypes = constructor.getParameterTypes(); 393 | 394 | parameterGenericTypes = constructor.getGenericParameterTypes(); 395 | 396 | parameterIndexes = new HashMap<>(); 397 | Annotation[][] annotations = constructor.getParameterAnnotations(); 398 | for (int i = 0; i < constructor.getParameterCount(); i++) { 399 | String parameterName = getParameterName(cls, i, annotations[i]); 400 | parameterIndexes.put(parameterName, i); 401 | } 402 | 403 | this.constructors.put( 404 | cls, 405 | new CachedConstructor( 406 | constructor, 407 | parameterTypes, 408 | parameterGenericTypes, 409 | parameterIndexes 410 | ) 411 | ); 412 | } else { 413 | constructor = cachedConstructor.getConstructor(); 414 | parameterTypes = cachedConstructor.getParameterTypes(); 415 | parameterGenericTypes = cachedConstructor.getParameterGenericTypes(); 416 | parameterIndexes = cachedConstructor.getParameterIndexes(); 417 | } 418 | 419 | Object[] parameters = new Object[parameterTypes.length]; 420 | for (int i = 0; i < size; i++) { 421 | String key = (String) this.decode(String.class, null).getValue(); 422 | 423 | Integer parameterIndex = parameterIndexes.get(key); 424 | if (parameterIndex == null) { 425 | int offset = this.nextValueOffset(this.buffer.position(), 1); 426 | this.buffer.position(offset); 427 | continue; 428 | } 429 | 430 | parameters[parameterIndex] = this.decode( 431 | parameterTypes[parameterIndex], 432 | parameterGenericTypes[parameterIndex] 433 | ).getValue(); 434 | } 435 | 436 | try { 437 | return constructor.newInstance(parameters); 438 | } catch (InstantiationException 439 | | IllegalAccessException 440 | | InvocationTargetException e) { 441 | throw new DeserializationException("Error creating object: " + e.getMessage(), e); 442 | } catch (IllegalArgumentException e) { 443 | StringBuilder sbErrors = new StringBuilder(); 444 | for (String key : parameterIndexes.keySet()) { 445 | int index = parameterIndexes.get(key); 446 | if (parameters[index] != null 447 | && !parameters[index].getClass().isAssignableFrom(parameterTypes[index])) { 448 | sbErrors.append(" argument type mismatch in " + key + " MMDB Type: " 449 | + parameters[index].getClass().getCanonicalName() 450 | + " Java Type: " + parameterTypes[index].getCanonicalName()); 451 | } 452 | } 453 | throw new DeserializationException( 454 | "Error creating object of type: " + cls.getSimpleName() + " - " + sbErrors, e); 455 | } 456 | } 457 | 458 | private static Constructor findConstructor(Class cls) 459 | throws ConstructorNotFoundException { 460 | Constructor[] constructors = cls.getConstructors(); 461 | for (Constructor constructor : constructors) { 462 | if (constructor.getAnnotation(MaxMindDbConstructor.class) == null) { 463 | continue; 464 | } 465 | @SuppressWarnings("unchecked") 466 | Constructor constructor2 = (Constructor) constructor; 467 | return constructor2; 468 | } 469 | 470 | throw new ConstructorNotFoundException("No constructor on class " + cls.getName() 471 | + " with the MaxMindDbConstructor annotation was found."); 472 | } 473 | 474 | private static String getParameterName( 475 | Class cls, 476 | int index, 477 | Annotation[] annotations 478 | ) throws ParameterNotFoundException { 479 | for (Annotation annotation : annotations) { 480 | if (!annotation.annotationType().equals(MaxMindDbParameter.class)) { 481 | continue; 482 | } 483 | MaxMindDbParameter paramAnnotation = (MaxMindDbParameter) annotation; 484 | return paramAnnotation.name(); 485 | } 486 | throw new ParameterNotFoundException( 487 | "Constructor parameter " + index + " on class " + cls.getName() 488 | + " is not annotated with MaxMindDbParameter."); 489 | } 490 | 491 | private int nextValueOffset(int offset, int numberToSkip) 492 | throws InvalidDatabaseException { 493 | if (numberToSkip == 0) { 494 | return offset; 495 | } 496 | 497 | CtrlData ctrlData = this.getCtrlData(offset); 498 | int ctrlByte = ctrlData.getCtrlByte(); 499 | int size = ctrlData.getSize(); 500 | offset = ctrlData.getOffset(); 501 | 502 | Type type = ctrlData.getType(); 503 | switch (type) { 504 | case POINTER: 505 | int pointerSize = ((ctrlByte >>> 3) & 0x3) + 1; 506 | offset += pointerSize; 507 | break; 508 | case MAP: 509 | numberToSkip += 2 * size; 510 | break; 511 | case ARRAY: 512 | numberToSkip += size; 513 | break; 514 | case BOOLEAN: 515 | break; 516 | default: 517 | offset += size; 518 | break; 519 | } 520 | 521 | return nextValueOffset(offset, numberToSkip - 1); 522 | } 523 | 524 | private CtrlData getCtrlData(int offset) 525 | throws InvalidDatabaseException { 526 | if (offset >= this.buffer.capacity()) { 527 | throw new InvalidDatabaseException( 528 | "The MaxMind DB file's data section contains bad data: " 529 | + "pointer larger than the database."); 530 | } 531 | 532 | this.buffer.position(offset); 533 | int ctrlByte = 0xFF & this.buffer.get(); 534 | offset++; 535 | 536 | Type type = Type.fromControlByte(ctrlByte); 537 | 538 | if (type.equals(Type.EXTENDED)) { 539 | int nextByte = this.buffer.get(); 540 | 541 | int typeNum = nextByte + 7; 542 | 543 | if (typeNum < 8) { 544 | throw new InvalidDatabaseException( 545 | "Something went horribly wrong in the decoder. An extended type " 546 | + "resolved to a type number < 8 (" + typeNum 547 | + ")"); 548 | } 549 | 550 | type = Type.get(typeNum); 551 | offset++; 552 | } 553 | 554 | int size = ctrlByte & 0x1f; 555 | if (size >= 29) { 556 | int bytesToRead = size - 28; 557 | offset += bytesToRead; 558 | switch (size) { 559 | case 29: 560 | size = 29 + (0xFF & buffer.get()); 561 | break; 562 | case 30: 563 | size = 285 + decodeInteger(2); 564 | break; 565 | default: 566 | size = 65821 + decodeInteger(3); 567 | } 568 | } 569 | 570 | return new CtrlData(type, ctrlByte, offset, size); 571 | } 572 | 573 | private byte[] getByteArray(int length) { 574 | return Decoder.getByteArray(this.buffer, length); 575 | } 576 | 577 | private static byte[] getByteArray(ByteBuffer buffer, int length) { 578 | byte[] bytes = new byte[length]; 579 | buffer.get(bytes); 580 | return bytes; 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/DeserializationException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | * Signals that the value could not be deserialized into the type. 5 | */ 6 | public class DeserializationException extends RuntimeException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | DeserializationException() { 10 | super("Database value cannot be deserialized into the type."); 11 | } 12 | 13 | DeserializationException(String message) { 14 | super(message); 15 | } 16 | 17 | DeserializationException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/InvalidDatabaseException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Signals that there was an issue reading from the MaxMind DB file due to 7 | * unexpected data formatting. This generally suggests that the database is 8 | * corrupt or otherwise not in a format supported by the reader. 9 | */ 10 | public class InvalidDatabaseException extends IOException { 11 | 12 | private static final long serialVersionUID = 6161763462364823003L; 13 | 14 | /** 15 | * @param message A message describing the reason why the exception was thrown. 16 | */ 17 | public InvalidDatabaseException(String message) { 18 | super(message); 19 | } 20 | 21 | /** 22 | * @param message A message describing the reason why the exception was thrown. 23 | * @param cause The cause of the exception. 24 | */ 25 | public InvalidDatabaseException(String message, Throwable cause) { 26 | super(message, cause); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/InvalidNetworkException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.net.InetAddress; 4 | 5 | /** 6 | * This is a custom exception that is thrown when the user attempts to use an 7 | * IPv6 address in an IPv4-only database. 8 | */ 9 | public class InvalidNetworkException extends Exception { 10 | /** 11 | * @param ip the IP address that was used 12 | */ 13 | public InvalidNetworkException(InetAddress ip) { 14 | super("you attempted to use an IPv6 network in an IPv4-only database: " + ip.toString()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/MaxMindDbConstructor.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | /** 7 | * {@code MaxMindDbConstructor} is an annotation that can be used to mark a constructor 8 | * that should be used to create an instance of a class when decoding a MaxMind 9 | * DB file. 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface MaxMindDbConstructor { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/MaxMindDbParameter.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | /** 7 | * Interface for a MaxMind DB parameter. This is used to mark a parameter that 8 | * should be used to create an instance of a class when decoding a MaxMind DB 9 | * file. 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface MaxMindDbParameter { 13 | /** 14 | * @return the name of the parameter in the MaxMind DB file 15 | */ 16 | String name(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Metadata.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Date; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * {@code Metadata} holds data associated with the database itself. 10 | */ 11 | public final class Metadata { 12 | private final int binaryFormatMajorVersion; 13 | private final int binaryFormatMinorVersion; 14 | 15 | private final BigInteger buildEpoch; 16 | 17 | private final String databaseType; 18 | 19 | private final Map description; 20 | 21 | private final int ipVersion; 22 | 23 | private final List languages; 24 | 25 | private final int nodeByteSize; 26 | 27 | private final int nodeCount; 28 | 29 | private final int recordSize; 30 | 31 | private final int searchTreeSize; 32 | 33 | /** 34 | * Constructs a {@code Metadata} object. 35 | * 36 | * @param binaryFormatMajorVersion The major version number for the database's 37 | * binary format. 38 | * @param binaryFormatMinorVersion The minor version number for the database's 39 | * binary format. 40 | * @param buildEpoch The date of the database build. 41 | * @param databaseType A string that indicates the structure of each 42 | * data record associated with an IP address. 43 | * The actual definition of these structures is 44 | * left up to the database creator. 45 | * @param languages List of languages supported by the database. 46 | * @param description Map from language code to description in that 47 | * language. 48 | * @param ipVersion Whether the database contains IPv4 or IPv6 49 | * address data. The only possible values are 4 50 | * and 6. 51 | * @param nodeCount The number of nodes in the search tree. 52 | * @param recordSize The number of bits in a record in the search 53 | * tree. Note that each node consists of two 54 | * records. 55 | */ 56 | @MaxMindDbConstructor 57 | public Metadata( 58 | @MaxMindDbParameter(name = "binary_format_major_version") int binaryFormatMajorVersion, 59 | @MaxMindDbParameter(name = "binary_format_minor_version") int binaryFormatMinorVersion, 60 | @MaxMindDbParameter(name = "build_epoch") BigInteger buildEpoch, 61 | @MaxMindDbParameter(name = "database_type") String databaseType, 62 | @MaxMindDbParameter(name = "languages") List languages, 63 | @MaxMindDbParameter(name = "description") Map description, 64 | @MaxMindDbParameter(name = "ip_version") int ipVersion, 65 | @MaxMindDbParameter(name = "node_count") long nodeCount, 66 | @MaxMindDbParameter(name = "record_size") int recordSize) { 67 | this.binaryFormatMajorVersion = binaryFormatMajorVersion; 68 | this.binaryFormatMinorVersion = binaryFormatMinorVersion; 69 | this.buildEpoch = buildEpoch; 70 | this.databaseType = databaseType; 71 | this.languages = languages; 72 | this.description = description; 73 | this.ipVersion = ipVersion; 74 | this.nodeCount = (int) nodeCount; 75 | this.recordSize = recordSize; 76 | 77 | this.nodeByteSize = this.recordSize / 4; 78 | this.searchTreeSize = this.nodeCount * this.nodeByteSize; 79 | } 80 | 81 | /** 82 | * @return the major version number for the database's binary format. 83 | */ 84 | public int getBinaryFormatMajorVersion() { 85 | return this.binaryFormatMajorVersion; 86 | } 87 | 88 | /** 89 | * @return the minor version number for the database's binary format. 90 | */ 91 | public int getBinaryFormatMinorVersion() { 92 | return this.binaryFormatMinorVersion; 93 | } 94 | 95 | /** 96 | * @return the date of the database build. 97 | */ 98 | public Date getBuildDate() { 99 | return new Date(this.buildEpoch.longValue() * 1000); 100 | } 101 | 102 | /** 103 | * @return a string that indicates the structure of each data record 104 | * associated with an IP address. The actual definition of these 105 | * structures is left up to the database creator. 106 | */ 107 | public String getDatabaseType() { 108 | return this.databaseType; 109 | } 110 | 111 | /** 112 | * @return map from language code to description in that language. 113 | */ 114 | public Map getDescription() { 115 | return this.description; 116 | } 117 | 118 | /** 119 | * @return whether the database contains IPv4 or IPv6 address data. The only 120 | * possible values are 4 and 6. 121 | */ 122 | public int getIpVersion() { 123 | return this.ipVersion; 124 | } 125 | 126 | /** 127 | * @return list of languages supported by the database. 128 | */ 129 | public List getLanguages() { 130 | return this.languages; 131 | } 132 | 133 | /** 134 | * @return the nodeByteSize 135 | */ 136 | int getNodeByteSize() { 137 | return this.nodeByteSize; 138 | } 139 | 140 | /** 141 | * @return the number of nodes in the search tree. 142 | */ 143 | int getNodeCount() { 144 | return this.nodeCount; 145 | } 146 | 147 | /** 148 | * @return the number of bits in a record in the search tree. Note that each 149 | * node consists of two records. 150 | */ 151 | int getRecordSize() { 152 | return this.recordSize; 153 | } 154 | 155 | /** 156 | * @return the searchTreeSize 157 | */ 158 | int getSearchTreeSize() { 159 | return this.searchTreeSize; 160 | } 161 | 162 | /* 163 | * (non-Javadoc) 164 | * 165 | * @see java.lang.Object#toString() 166 | */ 167 | @Override 168 | public String toString() { 169 | return "Metadata [binaryFormatMajorVersion=" 170 | + this.binaryFormatMajorVersion + ", binaryFormatMinorVersion=" 171 | + this.binaryFormatMinorVersion + ", buildEpoch=" 172 | + this.buildEpoch + ", databaseType=" + this.databaseType 173 | + ", description=" + this.description + ", ipVersion=" 174 | + this.ipVersion + ", nodeCount=" + this.nodeCount 175 | + ", recordSize=" + this.recordSize + "]"; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Network.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.net.InetAddress; 4 | import java.net.UnknownHostException; 5 | 6 | /** 7 | * Network represents an IP network. 8 | */ 9 | public final class Network { 10 | private final InetAddress ipAddress; 11 | private final int prefixLength; 12 | private InetAddress networkAddress = null; 13 | 14 | /** 15 | * Construct a Network 16 | * 17 | * @param ipAddress An IP address in the network. This does not have to be 18 | * the first address in the network. 19 | * @param prefixLength The prefix length for the network. 20 | */ 21 | public Network(InetAddress ipAddress, int prefixLength) { 22 | this.ipAddress = ipAddress; 23 | this.prefixLength = prefixLength; 24 | } 25 | 26 | /** 27 | * @return The first address in the network. 28 | */ 29 | public InetAddress getNetworkAddress() { 30 | if (networkAddress != null) { 31 | return networkAddress; 32 | } 33 | byte[] ipBytes = ipAddress.getAddress(); 34 | byte[] networkBytes = new byte[ipBytes.length]; 35 | int curPrefix = prefixLength; 36 | for (int i = 0; i < ipBytes.length && curPrefix > 0; i++) { 37 | byte b = ipBytes[i]; 38 | if (curPrefix < 8) { 39 | int shiftN = 8 - curPrefix; 40 | b = (byte) ((b >> shiftN) << shiftN); 41 | } 42 | networkBytes[i] = b; 43 | curPrefix -= 8; 44 | } 45 | 46 | try { 47 | networkAddress = InetAddress.getByAddress(networkBytes); 48 | } catch (UnknownHostException e) { 49 | throw new RuntimeException( 50 | "Illegal network address byte length of " + networkBytes.length); 51 | } 52 | return networkAddress; 53 | } 54 | 55 | /** 56 | * @return The prefix length is the number of leading 1 bits in the subnet 57 | * mask. Sometimes also known as netmask length. 58 | */ 59 | public int getPrefixLength() { 60 | return prefixLength; 61 | } 62 | 63 | /** 64 | * @return A string representation of the network in CIDR notation, e.g., 65 | * 1.2.3.0/24 or 2001::/8. 66 | */ 67 | public String toString() { 68 | return getNetworkAddress().getHostAddress() + "/" + prefixLength; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Networks.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | import java.net.Inet4Address; 5 | import java.net.InetAddress; 6 | import java.nio.ByteBuffer; 7 | import java.util.Arrays; 8 | import java.util.Iterator; 9 | import java.util.Stack; 10 | 11 | /** 12 | * Instances of this class provide an iterator over the networks in a database. 13 | * The iterator will return a {@link DatabaseRecord} for each network. 14 | * 15 | * @param The type of data returned by the iterator. 16 | */ 17 | public final class Networks implements Iterator> { 18 | private final Reader reader; 19 | private final Stack nodes; 20 | private NetworkNode lastNode; 21 | private final boolean includeAliasedNetworks; 22 | private final ByteBuffer buffer; /* Stores the buffer for Next() calls */ 23 | private final Class typeParameterClass; 24 | 25 | /** 26 | * Constructs a Networks instance. 27 | * 28 | * @param reader The reader object. 29 | * @param includeAliasedNetworks The boolean to include aliased networks. 30 | * @param typeParameterClass The type of data returned by the iterator. 31 | * @throws ClosedDatabaseException Exception for a closed database. 32 | */ 33 | Networks(Reader reader, boolean includeAliasedNetworks, Class typeParameterClass) 34 | throws ClosedDatabaseException { 35 | this(reader, includeAliasedNetworks, new NetworkNode[]{}, typeParameterClass); 36 | } 37 | 38 | /** 39 | * Constructs a Networks instance. 40 | * 41 | * @param reader The reader object. 42 | * @param includeAliasedNetworks The boolean to include aliased networks. 43 | * @param nodes The initial nodes array to start Networks iterator with. 44 | * @param typeParameterClass The type of data returned by the iterator. 45 | * @throws ClosedDatabaseException Exception for a closed database. 46 | */ 47 | Networks( 48 | Reader reader, 49 | boolean includeAliasedNetworks, 50 | NetworkNode[] nodes, 51 | Class typeParameterClass) 52 | throws ClosedDatabaseException { 53 | this.reader = reader; 54 | this.includeAliasedNetworks = includeAliasedNetworks; 55 | this.buffer = reader.getBufferHolder().get(); 56 | this.nodes = new Stack<>(); 57 | this.typeParameterClass = typeParameterClass; 58 | for (NetworkNode node : nodes) { 59 | this.nodes.push(node); 60 | } 61 | } 62 | 63 | /** 64 | * Constructs a Networks instance with includeAliasedNetworks set to false by default. 65 | * 66 | * @param reader The reader object. 67 | * @param typeParameterClass The type of data returned by the iterator. 68 | */ 69 | Networks(Reader reader, Class typeParameterClass) throws ClosedDatabaseException { 70 | this(reader, false, typeParameterClass); 71 | } 72 | 73 | /** 74 | * Returns the next DataRecord. 75 | * 76 | * @return The next DataRecord. 77 | * @throws NetworksIterationException An exception when iterating over the networks. 78 | */ 79 | @Override 80 | public DatabaseRecord next() { 81 | try { 82 | T data = this.reader.resolveDataPointer( 83 | this.buffer, this.lastNode.pointer, this.typeParameterClass); 84 | 85 | byte[] ip = this.lastNode.ip; 86 | int prefixLength = this.lastNode.prefix; 87 | 88 | // We do this because uses of includeAliasedNetworks will get IPv4 networks 89 | // from the ::FFFF:0:0/96. We want to return the IPv4 form of the address 90 | // in that case. 91 | if (!this.includeAliasedNetworks && isInIpv4Subtree(ip)) { 92 | ip = Arrays.copyOfRange(ip, 12, ip.length); 93 | prefixLength -= 96; 94 | } 95 | 96 | // If the ip is in ipv6 form, drop the prefix manually 97 | // as InetAddress converts it to ipv4. 98 | InetAddress ipAddr = InetAddress.getByAddress(ip); 99 | if (ipAddr instanceof Inet4Address && ip.length > 4 && prefixLength > 96) { 100 | prefixLength -= 96; 101 | } 102 | 103 | return new DatabaseRecord<>(data, ipAddr, prefixLength); 104 | } catch (IOException e) { 105 | throw new NetworksIterationException(e); 106 | } 107 | } 108 | 109 | private boolean isInIpv4Subtree(byte[] ip) { 110 | if (ip.length != 16) { 111 | return false; 112 | } 113 | for (int i = 0; i < 12; i++) { 114 | if (ip[i] != 0) { 115 | return false; 116 | } 117 | } 118 | return true; 119 | } 120 | 121 | /** 122 | * hasNext prepares the next network for reading with the Network method. It 123 | * returns true if there is another network to be processed and false if there 124 | * are no more networks. 125 | * 126 | * @return boolean True if there is another network to be processed. 127 | * @throws NetworksIterationException Exception while iterating over the networks. 128 | */ 129 | @Override 130 | public boolean hasNext() { 131 | while (!this.nodes.isEmpty()) { 132 | NetworkNode node = this.nodes.pop(); 133 | 134 | // Next until we don't have data. 135 | while (node.pointer != this.reader.getMetadata().getNodeCount()) { 136 | // This skips IPv4 aliases without hardcoding the networks that the writer 137 | // currently aliases. 138 | if (!this.includeAliasedNetworks && this.reader.getIpv4Start() != 0 139 | && node.pointer == this.reader.getIpv4Start() 140 | && !isInIpv4Subtree(node.ip)) { 141 | break; 142 | } 143 | 144 | if (node.pointer > this.reader.getMetadata().getNodeCount()) { 145 | this.lastNode = node; 146 | return true; 147 | } 148 | 149 | byte[] ipRight = Arrays.copyOf(node.ip, node.ip.length); 150 | if (ipRight.length <= (node.prefix >> 3)) { 151 | throw new NetworksIterationException("Invalid search tree"); 152 | } 153 | 154 | ipRight[node.prefix >> 3] |= 1 << (7 - (node.prefix % 8)); 155 | 156 | try { 157 | int rightPointer = this.reader.readNode(this.buffer, node.pointer, 1); 158 | node.prefix++; 159 | 160 | this.nodes.push(new NetworkNode(ipRight, node.prefix, rightPointer)); 161 | node.pointer = this.reader.readNode(this.buffer, node.pointer, 0); 162 | } catch (InvalidDatabaseException e) { 163 | throw new NetworksIterationException(e); 164 | } 165 | } 166 | } 167 | return false; 168 | } 169 | 170 | static class NetworkNode { 171 | /** The IP address of the node. */ 172 | public byte[] ip; 173 | /** The prefix of the node. */ 174 | public int prefix; 175 | /** The node number. */ 176 | public int pointer; 177 | 178 | /** 179 | * Constructs a network node for internal use. 180 | * 181 | * @param ip The ip address of the node. 182 | * @param prefix The prefix of the node. 183 | * @param pointer The node number 184 | */ 185 | NetworkNode(byte[] ip, int prefix, int pointer) { 186 | this.ip = ip; 187 | this.prefix = prefix; 188 | this.pointer = pointer; 189 | } 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/NetworksIterationException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | *

5 | * This class represents an error encountered while iterating over the networks. 6 | * The most likely causes are corrupt data in the database, or a bug in the reader code. 7 | *

8 | *

9 | * This exception extends RuntimeException because it is thrown by the iterator 10 | * methods in {@link Networks}. 11 | *

12 | * 13 | * @see Networks 14 | */ 15 | public class NetworksIterationException extends RuntimeException { 16 | NetworksIterationException(String message) { 17 | super(message); 18 | } 19 | 20 | NetworksIterationException(Throwable cause) { 21 | super(cause); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/NoCache.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * A no-op cache singleton. 7 | */ 8 | public final class NoCache implements NodeCache { 9 | 10 | private static final NoCache INSTANCE = new NoCache(); 11 | 12 | private NoCache() { 13 | } 14 | 15 | @Override 16 | public DecodedValue get(CacheKey key, Loader loader) throws IOException { 17 | return loader.load(key); 18 | } 19 | 20 | /** 21 | * @return the singleton instance of the NoCache class 22 | */ 23 | public static NoCache getInstance() { 24 | return INSTANCE; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/NodeCache.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * NodeCache is an interface for a cache that stores decoded values from the 7 | * data section of the database. 8 | */ 9 | public interface NodeCache { 10 | /** 11 | * A loader is used to load a value for a key that is not in the cache. 12 | */ 13 | interface Loader { 14 | /** 15 | * @param key 16 | * the key to load 17 | * @return the value for the key 18 | * @throws IOException 19 | * if there is an error loading the value 20 | */ 21 | DecodedValue load(CacheKey key) throws IOException; 22 | } 23 | 24 | /** 25 | * This method returns the value for the key. If the key is not in the cache 26 | * then the loader is called to load the value. 27 | * 28 | * @param key 29 | * the key to look up 30 | * @param loader 31 | * the loader to use if the key is not in the cache 32 | * @return the value for the key 33 | * @throws IOException 34 | * if there is an error loading the value 35 | */ 36 | DecodedValue get(CacheKey key, Loader loader) throws IOException; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/ParameterNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | /** 4 | * Signals that no annotated parameter was found. You should annotate 5 | * parameters of the constructor class with the MaxMindDbParameter annotation. 6 | */ 7 | public class ParameterNotFoundException extends RuntimeException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | ParameterNotFoundException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Reader.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.Inet6Address; 8 | import java.net.InetAddress; 9 | import java.net.UnknownHostException; 10 | import java.nio.ByteBuffer; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | /** 15 | * Instances of this class provide a reader for the MaxMind DB format. IP 16 | * addresses can be looked up using the get method. 17 | */ 18 | public final class Reader implements Closeable { 19 | private static final int IPV4_LEN = 4; 20 | private static final int DATA_SECTION_SEPARATOR_SIZE = 16; 21 | private static final byte[] METADATA_START_MARKER = {(byte) 0xAB, 22 | (byte) 0xCD, (byte) 0xEF, 'M', 'a', 'x', 'M', 'i', 'n', 'd', '.', 23 | 'c', 'o', 'm'}; 24 | 25 | private final int ipV4Start; 26 | private final Metadata metadata; 27 | private final AtomicReference bufferHolderReference; 28 | private final NodeCache cache; 29 | private final ConcurrentHashMap constructors; 30 | 31 | /** 32 | * The file mode to use when opening a MaxMind DB. 33 | */ 34 | public enum FileMode { 35 | /** 36 | * The default file mode. This maps the database to virtual memory. This 37 | * often provides similar performance to loading the database into real 38 | * memory without the overhead. 39 | */ 40 | MEMORY_MAPPED, 41 | /** 42 | * Loads the database into memory when the reader is constructed. 43 | */ 44 | MEMORY 45 | } 46 | 47 | /** 48 | * Constructs a Reader for the MaxMind DB format, with no caching. The file 49 | * passed to it must be a valid MaxMind DB file such as a GeoIP2 database 50 | * file. 51 | * 52 | * @param database the MaxMind DB file to use. 53 | * @throws IOException if there is an error opening or reading from the file. 54 | */ 55 | public Reader(File database) throws IOException { 56 | this(database, NoCache.getInstance()); 57 | } 58 | 59 | /** 60 | * Constructs a Reader for the MaxMind DB format, with the specified backing 61 | * cache. The file passed to it must be a valid MaxMind DB file such as a 62 | * GeoIP2 database file. 63 | * 64 | * @param database the MaxMind DB file to use. 65 | * @param cache backing cache instance 66 | * @throws IOException if there is an error opening or reading from the file. 67 | */ 68 | public Reader(File database, NodeCache cache) throws IOException { 69 | this(database, FileMode.MEMORY_MAPPED, cache); 70 | } 71 | 72 | /** 73 | * Constructs a Reader with no caching, as if in mode 74 | * {@link FileMode#MEMORY}, without using a File instance. 75 | * 76 | * @param source the InputStream that contains the MaxMind DB file. 77 | * @throws IOException if there is an error reading from the Stream. 78 | */ 79 | public Reader(InputStream source) throws IOException { 80 | this(source, NoCache.getInstance()); 81 | } 82 | 83 | /** 84 | * Constructs a Reader with the specified backing cache, as if in mode 85 | * {@link FileMode#MEMORY}, without using a File instance. 86 | * 87 | * @param source the InputStream that contains the MaxMind DB file. 88 | * @param cache backing cache instance 89 | * @throws IOException if there is an error reading from the Stream. 90 | */ 91 | public Reader(InputStream source, NodeCache cache) throws IOException { 92 | this(new BufferHolder(source), "", cache); 93 | } 94 | 95 | /** 96 | * Constructs a Reader for the MaxMind DB format, with no caching. The file 97 | * passed to it must be a valid MaxMind DB file such as a GeoIP2 database 98 | * file. 99 | * 100 | * @param database the MaxMind DB file to use. 101 | * @param fileMode the mode to open the file with. 102 | * @throws IOException if there is an error opening or reading from the file. 103 | */ 104 | public Reader(File database, FileMode fileMode) throws IOException { 105 | this(database, fileMode, NoCache.getInstance()); 106 | } 107 | 108 | /** 109 | * Constructs a Reader for the MaxMind DB format, with the specified backing 110 | * cache. The file passed to it must be a valid MaxMind DB file such as a 111 | * GeoIP2 database file. 112 | * 113 | * @param database the MaxMind DB file to use. 114 | * @param fileMode the mode to open the file with. 115 | * @param cache backing cache instance 116 | * @throws IOException if there is an error opening or reading from the file. 117 | */ 118 | public Reader(File database, FileMode fileMode, NodeCache cache) throws IOException { 119 | this(new BufferHolder(database, fileMode), database.getName(), cache); 120 | } 121 | 122 | private Reader(BufferHolder bufferHolder, String name, NodeCache cache) throws IOException { 123 | this.bufferHolderReference = new AtomicReference<>( 124 | bufferHolder); 125 | 126 | if (cache == null) { 127 | throw new NullPointerException("Cache cannot be null"); 128 | } 129 | this.cache = cache; 130 | 131 | ByteBuffer buffer = bufferHolder.get(); 132 | int start = this.findMetadataStart(buffer, name); 133 | 134 | Decoder metadataDecoder = new Decoder(this.cache, buffer, start); 135 | this.metadata = metadataDecoder.decode(start, Metadata.class); 136 | 137 | this.ipV4Start = this.findIpV4StartNode(buffer); 138 | 139 | this.constructors = new ConcurrentHashMap<>(); 140 | } 141 | 142 | /** 143 | * Looks up ipAddress in the MaxMind DB. 144 | * 145 | * @param the type to populate. 146 | * @param ipAddress the IP address to look up. 147 | * @param cls the class of object to populate. 148 | * @return the object. 149 | * @throws IOException if a file I/O error occurs. 150 | */ 151 | public T get(InetAddress ipAddress, Class cls) throws IOException { 152 | return getRecord(ipAddress, cls).getData(); 153 | } 154 | 155 | int getIpv4Start() { 156 | return this.ipV4Start; 157 | } 158 | 159 | /** 160 | * Looks up ipAddress in the MaxMind DB. 161 | * 162 | * @param the type to populate. 163 | * @param ipAddress the IP address to look up. 164 | * @param cls the class of object to populate. 165 | * @return the record for the IP address. If there is no data for the 166 | * address, the non-null {@link DatabaseRecord} will still be returned. 167 | * @throws IOException if a file I/O error occurs. 168 | */ 169 | public DatabaseRecord getRecord(InetAddress ipAddress, Class cls) 170 | throws IOException { 171 | 172 | byte[] rawAddress = ipAddress.getAddress(); 173 | 174 | int[] traverseResult = traverseTree(rawAddress, rawAddress.length * 8); 175 | 176 | int pl = traverseResult[1]; 177 | int record = traverseResult[0]; 178 | 179 | int nodeCount = this.metadata.getNodeCount(); 180 | ByteBuffer buffer = this.getBufferHolder().get(); 181 | T dataRecord = null; 182 | if (record > nodeCount) { 183 | // record is a data pointer 184 | try { 185 | dataRecord = this.resolveDataPointer(buffer, record, cls); 186 | } catch (DeserializationException exception) { 187 | throw new DeserializationException( 188 | "Error getting record for IP " + ipAddress + " - " + exception.getMessage(), 189 | exception); 190 | } 191 | } 192 | return new DatabaseRecord<>(dataRecord, ipAddress, pl); 193 | } 194 | 195 | /** 196 | * Creates a Networks iterator and skips aliased networks. 197 | * Please note that a MaxMind DB may map IPv4 networks into several locations 198 | * in an IPv6 database. networks() iterates over the canonical locations and 199 | * not the aliases. To include the aliases, you can set includeAliasedNetworks to true. 200 | * 201 | * @param Represents the data type(e.g., Map, HastMap, etc.). 202 | * @param typeParameterClass The type of data returned by the iterator. 203 | * @return Networks The Networks iterator. 204 | * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database. 205 | * @throws ClosedDatabaseException Exception for a closed databased. 206 | * @throws InvalidDatabaseException Exception for an invalid database. 207 | */ 208 | public Networks networks(Class typeParameterClass) throws 209 | InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { 210 | return this.networks(false, typeParameterClass); 211 | } 212 | 213 | /** 214 | * Creates a Networks iterator. 215 | * Please note that a MaxMind DB may map IPv4 networks into several locations 216 | * in an IPv6 database. This iterator will iterate over all of these locations 217 | * separately. To set the iteration over the IPv4 networks once, use the 218 | * includeAliasedNetworks option. 219 | * 220 | * @param Represents the data type(e.g., Map, HastMap, etc.). 221 | * @param includeAliasedNetworks Enable including aliased networks. 222 | * @return Networks The Networks iterator. 223 | * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database. 224 | * @throws ClosedDatabaseException Exception for a closed databased. 225 | * @throws InvalidDatabaseException Exception for an invalid database. 226 | */ 227 | public Networks networks( 228 | boolean includeAliasedNetworks, 229 | Class typeParameterClass) throws 230 | InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { 231 | try { 232 | if (this.getMetadata().getIpVersion() == 6) { 233 | InetAddress ipv6 = InetAddress.getByAddress(new byte[16]); 234 | Network ipAllV6 = new Network(ipv6, 0); // Mask 128. 235 | return this.networksWithin(ipAllV6, includeAliasedNetworks, typeParameterClass); 236 | } 237 | 238 | InetAddress ipv4 = InetAddress.getByAddress(new byte[4]); 239 | Network ipAllV4 = new Network(ipv4, 0); // Mask 32. 240 | return this.networksWithin(ipAllV4, includeAliasedNetworks, typeParameterClass); 241 | } catch (UnknownHostException e) { 242 | /* This is returned by getByAddress. This should never happen 243 | as the ipv4 and ipv6 are constants set by us. */ 244 | return null; 245 | } 246 | } 247 | 248 | BufferHolder getBufferHolder() throws ClosedDatabaseException { 249 | BufferHolder bufferHolder = this.bufferHolderReference.get(); 250 | if (bufferHolder == null) { 251 | throw new ClosedDatabaseException(); 252 | } 253 | return bufferHolder; 254 | } 255 | 256 | private int startNode(int bitLength) { 257 | // Check if we are looking up an IPv4 address in an IPv6 tree. If this 258 | // is the case, we can skip over the first 96 nodes. 259 | if (this.metadata.getIpVersion() == 6 && bitLength == 32) { 260 | return this.ipV4Start; 261 | } 262 | // The first node of the tree is always node 0, at the beginning of the 263 | // value 264 | return 0; 265 | } 266 | 267 | private int findIpV4StartNode(ByteBuffer buffer) 268 | throws InvalidDatabaseException { 269 | if (this.metadata.getIpVersion() == 4) { 270 | return 0; 271 | } 272 | 273 | int node = 0; 274 | for (int i = 0; i < 96 && node < this.metadata.getNodeCount(); i++) { 275 | node = this.readNode(buffer, node, 0); 276 | } 277 | return node; 278 | } 279 | 280 | /** 281 | * Returns an iterator within the specified network. 282 | * Please note that a MaxMind DB may map IPv4 networks into several locations 283 | * in an IPv6 database. This iterator will iterate over all of these locations 284 | * separately. To only iterate over the IPv4 networks once, use the 285 | * includeAliasedNetworks option. 286 | * 287 | * @param Represents the data type(e.g., Map, HastMap, etc.). 288 | * @param network Specifies the network to be iterated. 289 | * @param includeAliasedNetworks Boolean for including aliased networks. 290 | * @param typeParameterClass The type of data returned by the iterator. 291 | * @return Networks 292 | * @throws InvalidNetworkException Exception for using an IPv6 network in ipv4-only database. 293 | * @throws ClosedDatabaseException Exception for a closed databased. 294 | * @throws InvalidDatabaseException Exception for an invalid database. 295 | */ 296 | public Networks networksWithin( 297 | Network network, 298 | boolean includeAliasedNetworks, 299 | Class typeParameterClass) 300 | throws InvalidNetworkException, ClosedDatabaseException, InvalidDatabaseException { 301 | InetAddress networkAddress = network.getNetworkAddress(); 302 | if (this.metadata.getIpVersion() == 4 && networkAddress instanceof Inet6Address) { 303 | throw new InvalidNetworkException(networkAddress); 304 | } 305 | 306 | byte[] ipBytes = networkAddress.getAddress(); 307 | int prefixLength = network.getPrefixLength(); 308 | 309 | if (this.metadata.getIpVersion() == 6 && ipBytes.length == IPV4_LEN) { 310 | if (includeAliasedNetworks) { 311 | // Convert it to the IP address (in 16-byte from) of the IPv4 address. 312 | ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 313 | -1, -1, // -1 is for 0xff. 314 | ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]}; 315 | } else { 316 | ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 317 | ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3] }; 318 | } 319 | prefixLength += 96; 320 | } 321 | 322 | int[] traverseResult = this.traverseTree(ipBytes, prefixLength); 323 | int node = traverseResult[0]; 324 | int prefix = traverseResult[1]; 325 | 326 | return new Networks<>(this, includeAliasedNetworks, 327 | new Networks.NetworkNode[] {new Networks.NetworkNode(ipBytes, prefix, node)}, 328 | typeParameterClass); 329 | } 330 | 331 | /** 332 | * Returns the node number and the prefix for the network. 333 | * 334 | * @param ip The ip address to traverse. 335 | * @param bitCount The prefix. 336 | * @return int[] 337 | */ 338 | private int[] traverseTree(byte[] ip, int bitCount) 339 | throws ClosedDatabaseException, InvalidDatabaseException { 340 | ByteBuffer buffer = this.getBufferHolder().get(); 341 | int bitLength = ip.length * 8; 342 | int record = this.startNode(bitLength); 343 | int nodeCount = this.metadata.getNodeCount(); 344 | 345 | int i = 0; 346 | for (; i < bitCount && record < nodeCount; i++) { 347 | int b = 0xFF & ip[i / 8]; 348 | int bit = 1 & (b >> 7 - (i % 8)); 349 | 350 | // bit:0 -> left record. 351 | // bit:1 -> right record. 352 | record = this.readNode(buffer, record, bit); 353 | } 354 | 355 | return new int[]{record, i}; 356 | } 357 | 358 | int readNode(ByteBuffer buffer, int nodeNumber, int index) 359 | throws InvalidDatabaseException { 360 | // index is the index of the record within the node, which 361 | // can either be 0 or 1. 362 | int baseOffset = nodeNumber * this.metadata.getNodeByteSize(); 363 | 364 | switch (this.metadata.getRecordSize()) { 365 | case 24: 366 | // For a 24 bit record, each record is 3 bytes. 367 | buffer.position(baseOffset + index * 3); 368 | return Decoder.decodeInteger(buffer, 0, 3); 369 | case 28: 370 | int middle = buffer.get(baseOffset + 3); 371 | 372 | if (index == 0) { 373 | // We get the most significant from the first half 374 | // of the byte. It belongs to the first record. 375 | middle = (0xF0 & middle) >>> 4; 376 | } else { 377 | // We get the most significant byte of the second record. 378 | middle = 0x0F & middle; 379 | } 380 | buffer.position(baseOffset + index * 4); 381 | return Decoder.decodeInteger(buffer, middle, 3); 382 | case 32: 383 | buffer.position(baseOffset + index * 4); 384 | return Decoder.decodeInteger(buffer, 0, 4); 385 | default: 386 | throw new InvalidDatabaseException("Unknown record size: " 387 | + this.metadata.getRecordSize()); 388 | } 389 | } 390 | 391 | T resolveDataPointer( 392 | ByteBuffer buffer, 393 | int pointer, 394 | Class cls 395 | ) throws IOException { 396 | int resolved = (pointer - this.metadata.getNodeCount()) 397 | + this.metadata.getSearchTreeSize(); 398 | 399 | if (resolved >= buffer.capacity()) { 400 | throw new InvalidDatabaseException( 401 | "The MaxMind DB file's search tree is corrupt: " 402 | + "contains pointer larger than the database."); 403 | } 404 | 405 | // We only want the data from the decoder, not the offset where it was 406 | // found. 407 | Decoder decoder = new Decoder( 408 | this.cache, 409 | buffer, 410 | this.metadata.getSearchTreeSize() + DATA_SECTION_SEPARATOR_SIZE, 411 | this.constructors 412 | ); 413 | return decoder.decode(resolved, cls); 414 | } 415 | 416 | /* 417 | * Apparently searching a file for a sequence is not a solved problem in 418 | * Java. This searches from the end of the file for metadata start. 419 | * 420 | * This is an extremely naive but reasonably readable implementation. There 421 | * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever 422 | * an issue, but I suspect it won't be. 423 | */ 424 | private int findMetadataStart(ByteBuffer buffer, String databaseName) 425 | throws InvalidDatabaseException { 426 | int fileSize = buffer.capacity(); 427 | 428 | FILE: 429 | for (int i = 0; i < fileSize - METADATA_START_MARKER.length + 1; i++) { 430 | for (int j = 0; j < METADATA_START_MARKER.length; j++) { 431 | byte b = buffer.get(fileSize - i - j - 1); 432 | if (b != METADATA_START_MARKER[METADATA_START_MARKER.length - j 433 | - 1]) { 434 | continue FILE; 435 | } 436 | } 437 | return fileSize - i; 438 | } 439 | throw new InvalidDatabaseException( 440 | "Could not find a MaxMind DB metadata marker in this file (" 441 | + databaseName + "). Is this a valid MaxMind DB file?"); 442 | } 443 | 444 | /** 445 | * @return the metadata for the MaxMind DB file. 446 | */ 447 | public Metadata getMetadata() { 448 | return this.metadata; 449 | } 450 | 451 | /** 452 | *

453 | * Closes the database. 454 | *

455 | *

456 | * If you are using FileMode.MEMORY_MAPPED, this will 457 | * not unmap the underlying file due to a limitation in Java's 458 | * MappedByteBuffer. It will however set the reference to 459 | * the buffer to null, allowing the garbage collector to 460 | * collect it. 461 | *

462 | * 463 | * @throws IOException if an I/O error occurs. 464 | */ 465 | @Override 466 | public void close() throws IOException { 467 | this.bufferHolderReference.set(null); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/Type.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | enum Type { 4 | EXTENDED, POINTER, UTF8_STRING, DOUBLE, BYTES, UINT16, UINT32, MAP, INT32, UINT64, UINT128, 5 | ARRAY, CONTAINER, END_MARKER, BOOLEAN, FLOAT; 6 | 7 | // Java clones the array when you call values(). Caching it increased 8 | // the speed by about 5000 requests per second on my machine. 9 | static final Type[] values = Type.values(); 10 | 11 | static Type get(int i) throws InvalidDatabaseException { 12 | if (i >= Type.values.length) { 13 | throw new InvalidDatabaseException( 14 | "The MaxMind DB file's data section contains bad data"); 15 | } 16 | return Type.values[i]; 17 | } 18 | 19 | private static Type get(byte b) throws InvalidDatabaseException { 20 | // bytes are signed, but we want to treat them as unsigned here 21 | return Type.get(b & 0xFF); 22 | } 23 | 24 | static Type fromControlByte(int b) throws InvalidDatabaseException { 25 | // The type is encoded in the first 3 bits of the byte. 26 | return Type.get((byte) ((0xFF & b) >>> 5)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/maxmind/db/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author greg 3 | */ 4 | 5 | package com.maxmind.db; 6 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.maxmind.db { 2 | exports com.maxmind.db; 3 | } 4 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/DecoderTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import static org.hamcrest.CoreMatchers.containsString; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import java.io.IOException; 10 | import java.math.BigInteger; 11 | import java.nio.ByteBuffer; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import org.junit.jupiter.api.Test; 18 | 19 | @SuppressWarnings({"boxing", "static-method"}) 20 | public class DecoderTest { 21 | 22 | private static Map int32() { 23 | int max = (2 << 30) - 1; 24 | HashMap int32 = new HashMap<>(); 25 | 26 | int32.put(0, new byte[] {0x0, 0x1}); 27 | int32.put(-1, new byte[] {0x4, 0x1, (byte) 0xff, (byte) 0xff, 28 | (byte) 0xff, (byte) 0xff}); 29 | int32.put((2 << 7) - 1, new byte[] {0x1, 0x1, (byte) 0xff}); 30 | int32.put(1 - (2 << 7), new byte[] {0x4, 0x1, (byte) 0xff, 31 | (byte) 0xff, (byte) 0xff, 0x1}); 32 | int32.put(500, new byte[] {0x2, 0x1, 0x1, (byte) 0xf4}); 33 | 34 | int32.put(-500, new byte[] {0x4, 0x1, (byte) 0xff, (byte) 0xff, 35 | (byte) 0xfe, 0xc}); 36 | 37 | int32.put((2 << 15) - 1, new byte[] {0x2, 0x1, (byte) 0xff, 38 | (byte) 0xff}); 39 | int32.put(1 - (2 << 15), new byte[] {0x4, 0x1, (byte) 0xff, 40 | (byte) 0xff, 0x0, 0x1}); 41 | int32.put((2 << 23) - 1, new byte[] {0x3, 0x1, (byte) 0xff, 42 | (byte) 0xff, (byte) 0xff}); 43 | int32.put(1 - (2 << 23), new byte[] {0x4, 0x1, (byte) 0xff, 0x0, 0x0, 44 | 0x1}); 45 | int32.put(max, new byte[] {0x4, 0x1, 0x7f, (byte) 0xff, (byte) 0xff, 46 | (byte) 0xff}); 47 | int32.put(-max, new byte[] {0x4, 0x1, (byte) 0x80, 0x0, 0x0, 0x1}); 48 | return int32; 49 | } 50 | 51 | private static Map uint32() { 52 | long max = (((long) 1) << 32) - 1; 53 | HashMap uint32s = new HashMap<>(); 54 | 55 | uint32s.put((long) 0, new byte[] {(byte) 0xc0}); 56 | uint32s.put((long) ((1 << 8) - 1), new byte[] {(byte) 0xc1, 57 | (byte) 0xff}); 58 | uint32s.put((long) 500, new byte[] {(byte) 0xc2, 0x1, (byte) 0xf4}); 59 | uint32s.put((long) 10872, new byte[] {(byte) 0xc2, 0x2a, 0x78}); 60 | uint32s.put((long) ((1 << 16) - 1), new byte[] {(byte) 0xc2, 61 | (byte) 0xff, (byte) 0xff}); 62 | uint32s.put((long) ((1 << 24) - 1), new byte[] {(byte) 0xc3, 63 | (byte) 0xff, (byte) 0xff, (byte) 0xff}); 64 | uint32s.put(max, new byte[] {(byte) 0xc4, (byte) 0xff, (byte) 0xff, 65 | (byte) 0xff, (byte) 0xff}); 66 | 67 | return uint32s; 68 | } 69 | 70 | private static Map uint16() { 71 | int max = (1 << 16) - 1; 72 | 73 | Map uint16s = new HashMap<>(); 74 | 75 | uint16s.put(0, new byte[] {(byte) 0xa0}); 76 | uint16s.put((1 << 8) - 1, new byte[] {(byte) 0xa1, (byte) 0xff}); 77 | uint16s.put(500, new byte[] {(byte) 0xa2, 0x1, (byte) 0xf4}); 78 | uint16s.put(10872, new byte[] {(byte) 0xa2, 0x2a, 0x78}); 79 | uint16s.put(max, new byte[] {(byte) 0xa2, (byte) 0xff, (byte) 0xff}); 80 | return uint16s; 81 | } 82 | 83 | private static Map largeUint(int bits) { 84 | Map uints = new HashMap<>(); 85 | 86 | byte ctrlByte = (byte) (bits == 64 ? 0x2 : 0x3); 87 | 88 | uints.put(BigInteger.valueOf(0), new byte[] {0x0, ctrlByte}); 89 | uints.put(BigInteger.valueOf(500), new byte[] {0x2, ctrlByte, 0x1, 90 | (byte) 0xf4}); 91 | uints.put(BigInteger.valueOf(10872), new byte[] {0x2, ctrlByte, 0x2a, 92 | 0x78}); 93 | 94 | for (int power = 1; power <= bits / 8; power++) { 95 | 96 | BigInteger key = BigInteger.valueOf(2).pow(8 * power) 97 | .subtract(BigInteger.valueOf(1)); 98 | 99 | byte[] value = new byte[2 + power]; 100 | value[0] = (byte) power; 101 | value[1] = ctrlByte; 102 | for (int i = 2; i < value.length; i++) { 103 | value[i] = (byte) 0xff; 104 | } 105 | uints.put(key, value); 106 | } 107 | return uints; 108 | 109 | } 110 | 111 | private static Map pointers() { 112 | Map pointers = new HashMap<>(); 113 | 114 | pointers.put((long) 0, new byte[] {0x20, 0x0}); 115 | pointers.put((long) 5, new byte[] {0x20, 0x5}); 116 | pointers.put((long) 10, new byte[] {0x20, 0xa}); 117 | pointers.put((long) ((1 << 10) - 1), new byte[] {0x23, (byte) 0xff,}); 118 | pointers.put((long) 3017, new byte[] {0x28, 0x3, (byte) 0xc9}); 119 | pointers.put((long) ((1 << 19) - 5), new byte[] {0x2f, (byte) 0xf7, 120 | (byte) 0xfb}); 121 | pointers.put((long) ((1 << 19) + (1 << 11) - 1), new byte[] {0x2f, 122 | (byte) 0xff, (byte) 0xff}); 123 | pointers.put((long) ((1 << 27) - 2), new byte[] {0x37, (byte) 0xf7, 124 | (byte) 0xf7, (byte) 0xfe}); 125 | pointers.put((((long) 1) << 27) + (1 << 19) + (1 << 11) - 1, 126 | new byte[] {0x37, (byte) 0xff, (byte) 0xff, (byte) 0xff}); 127 | 128 | pointers.put((((long) 1) << 31) - 1, new byte[] {0x38, (byte) 0x7f, 129 | (byte) 0xff, (byte) 0xff, (byte) 0xff}); 130 | 131 | return pointers; 132 | } 133 | 134 | private static Map strings() { 135 | Map strings = new HashMap<>(); 136 | 137 | DecoderTest.addTestString(strings, (byte) 0x40, ""); 138 | DecoderTest.addTestString(strings, (byte) 0x41, "1"); 139 | DecoderTest.addTestString(strings, (byte) 0x43, "人"); 140 | DecoderTest.addTestString(strings, (byte) 0x43, "123"); 141 | DecoderTest.addTestString(strings, (byte) 0x5b, 142 | "123456789012345678901234567"); 143 | DecoderTest.addTestString(strings, (byte) 0x5c, 144 | "1234567890123456789012345678"); 145 | DecoderTest.addTestString(strings, (byte) 0x5c, 146 | "1234567890123456789012345678"); 147 | DecoderTest.addTestString(strings, new byte[] {0x5d, 0x0}, 148 | "12345678901234567890123456789"); 149 | DecoderTest.addTestString(strings, new byte[] {0x5d, (byte) 128}, 150 | "x".repeat(157)); 151 | 152 | DecoderTest 153 | .addTestString(strings, new byte[] {0x5d, 0x0, (byte) 0xd7}, 154 | "x".repeat(500)); 155 | 156 | DecoderTest 157 | .addTestString(strings, new byte[] {0x5e, 0x0, (byte) 0xd7}, 158 | "x".repeat(500)); 159 | DecoderTest.addTestString(strings, 160 | new byte[] {0x5e, 0x6, (byte) 0xb3}, 161 | "x".repeat(2000)); 162 | DecoderTest.addTestString(strings, 163 | new byte[] {0x5f, 0x0, 0x10, 0x53,}, 164 | "x".repeat(70000)); 165 | 166 | return strings; 167 | 168 | } 169 | 170 | private static Map bytes() { 171 | Map bytes = new HashMap<>(); 172 | 173 | Map strings = DecoderTest.strings(); 174 | 175 | for (String s : strings.keySet()) { 176 | byte[] ba = strings.get(s); 177 | ba[0] ^= 0xc0; 178 | 179 | bytes.put(s.getBytes(StandardCharsets.UTF_8), ba); 180 | } 181 | 182 | return bytes; 183 | } 184 | 185 | private static void addTestString(Map tests, byte ctrl, 186 | String str) { 187 | DecoderTest.addTestString(tests, new byte[] {ctrl}, str); 188 | } 189 | 190 | private static void addTestString(Map tests, byte[] ctrl, 191 | String str) { 192 | 193 | byte[] sb = str.getBytes(StandardCharsets.UTF_8); 194 | byte[] bytes = new byte[ctrl.length + sb.length]; 195 | 196 | System.arraycopy(ctrl, 0, bytes, 0, ctrl.length); 197 | System.arraycopy(sb, 0, bytes, ctrl.length, sb.length); 198 | tests.put(str, bytes); 199 | } 200 | 201 | private static Map doubles() { 202 | Map doubles = new HashMap<>(); 203 | doubles.put(0.0, new byte[] {0x68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 204 | 0x0}); 205 | doubles.put(0.5, new byte[] {0x68, 0x3F, (byte) 0xE0, 0x0, 0x0, 0x0, 206 | 0x0, 0x0, 0x0}); 207 | doubles.put(3.14159265359, new byte[] {0x68, 0x40, 0x9, 0x21, 208 | (byte) 0xFB, 0x54, 0x44, 0x2E, (byte) 0xEA}); 209 | doubles.put(123.0, new byte[] {0x68, 0x40, 0x5E, (byte) 0xC0, 0x0, 210 | 0x0, 0x0, 0x0, 0x0}); 211 | doubles.put(1073741824.12457, new byte[] {0x68, 0x41, (byte) 0xD0, 212 | 0x0, 0x0, 0x0, 0x7, (byte) 0xF8, (byte) 0xF4}); 213 | doubles.put(-0.5, new byte[] {0x68, (byte) 0xBF, (byte) 0xE0, 0x0, 214 | 0x0, 0x0, 0x0, 0x0, 0x0}); 215 | doubles.put(-3.14159265359, new byte[] {0x68, (byte) 0xC0, 0x9, 0x21, 216 | (byte) 0xFB, 0x54, 0x44, 0x2E, (byte) 0xEA}); 217 | doubles.put(-1073741824.12457, new byte[] {0x68, (byte) 0xC1, 218 | (byte) 0xD0, 0x0, 0x0, 0x0, 0x7, (byte) 0xF8, (byte) 0xF4}); 219 | 220 | return doubles; 221 | } 222 | 223 | private static Map floats() { 224 | Map floats = new HashMap<>(); 225 | floats.put((float) 0.0, new byte[] {0x4, 0x8, 0x0, 0x0, 0x0, 0x0}); 226 | floats.put((float) 1.0, new byte[] {0x4, 0x8, 0x3F, (byte) 0x80, 0x0, 227 | 0x0}); 228 | floats.put((float) 1.1, new byte[] {0x4, 0x8, 0x3F, (byte) 0x8C, 229 | (byte) 0xCC, (byte) 0xCD}); 230 | floats.put((float) 3.14, new byte[] {0x4, 0x8, 0x40, 0x48, 231 | (byte) 0xF5, (byte) 0xC3}); 232 | floats.put((float) 9999.99, new byte[] {0x4, 0x8, 0x46, 0x1C, 0x3F, 233 | (byte) 0xF6}); 234 | floats.put((float) -1.0, new byte[] {0x4, 0x8, (byte) 0xBF, 235 | (byte) 0x80, 0x0, 0x0}); 236 | floats.put((float) -1.1, new byte[] {0x4, 0x8, (byte) 0xBF, 237 | (byte) 0x8C, (byte) 0xCC, (byte) 0xCD}); 238 | floats.put((float) -3.14, new byte[] {0x4, 0x8, (byte) 0xC0, 0x48, 239 | (byte) 0xF5, (byte) 0xC3}); 240 | floats.put((float) -9999.99, new byte[] {0x4, 0x8, (byte) 0xC6, 0x1C, 241 | 0x3F, (byte) 0xF6}); 242 | 243 | return floats; 244 | } 245 | 246 | private static Map booleans() { 247 | Map booleans = new HashMap<>(); 248 | 249 | booleans.put(Boolean.FALSE, new byte[] {0x0, 0x7}); 250 | booleans.put(Boolean.TRUE, new byte[] {0x1, 0x7}); 251 | return booleans; 252 | } 253 | 254 | private static Map, byte[]> maps() { 255 | Map, byte[]> maps = new HashMap<>(); 256 | 257 | Map empty = Map.of(); 258 | maps.put(empty, new byte[] {(byte) 0xe0}); 259 | 260 | Map one = new HashMap<>(); 261 | one.put("en", "Foo"); 262 | maps.put(one, new byte[] {(byte) 0xe1, /* en */0x42, 0x65, 0x6e, 263 | /* Foo */0x43, 0x46, 0x6f, 0x6f}); 264 | 265 | Map two = new HashMap<>(); 266 | two.put("en", "Foo"); 267 | two.put("zh", "人"); 268 | maps.put(two, new byte[] {(byte) 0xe2, 269 | /* en */ 270 | 0x42, 0x65, 0x6e, 271 | /* Foo */ 272 | 0x43, 0x46, 0x6f, 0x6f, 273 | /* zh */ 274 | 0x42, 0x7a, 0x68, 275 | /* 人 */ 276 | 0x43, (byte) 0xe4, (byte) 0xba, (byte) 0xba}); 277 | 278 | Map> nested = new HashMap<>(); 279 | nested.put("name", two); 280 | 281 | maps.put(nested, new byte[] {(byte) 0xe1, /* name */ 282 | 0x44, 0x6e, 0x61, 0x6d, 0x65, (byte) 0xe2, /* en */ 283 | 0x42, 0x65, 0x6e, 284 | /* Foo */ 285 | 0x43, 0x46, 0x6f, 0x6f, 286 | /* zh */ 287 | 0x42, 0x7a, 0x68, 288 | /* 人 */ 289 | 0x43, (byte) 0xe4, (byte) 0xba, (byte) 0xba}); 290 | 291 | Map> guess = new HashMap<>(); 292 | List languages = new ArrayList<>(); 293 | languages.add("en"); 294 | languages.add("zh"); 295 | guess.put("languages", languages); 296 | maps.put(guess, new byte[] {(byte) 0xe1, /* languages */ 297 | 0x49, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 298 | /* array */ 299 | 0x2, 0x4, 300 | /* en */ 301 | 0x42, 0x65, 0x6e, 302 | /* zh */ 303 | 0x42, 0x7a, 0x68}); 304 | 305 | return maps; 306 | } 307 | 308 | private static Map, byte[]> arrays() { 309 | Map, byte[]> arrays = new HashMap<>(); 310 | 311 | ArrayList f1 = new ArrayList<>(); 312 | f1.add("Foo"); 313 | arrays.put(f1, new byte[] {0x1, 0x4, 314 | /* Foo */ 315 | 0x43, 0x46, 0x6f, 0x6f}); 316 | 317 | ArrayList f2 = new ArrayList<>(); 318 | f2.add("Foo"); 319 | f2.add("人"); 320 | arrays.put(f2, new byte[] {0x2, 0x4, 321 | /* Foo */ 322 | 0x43, 0x46, 0x6f, 0x6f, 323 | /* 人 */ 324 | 0x43, (byte) 0xe4, (byte) 0xba, (byte) 0xba}); 325 | 326 | ArrayList empty = new ArrayList<>(); 327 | arrays.put(empty, new byte[] {0x0, 0x4}); 328 | 329 | return arrays; 330 | } 331 | 332 | @Test 333 | public void testUint16() throws IOException { 334 | DecoderTest.testTypeDecoding(Type.UINT16, uint16()); 335 | } 336 | 337 | @Test 338 | public void testUint32() throws IOException { 339 | DecoderTest.testTypeDecoding(Type.UINT32, uint32()); 340 | } 341 | 342 | @Test 343 | public void testInt32() throws IOException { 344 | DecoderTest.testTypeDecoding(Type.INT32, int32()); 345 | } 346 | 347 | @Test 348 | public void testUint64() throws IOException { 349 | DecoderTest.testTypeDecoding(Type.UINT64, largeUint(64)); 350 | } 351 | 352 | @Test 353 | public void testUint128() throws IOException { 354 | DecoderTest.testTypeDecoding(Type.UINT128, largeUint(128)); 355 | } 356 | 357 | @Test 358 | public void testDoubles() throws IOException { 359 | DecoderTest 360 | .testTypeDecoding(Type.DOUBLE, DecoderTest.doubles()); 361 | } 362 | 363 | @Test 364 | public void testFloats() throws IOException { 365 | DecoderTest.testTypeDecoding(Type.FLOAT, DecoderTest.floats()); 366 | } 367 | 368 | @Test 369 | public void testPointers() throws IOException { 370 | DecoderTest.testTypeDecoding(Type.POINTER, pointers()); 371 | } 372 | 373 | @Test 374 | public void testStrings() throws IOException { 375 | DecoderTest.testTypeDecoding(Type.UTF8_STRING, 376 | DecoderTest.strings()); 377 | } 378 | 379 | @Test 380 | public void testBooleans() throws IOException { 381 | DecoderTest.testTypeDecoding(Type.BOOLEAN, 382 | DecoderTest.booleans()); 383 | } 384 | 385 | @Test 386 | public void testBytes() throws IOException { 387 | DecoderTest.testTypeDecoding(Type.BYTES, DecoderTest.bytes()); 388 | } 389 | 390 | @Test 391 | public void testMaps() throws IOException { 392 | DecoderTest.testTypeDecoding(Type.MAP, DecoderTest.maps()); 393 | } 394 | 395 | @Test 396 | public void testArrays() throws IOException { 397 | DecoderTest.testTypeDecoding(Type.ARRAY, DecoderTest.arrays()); 398 | } 399 | 400 | @Test 401 | public void testInvalidControlByte() { 402 | ByteBuffer buffer = ByteBuffer.wrap(new byte[] {0x0, 0xF}); 403 | 404 | Decoder decoder = new Decoder(new CHMCache(), buffer, 0); 405 | InvalidDatabaseException ex = assertThrows( 406 | InvalidDatabaseException.class, 407 | () -> decoder.decode(0, String.class)); 408 | assertThat(ex.getMessage(), 409 | containsString("The MaxMind DB file's data section contains bad data")); 410 | } 411 | 412 | private static void testTypeDecoding(Type type, Map tests) 413 | throws IOException { 414 | NodeCache cache = new CHMCache(); 415 | 416 | for (Map.Entry entry : tests.entrySet()) { 417 | T expect = entry.getKey(); 418 | byte[] input = entry.getValue(); 419 | 420 | String desc = "decoded " + type.name() + " - " + expect; 421 | ByteBuffer buffer = ByteBuffer.wrap(input); 422 | 423 | Decoder decoder = new TestDecoder(cache, buffer, 0); 424 | 425 | switch (type) { 426 | case BYTES: 427 | assertArrayEquals((byte[]) expect, decoder.decode(0, byte[].class), desc); 428 | break; 429 | case ARRAY: 430 | assertEquals(expect, decoder.decode(0, List.class), desc); 431 | break; 432 | case UINT16: 433 | case INT32: 434 | assertEquals(expect, decoder.decode(0, Integer.class), desc); 435 | break; 436 | case UINT32: 437 | case POINTER: 438 | assertEquals(expect, decoder.decode(0, Long.class), desc); 439 | break; 440 | case UINT64: 441 | case UINT128: 442 | assertEquals(expect, decoder.decode(0, BigInteger.class), desc); 443 | break; 444 | case DOUBLE: 445 | assertEquals(expect, decoder.decode(0, Double.class), desc); 446 | break; 447 | case FLOAT: 448 | assertEquals(expect, decoder.decode(0, Float.class), desc); 449 | break; 450 | case UTF8_STRING: 451 | assertEquals(expect, decoder.decode(0, String.class), desc); 452 | break; 453 | case BOOLEAN: 454 | assertEquals(expect, decoder.decode(0, Boolean.class), desc); 455 | break; 456 | default: { 457 | // We hit this for Type.MAP. 458 | 459 | Map got = decoder.decode(0, Map.class); 460 | Map expectMap = (Map) expect; 461 | 462 | assertEquals(expectMap.size(), got.size(), desc); 463 | 464 | for (Object keyObject : expectMap.keySet()) { 465 | String key = (String) keyObject; 466 | Object value = expectMap.get(key); 467 | 468 | if (value instanceof Object[]) { 469 | assertArrayEquals((Object[]) value, (Object[]) got.get(key), desc); 470 | } else { 471 | assertEquals(value, got.get(key), desc); 472 | } 473 | } 474 | } 475 | } 476 | } 477 | } 478 | 479 | } 480 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/MultiThreadedTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.io.IOException; 6 | import java.net.InetAddress; 7 | import java.util.Collections; 8 | import java.util.Map; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.ExecutionException; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Executors; 13 | import java.util.concurrent.Future; 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class MultiThreadedTest { 17 | 18 | @Test 19 | public void multipleMmapOpens() throws InterruptedException, 20 | ExecutionException { 21 | Callable> task = () -> { 22 | try (Reader reader = new Reader(ReaderTest.getFile("MaxMind-DB-test-decoder.mmdb"))) { 23 | return reader.get(InetAddress.getByName("::1.1.1.0"), Map.class); 24 | } 25 | }; 26 | MultiThreadedTest.runThreads(task); 27 | } 28 | 29 | @Test 30 | public void streamThreadTest() throws IOException, InterruptedException, 31 | ExecutionException { 32 | try (Reader reader = new Reader(ReaderTest.getStream("MaxMind-DB-test-decoder.mmdb"))) { 33 | MultiThreadedTest.threadTest(reader); 34 | } 35 | } 36 | 37 | @Test 38 | public void mmapThreadTest() throws IOException, InterruptedException, 39 | ExecutionException { 40 | try (Reader reader = new Reader(ReaderTest.getFile("MaxMind-DB-test-decoder.mmdb"))) { 41 | MultiThreadedTest.threadTest(reader); 42 | } 43 | } 44 | 45 | private static void threadTest(final Reader reader) 46 | throws InterruptedException, ExecutionException { 47 | Callable> task = () -> reader.get(InetAddress.getByName("::1.1.1.0"), Map.class); 48 | MultiThreadedTest.runThreads(task); 49 | } 50 | 51 | private static void runThreads(Callable> task) 52 | throws InterruptedException, ExecutionException { 53 | int threadCount = 256; 54 | var tasks = Collections.nCopies(threadCount, task); 55 | ExecutorService executorService = Executors 56 | .newFixedThreadPool(threadCount); 57 | var futures = executorService.invokeAll(tasks); 58 | 59 | for (Future> future : futures) { 60 | Map record = future.get(); 61 | assertEquals(268435456, (long) record.get("uint32")); 62 | assertEquals("unicode! ☯ - ♫", record.get("utf8_string")); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/NetworkTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.net.InetAddress; 6 | import java.net.UnknownHostException; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class NetworkTest { 10 | @Test 11 | public void testIPv6() throws UnknownHostException { 12 | Network network = new Network( 13 | InetAddress.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), 14 | 28 15 | ); 16 | 17 | assertEquals("2001:db0:0:0:0:0:0:0", network.getNetworkAddress().getHostAddress()); 18 | assertEquals(28, network.getPrefixLength()); 19 | assertEquals("2001:db0:0:0:0:0:0:0/28", network.toString()); 20 | } 21 | 22 | @Test 23 | public void TestIPv4() throws UnknownHostException { 24 | Network network = new Network( 25 | InetAddress.getByName("192.168.213.111"), 26 | 31 27 | ); 28 | 29 | assertEquals("192.168.213.110", network.getNetworkAddress().getHostAddress()); 30 | assertEquals(31, network.getPrefixLength()); 31 | assertEquals("192.168.213.110/31", network.toString()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/PointerTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import com.maxmind.db.Reader.FileMode; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class PointerTest { 13 | @SuppressWarnings("static-method") 14 | @Test 15 | public void testWithPointers() throws IOException { 16 | File file = ReaderTest.getFile("maps-with-pointers.raw"); 17 | BufferHolder ptf = new BufferHolder(file, FileMode.MEMORY); 18 | Decoder decoder = new Decoder(NoCache.getInstance(), ptf.get(), 0); 19 | 20 | Map map = new HashMap<>(); 21 | map.put("long_key", "long_value1"); 22 | assertEquals(map, decoder.decode(0, Map.class)); 23 | 24 | map = new HashMap<>(); 25 | map.put("long_key", "long_value2"); 26 | assertEquals(map, decoder.decode(22, Map.class)); 27 | 28 | map = new HashMap<>(); 29 | map.put("long_key2", "long_value1"); 30 | assertEquals(map, decoder.decode(37, Map.class)); 31 | 32 | map = new HashMap<>(); 33 | map.put("long_key2", "long_value2"); 34 | assertEquals(map, decoder.decode(50, Map.class)); 35 | 36 | map = new HashMap<>(); 37 | map.put("long_key", "long_value1"); 38 | assertEquals(map, decoder.decode(55, Map.class)); 39 | 40 | map = new HashMap<>(); 41 | map.put("long_key2", "long_value2"); 42 | assertEquals(map, decoder.decode(57, Map.class)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/ReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import static org.hamcrest.CoreMatchers.containsString; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | import static org.junit.jupiter.api.Assertions.assertNull; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.lang.reflect.Constructor; 19 | import java.math.BigInteger; 20 | import java.net.InetAddress; 21 | import java.net.UnknownHostException; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Calendar; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Vector; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | import org.junit.jupiter.api.AfterEach; 31 | import org.junit.jupiter.api.BeforeEach; 32 | import org.junit.jupiter.api.Test; 33 | 34 | public class ReaderTest { 35 | private Reader testReader; 36 | 37 | @BeforeEach 38 | public void setupReader() { 39 | this.testReader = null; 40 | } 41 | 42 | @AfterEach 43 | public void teardownReader() throws IOException { 44 | if (this.testReader != null) { 45 | this.testReader.close(); 46 | } 47 | } 48 | 49 | @Test 50 | public void test() throws IOException { 51 | for (long recordSize : new long[] {24, 28, 32}) { 52 | for (int ipVersion : new int[] {4, 6}) { 53 | File file = getFile("MaxMind-DB-test-ipv" + ipVersion + "-" + recordSize + ".mmdb"); 54 | try (Reader reader = new Reader(file)) { 55 | this.testMetadata(reader, ipVersion, recordSize); 56 | if (ipVersion == 4) { 57 | this.testIpV4(reader, file); 58 | } else { 59 | this.testIpV6(reader, file); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | static class GetRecordTest { 67 | InetAddress ip; 68 | File db; 69 | String network; 70 | boolean hasRecord; 71 | 72 | GetRecordTest(String ip, String file, String network, boolean hasRecord) 73 | throws UnknownHostException { 74 | this.ip = InetAddress.getByName(ip); 75 | db = getFile(file); 76 | this.network = network; 77 | this.hasRecord = hasRecord; 78 | } 79 | } 80 | 81 | @Test 82 | public void testNetworks() throws IOException, InvalidDatabaseException, InvalidNetworkException { 83 | for (long recordSize : new long[] {24, 28, 32}) { 84 | for (int ipVersion : new int[] {4, 6}) { 85 | File file = getFile("MaxMind-DB-test-ipv" + ipVersion + "-" + recordSize + ".mmdb"); 86 | 87 | Reader reader = new Reader(file); 88 | var networks = reader.networks(false, Map.class); 89 | 90 | while(networks.hasNext()) { 91 | var iteration = networks.next(); 92 | var data = (Map) iteration.getData(); 93 | 94 | InetAddress actualIPInData = InetAddress.getByName((String) data.get("ip")); 95 | 96 | assertEquals( 97 | iteration.getNetwork().getNetworkAddress(), 98 | actualIPInData, 99 | "expected ip address" 100 | ); 101 | } 102 | 103 | reader.close(); 104 | } 105 | } 106 | } 107 | 108 | @Test 109 | public void testNetworksWithInvalidSearchTree() throws IOException, InvalidNetworkException{ 110 | File file = getFile("MaxMind-DB-test-broken-search-tree-24.mmdb"); 111 | Reader reader = new Reader(file); 112 | 113 | var networks = reader.networks(false, Map.class); 114 | 115 | Exception exception = assertThrows(RuntimeException.class, () -> { 116 | while(networks.hasNext()){ 117 | assertNotNull(networks.next()); 118 | } 119 | }); 120 | 121 | assertEquals("Invalid search tree", exception.getMessage()); 122 | reader.close(); 123 | } 124 | 125 | private class networkTest { 126 | String network; 127 | String database; 128 | int prefix; 129 | String[] expected; 130 | boolean skipAliasedNetworks; 131 | public networkTest(String network, int prefix,String database, String[] expected, boolean skipAliasedNetworks){ 132 | this(network, prefix, database, expected); 133 | this.skipAliasedNetworks = skipAliasedNetworks; 134 | } 135 | public networkTest(String network, int prefix,String database, String[] expected){ 136 | this.network = network; 137 | this.prefix = prefix; 138 | this.database = database; 139 | this.expected = expected; 140 | } 141 | } 142 | 143 | private networkTest[] tests = new networkTest[]{ 144 | new networkTest( 145 | "0.0.0.0", 146 | 0, 147 | "ipv4", 148 | new String[]{ 149 | "1.1.1.1/32", 150 | "1.1.1.2/31", 151 | "1.1.1.4/30", 152 | "1.1.1.8/29", 153 | "1.1.1.16/28", 154 | "1.1.1.32/32", 155 | } 156 | ), 157 | new networkTest( 158 | "1.1.1.1", 159 | 30, 160 | "ipv4", 161 | new String[]{ 162 | "1.1.1.1/32", 163 | "1.1.1.2/31", 164 | } 165 | ), 166 | new networkTest( 167 | "1.1.1.1", 168 | 32, 169 | "ipv4", 170 | new String[]{ 171 | "1.1.1.1/32", 172 | } 173 | ), 174 | new networkTest( 175 | "255.255.255.0", 176 | 24, 177 | "ipv4", 178 | new String[]{} 179 | ), 180 | new networkTest( 181 | "1.1.1.1", 182 | 32, 183 | "mixed", 184 | new String[]{ 185 | "1.1.1.1/32", 186 | } 187 | ), 188 | new networkTest( 189 | "255.255.255.0", 190 | 24, 191 | "mixed", 192 | new String[]{} 193 | ), 194 | new networkTest( 195 | "::1:ffff:ffff", 196 | 128, 197 | "ipv6", 198 | new String[]{ 199 | "0:0:0:0:0:1:ffff:ffff/128", 200 | }, 201 | true 202 | ), 203 | new networkTest( 204 | "::", 205 | 0, 206 | "ipv6", 207 | new String[]{ 208 | "0:0:0:0:0:1:ffff:ffff/128", 209 | "0:0:0:0:0:2:0:0/122", 210 | "0:0:0:0:0:2:0:40/124", 211 | "0:0:0:0:0:2:0:50/125", 212 | "0:0:0:0:0:2:0:58/127", 213 | } 214 | ), 215 | new networkTest( 216 | "::2:0:40", 217 | 123, 218 | "ipv6", 219 | new String[]{ 220 | "0:0:0:0:0:2:0:40/124", 221 | "0:0:0:0:0:2:0:50/125", 222 | "0:0:0:0:0:2:0:58/127", 223 | } 224 | ), 225 | new networkTest( 226 | "0:0:0:0:0:ffff:ffff:ff00", 227 | 120, 228 | "ipv6", 229 | new String[]{} 230 | ), 231 | new networkTest( 232 | "0.0.0.0", 233 | 0, 234 | "mixed", 235 | new String[]{ 236 | "1.1.1.1/32", 237 | "1.1.1.2/31", 238 | "1.1.1.4/30", 239 | "1.1.1.8/29", 240 | "1.1.1.16/28", 241 | "1.1.1.32/32", 242 | } 243 | ), 244 | new networkTest( 245 | "0.0.0.0", 246 | 0, 247 | "mixed", 248 | new String[]{ 249 | "1.1.1.1/32", 250 | "1.1.1.2/31", 251 | "1.1.1.4/30", 252 | "1.1.1.8/29", 253 | "1.1.1.16/28", 254 | "1.1.1.32/32", 255 | }, 256 | true 257 | ), 258 | new networkTest( 259 | "::", 260 | 0, 261 | "mixed", 262 | new String[]{ 263 | "0:0:0:0:0:0:101:101/128", 264 | "0:0:0:0:0:0:101:102/127", 265 | "0:0:0:0:0:0:101:104/126", 266 | "0:0:0:0:0:0:101:108/125", 267 | "0:0:0:0:0:0:101:110/124", 268 | "0:0:0:0:0:0:101:120/128", 269 | "0:0:0:0:0:1:ffff:ffff/128", 270 | "0:0:0:0:0:2:0:0/122", 271 | "0:0:0:0:0:2:0:40/124", 272 | "0:0:0:0:0:2:0:50/125", 273 | "0:0:0:0:0:2:0:58/127", 274 | "1.1.1.1/32", 275 | "1.1.1.2/31", 276 | "1.1.1.4/30", 277 | "1.1.1.8/29", 278 | "1.1.1.16/28", 279 | "1.1.1.32/32", 280 | "2001:0:101:101:0:0:0:0/64", 281 | "2001:0:101:102:0:0:0:0/63", 282 | "2001:0:101:104:0:0:0:0/62", 283 | "2001:0:101:108:0:0:0:0/61", 284 | "2001:0:101:110:0:0:0:0/60", 285 | "2001:0:101:120:0:0:0:0/64", 286 | "2002:101:101:0:0:0:0:0/48", 287 | "2002:101:102:0:0:0:0:0/47", 288 | "2002:101:104:0:0:0:0:0/46", 289 | "2002:101:108:0:0:0:0:0/45", 290 | "2002:101:110:0:0:0:0:0/44", 291 | "2002:101:120:0:0:0:0:0/48", 292 | } 293 | ), 294 | new networkTest( 295 | "::", 296 | 0, 297 | "mixed", 298 | new String[]{ 299 | "1.1.1.1/32", 300 | "1.1.1.2/31", 301 | "1.1.1.4/30", 302 | "1.1.1.8/29", 303 | "1.1.1.16/28", 304 | "1.1.1.32/32", 305 | "0:0:0:0:0:1:ffff:ffff/128", 306 | "0:0:0:0:0:2:0:0/122", 307 | "0:0:0:0:0:2:0:40/124", 308 | "0:0:0:0:0:2:0:50/125", 309 | "0:0:0:0:0:2:0:58/127", 310 | }, 311 | true 312 | ), 313 | new networkTest( 314 | "1.1.1.16", 315 | 28, 316 | "mixed", 317 | new String[]{ 318 | "1.1.1.16/28" 319 | } 320 | ), 321 | new networkTest( 322 | "1.1.1.4", 323 | 30, 324 | "ipv4", 325 | new String[]{ 326 | "1.1.1.4/30" 327 | } 328 | ) 329 | }; 330 | 331 | @Test 332 | public void testNetworksWithin() throws IOException, InvalidNetworkException{ 333 | for(networkTest test : tests){ 334 | for(int recordSize : new int[]{24, 28, 32}){ 335 | File file = getFile("MaxMind-DB-test-"+test.database+"-"+recordSize+".mmdb"); 336 | Reader reader = new Reader(file); 337 | 338 | InetAddress address = InetAddress.getByName(test.network); 339 | Network network = new Network(address, test.prefix); 340 | 341 | boolean includeAliasedNetworks = !test.skipAliasedNetworks; 342 | var networks = reader.networksWithin(network, includeAliasedNetworks, Map.class); 343 | 344 | List innerIPs = new ArrayList<>(); 345 | while(networks.hasNext()){ 346 | var iteration = networks.next(); 347 | innerIPs.add(iteration.getNetwork().toString()); 348 | } 349 | 350 | assertArrayEquals(test.expected, innerIPs.toArray()); 351 | 352 | reader.close(); 353 | } 354 | } 355 | } 356 | 357 | private networkTest[] geoipTests = new networkTest[]{ 358 | new networkTest( 359 | "81.2.69.128", 360 | 26, 361 | "GeoIP2-Country-Test.mmdb", 362 | new String[]{ 363 | "81.2.69.142/31", 364 | "81.2.69.144/28", 365 | "81.2.69.160/27", 366 | } 367 | ) 368 | }; 369 | 370 | @Test 371 | public void testGeoIPNetworksWithin() throws IOException, InvalidNetworkException{ 372 | for (networkTest test : geoipTests){ 373 | File file = getFile(test.database); 374 | Reader reader = new Reader(file); 375 | 376 | InetAddress address = InetAddress.getByName(test.network); 377 | Network network = new Network(address, test.prefix); 378 | 379 | var networks = reader.networksWithin(network, test.skipAliasedNetworks, Map.class); 380 | 381 | ArrayList innerIPs = new ArrayList<>(); 382 | while(networks.hasNext()){ 383 | var iteration = networks.next(); 384 | innerIPs.add(iteration.getNetwork().toString()); 385 | } 386 | 387 | assertArrayEquals(test.expected, innerIPs.toArray()); 388 | 389 | reader.close(); 390 | } 391 | } 392 | 393 | @Test 394 | public void testGetRecord() throws IOException { 395 | GetRecordTest[] mapTests = { 396 | new GetRecordTest("1.1.1.1", "MaxMind-DB-test-ipv6-32.mmdb", "1.0.0.0/8", false), 397 | new GetRecordTest("::1:ffff:ffff", "MaxMind-DB-test-ipv6-24.mmdb", 398 | "0:0:0:0:0:1:ffff:ffff/128", true), 399 | new GetRecordTest("::2:0:1", "MaxMind-DB-test-ipv6-24.mmdb", "0:0:0:0:0:2:0:0/122", 400 | true), 401 | new GetRecordTest("1.1.1.1", "MaxMind-DB-test-ipv4-24.mmdb", "1.1.1.1/32", true), 402 | new GetRecordTest("1.1.1.3", "MaxMind-DB-test-ipv4-24.mmdb", "1.1.1.2/31", true), 403 | new GetRecordTest("1.1.1.3", "MaxMind-DB-test-decoder.mmdb", "1.1.1.0/24", true), 404 | new GetRecordTest("::ffff:1.1.1.128", "MaxMind-DB-test-decoder.mmdb", "1.1.1.0/24", 405 | true), 406 | new GetRecordTest("::1.1.1.128", "MaxMind-DB-test-decoder.mmdb", 407 | "0:0:0:0:0:0:101:100/120", true), 408 | }; 409 | for (GetRecordTest test : mapTests) { 410 | try (Reader reader = new Reader(test.db)) { 411 | DatabaseRecord record = reader.getRecord(test.ip, Map.class); 412 | 413 | assertEquals(test.network, record.getNetwork().toString()); 414 | 415 | if (test.hasRecord) { 416 | assertNotNull(record.getData()); 417 | } else { 418 | assertNull(record.getData()); 419 | } 420 | } 421 | } 422 | 423 | GetRecordTest[] stringTests = { 424 | new GetRecordTest("200.0.2.1", "MaxMind-DB-no-ipv4-search-tree.mmdb", "0.0.0.0/0", 425 | true), 426 | new GetRecordTest("::200.0.2.1", "MaxMind-DB-no-ipv4-search-tree.mmdb", 427 | "0:0:0:0:0:0:0:0/64", true), 428 | new GetRecordTest("0:0:0:0:ffff:ffff:ffff:ffff", "MaxMind-DB-no-ipv4-search-tree.mmdb", 429 | "0:0:0:0:0:0:0:0/64", true), 430 | new GetRecordTest("ef00::", "MaxMind-DB-no-ipv4-search-tree.mmdb", 431 | "8000:0:0:0:0:0:0:0/1", false) 432 | }; 433 | for (GetRecordTest test : stringTests) { 434 | try (Reader reader = new Reader(test.db)) { 435 | var record = reader.getRecord(test.ip, String.class); 436 | 437 | assertEquals(test.network, record.getNetwork().toString()); 438 | 439 | if (test.hasRecord) { 440 | assertNotNull(record.getData()); 441 | } else { 442 | assertNull(record.getData()); 443 | } 444 | } 445 | } 446 | } 447 | 448 | @Test 449 | public void testMetadataPointers() throws IOException { 450 | Reader reader = new Reader(getFile("MaxMind-DB-test-metadata-pointers.mmdb")); 451 | assertEquals("Lots of pointers in metadata", reader.getMetadata().getDatabaseType()); 452 | } 453 | 454 | @Test 455 | public void testNoIpV4SearchTreeFile() throws IOException { 456 | this.testReader = new Reader(getFile("MaxMind-DB-no-ipv4-search-tree.mmdb")); 457 | this.testNoIpV4SearchTree(this.testReader); 458 | } 459 | 460 | @Test 461 | public void testNoIpV4SearchTreeStream() throws IOException { 462 | this.testReader = new Reader(getStream("MaxMind-DB-no-ipv4-search-tree.mmdb")); 463 | this.testNoIpV4SearchTree(this.testReader); 464 | } 465 | 466 | private void testNoIpV4SearchTree(Reader reader) throws IOException { 467 | 468 | assertEquals("::0/64", reader.get(InetAddress.getByName("1.1.1.1"), String.class)); 469 | assertEquals("::0/64", reader.get(InetAddress.getByName("192.1.1.1"), String.class)); 470 | } 471 | 472 | @Test 473 | public void testDecodingTypesFile() throws IOException { 474 | this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 475 | this.testDecodingTypes(this.testReader, true); 476 | this.testDecodingTypesIntoModelObject(this.testReader, true); 477 | this.testDecodingTypesIntoModelObjectBoxed(this.testReader, true); 478 | this.testDecodingTypesIntoModelWithList(this.testReader); 479 | } 480 | 481 | @Test 482 | public void testDecodingTypesStream() throws IOException { 483 | this.testReader = new Reader(getStream("MaxMind-DB-test-decoder.mmdb")); 484 | this.testDecodingTypes(this.testReader, true); 485 | this.testDecodingTypesIntoModelObject(this.testReader, true); 486 | this.testDecodingTypesIntoModelObjectBoxed(this.testReader, true); 487 | this.testDecodingTypesIntoModelWithList(this.testReader); 488 | } 489 | 490 | @Test 491 | public void testDecodingTypesPointerDecoderFile() throws IOException { 492 | this.testReader = new Reader(getFile("MaxMind-DB-test-pointer-decoder.mmdb")); 493 | this.testDecodingTypes(this.testReader, false); 494 | this.testDecodingTypesIntoModelObject(this.testReader, false); 495 | this.testDecodingTypesIntoModelObjectBoxed(this.testReader, false); 496 | this.testDecodingTypesIntoModelWithList(this.testReader); 497 | } 498 | 499 | private void testDecodingTypes(Reader reader, boolean booleanValue) throws IOException { 500 | var record = reader.get(InetAddress.getByName("::1.1.1.0"), Map.class); 501 | 502 | if (booleanValue) { 503 | assertTrue((boolean) record.get("boolean")); 504 | } else { 505 | assertFalse((boolean) record.get("boolean")); 506 | } 507 | 508 | assertArrayEquals(new byte[] {0, 0, 0, (byte) 42}, (byte[]) record 509 | .get("bytes")); 510 | 511 | assertEquals("unicode! ☯ - ♫", record.get("utf8_string")); 512 | 513 | var array = (List) record.get("array"); 514 | assertEquals(3, array.size()); 515 | assertEquals(1, (long) array.get(0)); 516 | assertEquals(2, (long) array.get(1)); 517 | assertEquals(3, (long) array.get(2)); 518 | 519 | var map = (Map) record.get("map"); 520 | assertEquals(1, map.size()); 521 | 522 | var mapX = (Map) map.get("mapX"); 523 | assertEquals(2, mapX.size()); 524 | 525 | var arrayX = (List) mapX.get("arrayX"); 526 | assertEquals(3, arrayX.size()); 527 | assertEquals(7, (long) arrayX.get(0)); 528 | assertEquals(8, (long) arrayX.get(1)); 529 | assertEquals(9, (long) arrayX.get(2)); 530 | 531 | assertEquals("hello", mapX.get("utf8_stringX")); 532 | 533 | assertEquals(42.123456, (double) record.get("double"), 0.000000001); 534 | assertEquals(1.1, (float) record.get("float"), 0.000001); 535 | assertEquals(-268435456, (int) record.get("int32")); 536 | assertEquals(100, (int) record.get("uint16")); 537 | assertEquals(268435456, (long) record.get("uint32")); 538 | assertEquals(new BigInteger("1152921504606846976"), record 539 | .get("uint64")); 540 | assertEquals(new BigInteger("1329227995784915872903807060280344576"), 541 | record.get("uint128")); 542 | } 543 | 544 | private void testDecodingTypesIntoModelObject(Reader reader, boolean booleanValue) 545 | throws IOException { 546 | TestModel model = reader.get(InetAddress.getByName("::1.1.1.0"), TestModel.class); 547 | 548 | if (booleanValue) { 549 | assertTrue(model.booleanField); 550 | } else { 551 | assertFalse(model.booleanField); 552 | } 553 | 554 | assertArrayEquals(new byte[] {0, 0, 0, (byte) 42}, model.bytesField); 555 | 556 | assertEquals("unicode! ☯ - ♫", model.utf8StringField); 557 | 558 | List expectedArray = new ArrayList<>(Arrays.asList( 559 | (long) 1, (long) 2, (long) 3 560 | )); 561 | assertEquals(expectedArray, model.arrayField); 562 | 563 | List expectedArray2 = new ArrayList<>(Arrays.asList( 564 | (long) 7, (long) 8, (long) 9 565 | )); 566 | assertEquals(expectedArray2, model.mapField.mapXField.arrayXField); 567 | 568 | assertEquals("hello", model.mapField.mapXField.utf8StringXField); 569 | 570 | assertEquals(42.123456, model.doubleField, 0.000000001); 571 | assertEquals(1.1, model.floatField, 0.000001); 572 | assertEquals(-268435456, model.int32Field); 573 | assertEquals(100, model.uint16Field); 574 | assertEquals(268435456, model.uint32Field); 575 | assertEquals(new BigInteger("1152921504606846976"), model.uint64Field); 576 | assertEquals(new BigInteger("1329227995784915872903807060280344576"), 577 | model.uint128Field); 578 | } 579 | 580 | static class TestModel { 581 | boolean booleanField; 582 | byte[] bytesField; 583 | String utf8StringField; 584 | List arrayField; 585 | MapModel mapField; 586 | double doubleField; 587 | float floatField; 588 | int int32Field; 589 | int uint16Field; 590 | long uint32Field; 591 | BigInteger uint64Field; 592 | BigInteger uint128Field; 593 | 594 | @MaxMindDbConstructor 595 | public TestModel( 596 | @MaxMindDbParameter(name = "boolean") 597 | boolean booleanField, 598 | @MaxMindDbParameter(name = "bytes") 599 | byte[] bytesField, 600 | @MaxMindDbParameter(name = "utf8_string") 601 | String utf8StringField, 602 | @MaxMindDbParameter(name = "array") 603 | List arrayField, 604 | @MaxMindDbParameter(name = "map") 605 | MapModel mapField, 606 | @MaxMindDbParameter(name = "double") 607 | double doubleField, 608 | @MaxMindDbParameter(name = "float") 609 | float floatField, 610 | @MaxMindDbParameter(name = "int32") 611 | int int32Field, 612 | @MaxMindDbParameter(name = "uint16") 613 | int uint16Field, 614 | @MaxMindDbParameter(name = "uint32") 615 | long uint32Field, 616 | @MaxMindDbParameter(name = "uint64") 617 | BigInteger uint64Field, 618 | @MaxMindDbParameter(name = "uint128") 619 | BigInteger uint128Field 620 | ) { 621 | this.booleanField = booleanField; 622 | this.bytesField = bytesField; 623 | this.utf8StringField = utf8StringField; 624 | this.arrayField = arrayField; 625 | this.mapField = mapField; 626 | this.doubleField = doubleField; 627 | this.floatField = floatField; 628 | this.int32Field = int32Field; 629 | this.uint16Field = uint16Field; 630 | this.uint32Field = uint32Field; 631 | this.uint64Field = uint64Field; 632 | this.uint128Field = uint128Field; 633 | } 634 | } 635 | 636 | static class MapModel { 637 | MapXModel mapXField; 638 | 639 | @MaxMindDbConstructor 640 | public MapModel( 641 | @MaxMindDbParameter(name = "mapX") 642 | MapXModel mapXField 643 | ) { 644 | this.mapXField = mapXField; 645 | } 646 | } 647 | 648 | static class MapXModel { 649 | List arrayXField; 650 | String utf8StringXField; 651 | 652 | @MaxMindDbConstructor 653 | public MapXModel( 654 | @MaxMindDbParameter(name = "arrayX") 655 | List arrayXField, 656 | @MaxMindDbParameter(name = "utf8_stringX") 657 | String utf8StringXField 658 | ) { 659 | this.arrayXField = arrayXField; 660 | this.utf8StringXField = utf8StringXField; 661 | } 662 | } 663 | 664 | private void testDecodingTypesIntoModelObjectBoxed(Reader reader, boolean booleanValue) 665 | throws IOException { 666 | TestModelBoxed model = reader.get(InetAddress.getByName("::1.1.1.0"), TestModelBoxed.class); 667 | 668 | if (booleanValue) { 669 | assertTrue(model.booleanField); 670 | } else { 671 | assertFalse(model.booleanField); 672 | } 673 | 674 | assertArrayEquals(new byte[] {0, 0, 0, (byte) 42}, model.bytesField); 675 | 676 | assertEquals("unicode! ☯ - ♫", model.utf8StringField); 677 | 678 | List expectedArray = new ArrayList<>(Arrays.asList( 679 | (long) 1, (long) 2, (long) 3 680 | )); 681 | assertEquals(expectedArray, model.arrayField); 682 | 683 | List expectedArray2 = new ArrayList<>(Arrays.asList( 684 | (long) 7, (long) 8, (long) 9 685 | )); 686 | assertEquals(expectedArray2, model.mapField.mapXField.arrayXField); 687 | 688 | assertEquals("hello", model.mapField.mapXField.utf8StringXField); 689 | 690 | assertEquals(Double.valueOf(42.123456), model.doubleField, 0.000000001); 691 | assertEquals(Float.valueOf((float) 1.1), model.floatField, 0.000001); 692 | assertEquals(Integer.valueOf(-268435456), model.int32Field); 693 | assertEquals(Integer.valueOf(100), model.uint16Field); 694 | assertEquals(Long.valueOf(268435456), model.uint32Field); 695 | assertEquals(new BigInteger("1152921504606846976"), model.uint64Field); 696 | assertEquals(new BigInteger("1329227995784915872903807060280344576"), 697 | model.uint128Field); 698 | } 699 | 700 | static class TestModelBoxed { 701 | Boolean booleanField; 702 | byte[] bytesField; 703 | String utf8StringField; 704 | List arrayField; 705 | MapModelBoxed mapField; 706 | Double doubleField; 707 | Float floatField; 708 | Integer int32Field; 709 | Integer uint16Field; 710 | Long uint32Field; 711 | BigInteger uint64Field; 712 | BigInteger uint128Field; 713 | 714 | @MaxMindDbConstructor 715 | public TestModelBoxed( 716 | @MaxMindDbParameter(name = "boolean") 717 | Boolean booleanField, 718 | @MaxMindDbParameter(name = "bytes") 719 | byte[] bytesField, 720 | @MaxMindDbParameter(name = "utf8_string") 721 | String utf8StringField, 722 | @MaxMindDbParameter(name = "array") 723 | List arrayField, 724 | @MaxMindDbParameter(name = "map") 725 | MapModelBoxed mapField, 726 | @MaxMindDbParameter(name = "double") 727 | Double doubleField, 728 | @MaxMindDbParameter(name = "float") 729 | Float floatField, 730 | @MaxMindDbParameter(name = "int32") 731 | Integer int32Field, 732 | @MaxMindDbParameter(name = "uint16") 733 | Integer uint16Field, 734 | @MaxMindDbParameter(name = "uint32") 735 | Long uint32Field, 736 | @MaxMindDbParameter(name = "uint64") 737 | BigInteger uint64Field, 738 | @MaxMindDbParameter(name = "uint128") 739 | BigInteger uint128Field 740 | ) { 741 | this.booleanField = booleanField; 742 | this.bytesField = bytesField; 743 | this.utf8StringField = utf8StringField; 744 | this.arrayField = arrayField; 745 | this.mapField = mapField; 746 | this.doubleField = doubleField; 747 | this.floatField = floatField; 748 | this.int32Field = int32Field; 749 | this.uint16Field = uint16Field; 750 | this.uint32Field = uint32Field; 751 | this.uint64Field = uint64Field; 752 | this.uint128Field = uint128Field; 753 | } 754 | } 755 | 756 | static class MapModelBoxed { 757 | MapXModelBoxed mapXField; 758 | 759 | @MaxMindDbConstructor 760 | public MapModelBoxed( 761 | @MaxMindDbParameter(name = "mapX") 762 | MapXModelBoxed mapXField 763 | ) { 764 | this.mapXField = mapXField; 765 | } 766 | } 767 | 768 | static class MapXModelBoxed { 769 | List arrayXField; 770 | String utf8StringXField; 771 | 772 | @MaxMindDbConstructor 773 | public MapXModelBoxed( 774 | @MaxMindDbParameter(name = "arrayX") 775 | List arrayXField, 776 | @MaxMindDbParameter(name = "utf8_stringX") 777 | String utf8StringXField 778 | ) { 779 | this.arrayXField = arrayXField; 780 | this.utf8StringXField = utf8StringXField; 781 | } 782 | } 783 | 784 | private void testDecodingTypesIntoModelWithList(Reader reader) 785 | throws IOException { 786 | TestModelList model = reader.get(InetAddress.getByName("::1.1.1.0"), TestModelList.class); 787 | 788 | assertEquals(Arrays.asList((long) 1, (long) 2, (long) 3), model.arrayField); 789 | } 790 | 791 | static class TestModelList { 792 | List arrayField; 793 | 794 | @MaxMindDbConstructor 795 | public TestModelList( 796 | @MaxMindDbParameter(name = "array") List arrayField 797 | ) { 798 | this.arrayField = arrayField; 799 | } 800 | } 801 | 802 | @Test 803 | public void testZerosFile() throws IOException { 804 | this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 805 | this.testZeros(this.testReader); 806 | this.testZerosModelObject(this.testReader); 807 | } 808 | 809 | @Test 810 | public void testZerosStream() throws IOException { 811 | this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 812 | this.testZeros(this.testReader); 813 | this.testZerosModelObject(this.testReader); 814 | } 815 | 816 | private void testZeros(Reader reader) throws IOException { 817 | var record = reader.get(InetAddress.getByName("::"), Map.class); 818 | 819 | assertFalse((boolean) record.get("boolean")); 820 | 821 | assertArrayEquals(new byte[0], (byte[]) record.get("bytes")); 822 | 823 | assertEquals("", record.get("utf8_string")); 824 | 825 | var array = (List) record.get("array"); 826 | assertEquals(0, array.size()); 827 | 828 | var map = (Map) record.get("map"); 829 | assertEquals(0, map.size()); 830 | 831 | assertEquals(0, (double) record.get("double"), 0.000000001); 832 | assertEquals(0, (float) record.get("float"), 0.000001); 833 | assertEquals(0, (int) record.get("int32")); 834 | assertEquals(0, (int) record.get("uint16")); 835 | assertEquals(0, (long) record.get("uint32")); 836 | assertEquals(BigInteger.ZERO, record.get("uint64")); 837 | assertEquals(BigInteger.ZERO, record.get("uint128")); 838 | } 839 | 840 | private void testZerosModelObject(Reader reader) throws IOException { 841 | TestModel model = reader.get(InetAddress.getByName("::"), TestModel.class); 842 | 843 | assertFalse(model.booleanField); 844 | 845 | assertArrayEquals(new byte[0], model.bytesField); 846 | 847 | assertEquals("", model.utf8StringField); 848 | 849 | List expectedArray = new ArrayList<>(); 850 | assertEquals(expectedArray, model.arrayField); 851 | 852 | assertNull(model.mapField.mapXField); 853 | 854 | assertEquals(0, model.doubleField, 0.000000001); 855 | assertEquals(0, model.floatField, 0.000001); 856 | assertEquals(0, model.int32Field); 857 | assertEquals(0, model.uint16Field); 858 | assertEquals(0, model.uint32Field); 859 | assertEquals(BigInteger.ZERO, model.uint64Field); 860 | assertEquals(BigInteger.ZERO, model.uint128Field); 861 | } 862 | 863 | @Test 864 | public void testDecodeSubdivisions() throws IOException { 865 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 866 | 867 | TestModelSubdivisions model = this.testReader.get( 868 | InetAddress.getByName("2.125.160.216"), 869 | TestModelSubdivisions.class 870 | ); 871 | 872 | assertEquals(2, model.subdivisions.size()); 873 | assertEquals("ENG", model.subdivisions.get(0).isoCode); 874 | assertEquals("WBK", model.subdivisions.get(1).isoCode); 875 | } 876 | 877 | static class TestModelSubdivisions { 878 | List subdivisions; 879 | 880 | @MaxMindDbConstructor 881 | public TestModelSubdivisions( 882 | @MaxMindDbParameter(name = "subdivisions") 883 | List subdivisions 884 | ) { 885 | this.subdivisions = subdivisions; 886 | } 887 | } 888 | 889 | static class TestModelSubdivision { 890 | String isoCode; 891 | 892 | @MaxMindDbConstructor 893 | public TestModelSubdivision( 894 | @MaxMindDbParameter(name = "iso_code") 895 | String isoCode 896 | ) { 897 | this.isoCode = isoCode; 898 | } 899 | } 900 | 901 | @Test 902 | public void testDecodeWrongTypeWithConstructorException() throws IOException { 903 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 904 | DeserializationException ex = assertThrows(DeserializationException.class, 905 | () -> this.testReader.get(InetAddress.getByName("2.125.160.216"), 906 | TestModelSubdivisionsWithUnknownException.class)); 907 | 908 | assertThat(ex.getMessage(), 909 | containsString("Error getting record for IP /2.125.160.216 - Error creating object")); 910 | } 911 | 912 | static class TestModelSubdivisionsWithUnknownException { 913 | List subdivisions; 914 | 915 | @MaxMindDbConstructor 916 | public TestModelSubdivisionsWithUnknownException( 917 | @MaxMindDbParameter(name = "subdivisions") 918 | List subdivisions 919 | ) throws Exception { 920 | throw new Exception(); 921 | } 922 | } 923 | 924 | @Test 925 | public void testDecodeWrongTypeWithWrongArguments() throws IOException { 926 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 927 | DeserializationException ex = assertThrows(DeserializationException.class, 928 | () -> this.testReader.get(InetAddress.getByName("2.125.160.216"), 929 | TestWrongModelSubdivisions.class)); 930 | assertThat(ex.getMessage(), containsString("Error getting record for IP")); 931 | } 932 | 933 | @Test 934 | public void testDecodeWithDataTypeMismatchInModel() throws IOException { 935 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 936 | DeserializationException ex = assertThrows(DeserializationException.class, 937 | () -> this.testReader.get(InetAddress.getByName("2.125.160.216"), 938 | TestDataTypeMismatchInModel.class)); 939 | assertThat(ex.getMessage(), containsString("Error getting record for IP")); 940 | assertThat(ex.getMessage(), containsString("Error creating map entry for")); 941 | assertThat(ex.getCause().getCause().getClass(), equalTo(ClassCastException.class)); 942 | } 943 | 944 | 945 | static class TestConstructorMismatchModel { 946 | @MaxMindDbConstructor 947 | public TestConstructorMismatchModel( 948 | @MaxMindDbParameter(name = "other") 949 | String other, 950 | @MaxMindDbParameter(name = "utf8_string") 951 | double utf8StringField 952 | ) { 953 | } 954 | } 955 | 956 | @Test 957 | public void testDecodeWithDataTypeMismatchInModelAndNullValue() throws IOException { 958 | this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 959 | 960 | DeserializationException ex = assertThrows(DeserializationException.class, 961 | () -> this.testReader.get( 962 | InetAddress.getByName("::1.1.1.0"), 963 | TestConstructorMismatchModel.class)); 964 | 965 | assertThat(ex.getMessage(), containsString("Error creating object of type")); 966 | assertThat(ex.getCause().getCause().getClass(), equalTo(IllegalArgumentException.class)); 967 | } 968 | 969 | static class TestWrongModelSubdivisions { 970 | List subdivisions; 971 | 972 | @MaxMindDbConstructor 973 | public TestWrongModelSubdivisions( 974 | @MaxMindDbParameter(name = "subdivisions") 975 | List subdivisions 976 | ) { 977 | this.subdivisions = subdivisions; 978 | } 979 | } 980 | 981 | static class TestWrongModelSubdivision { 982 | Integer uint16Field; 983 | 984 | @MaxMindDbConstructor 985 | public TestWrongModelSubdivision( 986 | @MaxMindDbParameter(name = "iso_code") 987 | Integer uint16Field 988 | ) { 989 | this.uint16Field = uint16Field; 990 | } 991 | } 992 | 993 | static class TestDataTypeMismatchInModel { 994 | Map location; 995 | 996 | @MaxMindDbConstructor 997 | public TestDataTypeMismatchInModel( 998 | @MaxMindDbParameter(name = "location") 999 | Map location 1000 | ) { 1001 | this.location = location; 1002 | } 1003 | } 1004 | 1005 | @Test 1006 | public void testDecodeConcurrentHashMap() throws IOException { 1007 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 1008 | 1009 | var m = this.testReader.get( 1010 | InetAddress.getByName("2.125.160.216"), 1011 | ConcurrentHashMap.class 1012 | ); 1013 | 1014 | var subdivisions = (List) m.get("subdivisions"); 1015 | 1016 | var eng = (Map) subdivisions.get(0); 1017 | 1018 | String isoCode = (String) eng.get("iso_code"); 1019 | assertEquals("ENG", isoCode); 1020 | } 1021 | 1022 | @Test 1023 | public void testDecodeVector() throws IOException { 1024 | this.testReader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 1025 | 1026 | TestModelVector model = this.testReader.get( 1027 | InetAddress.getByName("::1.1.1.0"), 1028 | TestModelVector.class 1029 | ); 1030 | 1031 | assertEquals(3, model.arrayField.size()); 1032 | assertEquals(1, (long) model.arrayField.get(0)); 1033 | assertEquals(2, (long) model.arrayField.get(1)); 1034 | assertEquals(3, (long) model.arrayField.get(2)); 1035 | } 1036 | 1037 | static class TestModelVector { 1038 | Vector arrayField; 1039 | 1040 | @MaxMindDbConstructor 1041 | public TestModelVector( 1042 | @MaxMindDbParameter(name = "array") 1043 | Vector arrayField 1044 | ) { 1045 | this.arrayField = arrayField; 1046 | } 1047 | } 1048 | 1049 | // Test that we cache differently depending on more than the offset. 1050 | @Test 1051 | public void testCacheWithDifferentModels() throws IOException { 1052 | NodeCache cache = new CHMCache(); 1053 | 1054 | this.testReader = new Reader( 1055 | getFile("MaxMind-DB-test-decoder.mmdb"), 1056 | cache 1057 | ); 1058 | 1059 | TestModelA modelA = this.testReader.get( 1060 | InetAddress.getByName("::1.1.1.0"), 1061 | TestModelA.class 1062 | ); 1063 | assertEquals("unicode! ☯ - ♫", modelA.utf8StringFieldA); 1064 | 1065 | TestModelB modelB = this.testReader.get( 1066 | InetAddress.getByName("::1.1.1.0"), 1067 | TestModelB.class 1068 | ); 1069 | assertEquals("unicode! ☯ - ♫", modelB.utf8StringFieldB); 1070 | } 1071 | 1072 | static class TestModelA { 1073 | String utf8StringFieldA; 1074 | 1075 | @MaxMindDbConstructor 1076 | public TestModelA( 1077 | @MaxMindDbParameter(name = "utf8_string") String utf8StringFieldA 1078 | ) { 1079 | this.utf8StringFieldA = utf8StringFieldA; 1080 | } 1081 | } 1082 | 1083 | static class TestModelB { 1084 | String utf8StringFieldB; 1085 | 1086 | @MaxMindDbConstructor 1087 | public TestModelB( 1088 | @MaxMindDbParameter(name = "utf8_string") String utf8StringFieldB 1089 | ) { 1090 | this.utf8StringFieldB = utf8StringFieldB; 1091 | } 1092 | } 1093 | 1094 | @Test 1095 | public void testCacheKey() { 1096 | Class cls = TestModelCacheKey.class; 1097 | 1098 | CacheKey a = new CacheKey<>(1, cls, getType(cls, 0)); 1099 | CacheKey b = new CacheKey<>(1, cls, getType(cls, 0)); 1100 | assertEquals(a, b); 1101 | 1102 | CacheKey c = new CacheKey<>(2, cls, getType(cls, 0)); 1103 | assertNotEquals(a, c); 1104 | 1105 | CacheKey d = new CacheKey<>(1, String.class, getType(cls, 0)); 1106 | assertNotEquals(a, d); 1107 | 1108 | CacheKey e = new CacheKey<>(1, cls, getType(cls, 1)); 1109 | assertNotEquals(a, e); 1110 | } 1111 | 1112 | private java.lang.reflect.Type getType(Class cls, int i) { 1113 | Constructor[] constructors = cls.getConstructors(); 1114 | Constructor constructor = null; 1115 | for (Constructor constructor2 : constructors) { 1116 | constructor = (Constructor) constructor2; 1117 | break; 1118 | } 1119 | assertNotNull(constructor); 1120 | 1121 | java.lang.reflect.Type[] types = constructor.getGenericParameterTypes(); 1122 | return types[i]; 1123 | } 1124 | 1125 | static class TestModelCacheKey { 1126 | private final List a; 1127 | private final List b; 1128 | 1129 | public TestModelCacheKey(List a, List b) { 1130 | this.a = a; 1131 | this.b = b; 1132 | } 1133 | } 1134 | 1135 | @Test 1136 | public void testBrokenDatabaseFile() throws IOException { 1137 | this.testReader = new Reader(getFile("GeoIP2-City-Test-Broken-Double-Format.mmdb")); 1138 | this.testBrokenDatabase(this.testReader); 1139 | } 1140 | 1141 | @Test 1142 | public void testBrokenDatabaseStream() throws IOException { 1143 | this.testReader = new Reader(getStream("GeoIP2-City-Test-Broken-Double-Format.mmdb")); 1144 | this.testBrokenDatabase(this.testReader); 1145 | } 1146 | 1147 | private void testBrokenDatabase(Reader reader) { 1148 | InvalidDatabaseException ex = assertThrows( 1149 | InvalidDatabaseException.class, 1150 | () -> reader.get(InetAddress.getByName("2001:220::"), Map.class)); 1151 | assertThat(ex.getMessage(), 1152 | containsString("The MaxMind DB file's data section contains bad data")); 1153 | } 1154 | 1155 | @Test 1156 | public void testBrokenSearchTreePointerFile() throws IOException { 1157 | this.testReader = new Reader(getFile("MaxMind-DB-test-broken-pointers-24.mmdb")); 1158 | this.testBrokenSearchTreePointer(this.testReader); 1159 | } 1160 | 1161 | @Test 1162 | public void testBrokenSearchTreePointerStream() throws IOException { 1163 | this.testReader = new Reader(getStream("MaxMind-DB-test-broken-pointers-24.mmdb")); 1164 | this.testBrokenSearchTreePointer(this.testReader); 1165 | } 1166 | 1167 | private void testBrokenSearchTreePointer(Reader reader) { 1168 | InvalidDatabaseException ex = assertThrows(InvalidDatabaseException.class, 1169 | () -> reader.get(InetAddress.getByName("1.1.1.32"), Map.class)); 1170 | assertThat(ex.getMessage(), containsString("The MaxMind DB file's search tree is corrupt")); 1171 | } 1172 | 1173 | @Test 1174 | public void testBrokenDataPointerFile() throws IOException { 1175 | this.testReader = new Reader(getFile("MaxMind-DB-test-broken-pointers-24.mmdb")); 1176 | this.testBrokenDataPointer(this.testReader); 1177 | } 1178 | 1179 | @Test 1180 | public void testBrokenDataPointerStream() throws IOException { 1181 | this.testReader = new Reader(getStream("MaxMind-DB-test-broken-pointers-24.mmdb")); 1182 | this.testBrokenDataPointer(this.testReader); 1183 | } 1184 | 1185 | private void testBrokenDataPointer(Reader reader) { 1186 | InvalidDatabaseException ex = assertThrows(InvalidDatabaseException.class, 1187 | () -> reader.get(InetAddress.getByName("1.1.1.16"), Map.class)); 1188 | assertThat(ex.getMessage(), 1189 | containsString("The MaxMind DB file's data section contains bad data")); 1190 | } 1191 | 1192 | @Test 1193 | public void testClosedReaderThrowsException() throws IOException { 1194 | Reader reader = new Reader(getFile("MaxMind-DB-test-decoder.mmdb")); 1195 | 1196 | reader.close(); 1197 | ClosedDatabaseException ex = assertThrows(ClosedDatabaseException.class, 1198 | () -> reader.get(InetAddress.getByName("1.1.1.16"), Map.class)); 1199 | assertEquals("The MaxMind DB has been closed.", ex.getMessage()); 1200 | } 1201 | 1202 | @Test 1203 | public void voidTestMapKeyIsString() throws IOException { 1204 | this.testReader = new Reader(getFile("GeoIP2-City-Test.mmdb")); 1205 | 1206 | DeserializationException ex = assertThrows( 1207 | DeserializationException.class, 1208 | () -> this.testReader.get( 1209 | InetAddress.getByName("2.125.160.216"), 1210 | TestModelInvalidMap.class 1211 | ) 1212 | ); 1213 | assertEquals("Error getting record for IP /2.125.160.216 - Map keys must be strings.", 1214 | ex.getMessage()); 1215 | } 1216 | 1217 | static class TestModelInvalidMap { 1218 | Map postal; 1219 | 1220 | @MaxMindDbConstructor 1221 | public TestModelInvalidMap( 1222 | @MaxMindDbParameter(name = "postal") 1223 | Map postal 1224 | ) { 1225 | this.postal = postal; 1226 | } 1227 | } 1228 | 1229 | private void testMetadata(Reader reader, int ipVersion, long recordSize) { 1230 | 1231 | Metadata metadata = reader.getMetadata(); 1232 | 1233 | assertEquals(2, metadata.getBinaryFormatMajorVersion(), "major version"); 1234 | assertEquals(0, metadata.getBinaryFormatMinorVersion()); 1235 | assertEquals(ipVersion, metadata.getIpVersion()); 1236 | assertEquals("Test", metadata.getDatabaseType()); 1237 | 1238 | List languages = new ArrayList<>(Arrays.asList("en", "zh")); 1239 | 1240 | assertEquals(languages, metadata.getLanguages()); 1241 | 1242 | Map description = new HashMap<>(); 1243 | description.put("en", "Test Database"); 1244 | description.put("zh", "Test Database Chinese"); 1245 | 1246 | assertEquals(description, metadata.getDescription()); 1247 | assertEquals(recordSize, metadata.getRecordSize()); 1248 | 1249 | Calendar cal = Calendar.getInstance(); 1250 | cal.set(2014, Calendar.JANUARY, 1); 1251 | 1252 | assertTrue(metadata.getBuildDate().compareTo(cal.getTime()) > 0); 1253 | } 1254 | 1255 | private void testIpV4(Reader reader, File file) throws IOException { 1256 | 1257 | for (int i = 0; i <= 5; i++) { 1258 | String address = "1.1.1." + (int) Math.pow(2, i); 1259 | Map data = new HashMap<>(); 1260 | data.put("ip", address); 1261 | 1262 | assertEquals( 1263 | data, 1264 | reader.get(InetAddress.getByName(address), Map.class), 1265 | "found expected data record for " + address + " in " + file 1266 | ); 1267 | } 1268 | 1269 | Map pairs = new HashMap<>(); 1270 | pairs.put("1.1.1.3", "1.1.1.2"); 1271 | pairs.put("1.1.1.5", "1.1.1.4"); 1272 | pairs.put("1.1.1.7", "1.1.1.4"); 1273 | pairs.put("1.1.1.9", "1.1.1.8"); 1274 | pairs.put("1.1.1.15", "1.1.1.8"); 1275 | pairs.put("1.1.1.17", "1.1.1.16"); 1276 | pairs.put("1.1.1.31", "1.1.1.16"); 1277 | for (String address : pairs.keySet()) { 1278 | Map data = new HashMap<>(); 1279 | data.put("ip", pairs.get(address)); 1280 | 1281 | assertEquals( 1282 | data, 1283 | reader.get(InetAddress.getByName(address), Map.class), 1284 | "found expected data record for " + address + " in " + file 1285 | ); 1286 | } 1287 | 1288 | for (String ip : new String[] {"1.1.1.33", "255.254.253.123"}) { 1289 | assertNull(reader.get(InetAddress.getByName(ip), Map.class)); 1290 | } 1291 | } 1292 | 1293 | // XXX - logic could be combined with above 1294 | private void testIpV6(Reader reader, File file) throws IOException { 1295 | String[] subnets = new String[] {"::1:ffff:ffff", "::2:0:0", 1296 | "::2:0:40", "::2:0:50", "::2:0:58"}; 1297 | 1298 | for (String address : subnets) { 1299 | Map data = new HashMap<>(); 1300 | data.put("ip", address); 1301 | 1302 | assertEquals( 1303 | data, 1304 | reader.get(InetAddress.getByName(address), Map.class), 1305 | "found expected data record for " + address + " in " + file 1306 | ); 1307 | } 1308 | 1309 | Map pairs = new HashMap<>(); 1310 | pairs.put("::2:0:1", "::2:0:0"); 1311 | pairs.put("::2:0:33", "::2:0:0"); 1312 | pairs.put("::2:0:39", "::2:0:0"); 1313 | pairs.put("::2:0:41", "::2:0:40"); 1314 | pairs.put("::2:0:49", "::2:0:40"); 1315 | pairs.put("::2:0:52", "::2:0:50"); 1316 | pairs.put("::2:0:57", "::2:0:50"); 1317 | pairs.put("::2:0:59", "::2:0:58"); 1318 | 1319 | for (String address : pairs.keySet()) { 1320 | Map data = new HashMap<>(); 1321 | data.put("ip", pairs.get(address)); 1322 | 1323 | assertEquals( 1324 | data, 1325 | reader.get(InetAddress.getByName(address), Map.class), 1326 | "found expected data record for " + address + " in " + file 1327 | ); 1328 | } 1329 | 1330 | for (String ip : new String[] {"1.1.1.33", "255.254.253.123", "89fa::"}) { 1331 | assertNull(reader.get(InetAddress.getByName(ip), Map.class)); 1332 | } 1333 | } 1334 | 1335 | static File getFile(String name) { 1336 | return new File(ReaderTest.class.getResource("/maxmind-db/test-data/" + name).getFile()); 1337 | } 1338 | 1339 | static InputStream getStream(String name) { 1340 | return ReaderTest.class.getResourceAsStream("/maxmind-db/test-data/" + name); 1341 | } 1342 | } 1343 | -------------------------------------------------------------------------------- /src/test/java/com/maxmind/db/TestDecoder.java: -------------------------------------------------------------------------------- 1 | package com.maxmind.db; 2 | 3 | import java.lang.reflect.Type; 4 | import java.nio.ByteBuffer; 5 | 6 | final class TestDecoder extends Decoder { 7 | 8 | TestDecoder(NodeCache cache, ByteBuffer buffer, long pointerBase) { 9 | super(cache, buffer, pointerBase); 10 | } 11 | 12 | @Override 13 | DecodedValue decodePointer(long pointer, Class cls, Type genericType) { 14 | // bypass cache 15 | return new DecodedValue(pointer); 16 | } 17 | 18 | } 19 | --------------------------------------------------------------------------------