maxminddb-0.28.1/.cargo_vcs_info.json0000644000000001361046102023000131110ustar { "git": { "sha1": "8e8612895c9e3e8c5d4ffec1c80ec66472e965db" }, "path_in_vcs": "" }maxminddb-0.28.1/Cargo.lock0000644000000500071046102023000110660ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "alloca" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" dependencies = [ "cc", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "criterion" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", "itertools", "num-traits", "oorandom", "page_size", "plotters", "rayon", "regex", "serde", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "env_filter" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "ipnetwork" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde_core", ] [[package]] name = "jiff-static" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "js-sys" version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "maxminddb" version = "0.28.1" dependencies = [ "criterion", "env_logger", "ipnetwork", "log", "memchr", "memmap2", "rayon", "serde", "serde_json", "simdutf8", "thiserror", ] [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "page_size" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi", ] [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simdutf8" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasm-bindgen" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" maxminddb-0.28.1/Cargo.toml0000644000000041001046102023000111020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "maxminddb" version = "0.28.1" authors = ["Gregory J. Oschwald "] build = false include = [ "/Cargo.toml", "/benches/*.rs", "/src/**/*.rs", "/README.md", "/UPGRADING.md", "/LICENSE", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Library for reading MaxMind DB format used by GeoIP2 and GeoLite2" homepage = "https://github.com/oschwald/maxminddb-rust" documentation = "https://docs.rs/maxminddb" readme = "README.md" keywords = [ "MaxMind", "GeoIP2", "GeoIP", "geolocation", "ip", ] categories = [ "database", "network-programming", ] license = "ISC" repository = "https://github.com/oschwald/maxminddb-rust" [features] default = [] mmap = ["memmap2"] simdutf8 = ["dep:simdutf8"] unsafe-str-decode = [] [lib] name = "maxminddb" path = "src/lib.rs" [[bench]] name = "common" path = "benches/common.rs" [[bench]] name = "lookup" path = "benches/lookup.rs" harness = false [[bench]] name = "serde_usage" path = "benches/serde_usage.rs" harness = false [dependencies.ipnetwork] version = "0.21.1" [dependencies.log] version = "0.4" [dependencies.memchr] version = "2.4" [dependencies.memmap2] version = "0.9.0" optional = true [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.simdutf8] version = "0.1.5" optional = true [dependencies.thiserror] version = "2.0" [dev-dependencies.criterion] version = "0.8" [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.rayon] version = "1.5" [dev-dependencies.serde_json] version = "1.0" maxminddb-0.28.1/Cargo.toml.orig000064400000000000000000000025321046102023000145500ustar 00000000000000[package] name = "maxminddb" version = "0.28.1" authors = [ "Gregory J. Oschwald " ] description = "Library for reading MaxMind DB format used by GeoIP2 and GeoLite2" readme = "README.md" keywords = ["MaxMind", "GeoIP2", "GeoIP", "geolocation", "ip"] categories = ["database", "network-programming"] homepage = "https://github.com/oschwald/maxminddb-rust" documentation = "https://docs.rs/maxminddb" repository = "https://github.com/oschwald/maxminddb-rust" license = "ISC" include = ["/Cargo.toml", "/benches/*.rs", "/src/**/*.rs", "/README.md", "/UPGRADING.md", "/LICENSE"] edition = "2021" [features] default = [] # SIMD-accelerated UTF-8 validation during string decoding simdutf8 = ["dep:simdutf8"] # Memory-mapped file access for better performance in long-running applications mmap = ["memmap2"] # Skip UTF-8 validation for maximum performance (mutually exclusive with simdutf8) unsafe-str-decode = [] [lib] name = "maxminddb" [dependencies] ipnetwork = "0.21.1" log = "0.4" serde = { version = "1.0", features = ["derive"] } memchr = "2.4" memmap2 = { version = "0.9.0", optional = true } simdutf8 = { version = "0.1.5", optional = true } thiserror = "2.0" [dev-dependencies] env_logger = "0.11" criterion = "0.8" rayon = "1.5" serde_json = "1.0" [[bench]] name = "lookup" harness = false [[bench]] name = "serde_usage" harness = false maxminddb-0.28.1/LICENSE000064400000000000000000000014041046102023000126630ustar 00000000000000ISC License Copyright (c) 2015, Gregory J. Oschwald Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. maxminddb-0.28.1/README.md000064400000000000000000000077771046102023000131600ustar 00000000000000# Rust MaxMind DB Reader [![crates.io](https://img.shields.io/crates/v/maxminddb.svg)](https://crates.io/crates/maxminddb) [![Released API docs](https://docs.rs/maxminddb/badge.svg)](http://docs.rs/maxminddb) This library reads the MaxMind DB format, including the GeoIP2 and GeoLite2 databases. ## Building To build everything: ``` cargo build ``` ## Testing This crate manages its test data within a git submodule. To run the tests, you will first need to run the following command. ```bash git submodule update --init ``` ## Usage Add this to your `Cargo.toml`: ```toml [dependencies] maxminddb = "0.28" ``` Enable optional features as needed: ```toml [dependencies] maxminddb = { version = "0.28", features = ["mmap"] } ``` ## Example ```rust use maxminddb::{geoip2, path, Reader}; use std::net::IpAddr; fn main() -> Result<(), Box> { let reader = Reader::open_readfile("/path/to/GeoLite2-City.mmdb")?; let ip: IpAddr = "89.160.20.128".parse()?; let result = reader.lookup(ip)?; println!("Network: {}", result.network()?); if let Some(city) = result.decode::()? { println!("Country: {}", city.country.iso_code.unwrap_or("N/A")); println!("City: {}", city.city.names.english.unwrap_or("N/A")); } let iso_code: Option<&str> = result.decode_path(&path!["country", "iso_code"])?; println!("Country code via decode_path: {}", iso_code.unwrap_or("N/A")); Ok(()) } ``` `lookup()` returns a lightweight `LookupResult` handle. You can: - Check whether a record exists with `has_data()` - Read the matched network with `network()` - Decode the full record with `decode()` - Decode one field with `decode_path()` - Reuse `offset()` as a cache key when many IPs share the same record ## Iterating networks Use `within()` to iterate over the networks contained in a CIDR range, or `networks()` to iterate over the whole database. The example below uses the [`ipnetwork`](https://crates.io/crates/ipnetwork) crate, which is not re-exported by `maxminddb`; add it to your own `Cargo.toml` to run this code. ```rust use ipnetwork::IpNetwork; use maxminddb::{Reader, WithinOptions}; fn main() -> Result<(), Box> { let reader = Reader::open_readfile("/path/to/GeoLite2-City.mmdb")?; let cidr: IpNetwork = "89.160.20.0/24".parse()?; let opts = WithinOptions::default().skip_empty_values(); for result in reader.within(cidr, opts)? { let lookup = result?; println!("{}", lookup.network()?); } Ok(()) } ``` See the [examples](examples/) directory for runnable programs, including: - `cargo run --example lookup -- ` - `cargo run --example within -- ` ## Features Optional features: - **`mmap`**: Memory-mapped file access for long-running applications - **`simdutf8`**: SIMD-accelerated UTF-8 validation - **`unsafe-str-decode`**: Skip UTF-8 validation (requires trusted data) Enable in `Cargo.toml`: ```toml [dependencies] maxminddb = { version = "0.28", features = ["mmap"] } ``` Note: `simdutf8` and `unsafe-str-decode` are mutually exclusive. ## Documentation [API documentation on docs.rs](https://docs.rs/maxminddb) ## Benchmarks The project includes benchmarks using [Criterion.rs](https://github.com/bheisler/criterion.rs). First you need to have a working copy of the GeoIP City database. You can fetch it from [here](https://dev.maxmind.com/geoip/geoip2/geolite2/). Place it in the root folder as `GeoIP2-City.mmdb`. Once this is done, run ``` cargo bench ``` Two focused benchmarks are especially useful while iterating on changes: ```bash cargo bench --bench lookup cargo bench --bench serde_usage ``` If [gnuplot](http://www.gnuplot.info/) is installed, Criterion.rs can generate an HTML report displaying the results of the benchmark under `target/criterion/report/index.html`. ## Contributing Contributions welcome! Please fork the repository and open a pull request with your changes. ## License This is free software, licensed under the ISC license. maxminddb-0.28.1/UPGRADING.md000064400000000000000000000105171046102023000135250ustar 00000000000000# Upgrading Guide ## 0.26 to 0.27 This release includes significant API changes to improve ergonomics and enable new features like lazy decoding and selective field access. ### Lookup API The `lookup()` method now returns a `LookupResult` that supports lazy decoding. **Before (0.26):** ```rust let city: Option = reader.lookup(ip)?; if let Some(city) = city { println!("{:?}", city.city); } ``` **After (0.27):** ```rust let result = reader.lookup(ip)?; if let Some(city) = result.decode::()? { println!("{:?}", city.city); } ``` The new API allows you to: - Check if data exists without decoding: `result.has_data()` - Get the network for the IP: `result.network()?` - Decode only specific fields: `result.decode_path(&[...])?` ### lookup_prefix Removal The `lookup_prefix()` method has been removed. Use `lookup()` with `network()`. **Before (0.26):** ```rust let (city, prefix_len) = reader.lookup_prefix(ip)?; ``` **After (0.27):** ```rust let result = reader.lookup(ip)?; let city = result.decode::()?; let network = result.network()?; // Returns IpNetwork with prefix ``` ### Within Iterator The `within()` method now requires a `WithinOptions` parameter. **Before (0.26):** ```rust for item in reader.within::(cidr)? { let item = item?; println!("{}: {:?}", item.ip_net, item.info); } ``` **After (0.27):** ```rust use maxminddb::WithinOptions; for result in reader.within(cidr, Default::default())? { let result = result?; let network = result.network()?; if let Some(city) = result.decode::()? { println!("{}: {:?}", network, city); } } ``` To customize iteration behavior: ```rust let options = WithinOptions::default() .include_aliased_networks() // Include IPv4 via IPv6 aliases .include_networks_without_data() // Include networks without data .skip_empty_values(); // Skip empty maps/arrays for result in reader.within(cidr, options)? { // ... } ``` ### GeoIP2 Name Fields The `names` fields now use a `Names` struct instead of `BTreeMap`. **Before (0.26):** ```rust let name = city.city .as_ref() .and_then(|c| c.names.as_ref()) .and_then(|n| n.get("en")); ``` **After (0.27):** ```rust let name = city.city.names.english; ``` Available language fields: - `german` - `english` - `spanish` - `french` - `japanese` - `brazilian_portuguese` - `russian` - `simplified_chinese` ### GeoIP2 Nested Structs Nested struct fields are now non-optional with `Default`. **Before (0.26):** ```rust let iso_code = city.country .as_ref() .and_then(|c| c.iso_code.as_ref()); let subdivisions = city.subdivisions .as_ref() .map(|v| v.iter()) .into_iter() .flatten(); ``` **After (0.27):** ```rust let iso_code = city.country.iso_code; for subdivision in &city.subdivisions { // ... } ``` Leaf values (strings, numbers, bools) remain `Option`. ### Removed Trait Fields The `is_anonymous_proxy` and `is_satellite_provider` fields have been removed from `country::Traits` and `enterprise::Traits`. These fields are no longer present in MaxMind databases. For anonymity detection, use the [Anonymous IP database](https://www.maxmind.com/en/geoip2-anonymous-ip-database). ### Error Types Error variants now use structured fields. **Before (0.26):** ```rust match error { MaxMindDbError::InvalidDatabase(msg) => { println!("Invalid database: {}", msg); } // ... } ``` **After (0.27):** ```rust match error { MaxMindDbError::InvalidDatabase { message, offset } => { println!("Invalid database: {} at {:?}", message, offset); } MaxMindDbError::InvalidInput { message } => { println!("Invalid input: {}", message); } // ... } ``` The new `InvalidInput` variant is used for user errors like looking up an IPv6 address in an IPv4-only database. ### Quick Migration Checklist 1. Update `lookup()` calls to use `.decode::()?` 2. Replace `lookup_prefix()` with `lookup()` + `network()` 3. Add `Default::default()` as second argument to `within()` 4. Update `within()` loops to use `result.network()` and `result.decode()` 5. Replace `names.get("en")` with `names.english` 6. Remove `.as_ref()` chains for nested GeoIP2 fields 7. Remove references to `is_anonymous_proxy` and `is_satellite_provider` 8. Update error matching to use struct patterns maxminddb-0.28.1/benches/common.rs000064400000000000000000000017551046102023000151340ustar 00000000000000use std::net::{IpAddr, Ipv4Addr}; // Generate `count` IPv4 addresses from a deterministic LCG stream. #[must_use] pub fn generate_ipv4(count: u64) -> Vec { let mut ips = Vec::with_capacity(count as usize); let mut state = 0x4D59_5DF4_D0F3_3173_u64; for _ in 0..count { state = state .wrapping_mul(6_364_136_223_846_793_005) .wrapping_add(1_442_695_040_888_963_407); let ip = Ipv4Addr::new( (state >> 24) as u8, (state >> 32) as u8, (state >> 40) as u8, (state >> 48) as u8, ); ips.push(IpAddr::V4(ip)); } ips } pub fn open_reader(db_file: &str) -> maxminddb::Reader> { #[cfg(not(feature = "mmap"))] return maxminddb::Reader::open_readfile(db_file).unwrap(); #[cfg(feature = "mmap")] // SAFETY: The benchmark database file will not be modified during the benchmark. return unsafe { maxminddb::Reader::open_mmap(db_file) }.unwrap(); } maxminddb-0.28.1/benches/lookup.rs000064400000000000000000000030701046102023000151450ustar 00000000000000#[macro_use] extern crate criterion; extern crate maxminddb; extern crate rayon; use criterion::Criterion; use maxminddb::geoip2; use rayon::prelude::*; use std::net::IpAddr; mod common; use common::{generate_ipv4, open_reader}; // Single-threaded pub fn bench_maxminddb(ips: &[IpAddr], reader: &maxminddb::Reader) where T: AsRef<[u8]>, { for ip in ips.iter() { let result = reader.lookup(*ip).unwrap(); if result.has_data() { let _: geoip2::City = result.decode().unwrap().unwrap(); } } } // Using rayon for parallel execution pub fn bench_par_maxminddb(ips: &[IpAddr], reader: &maxminddb::Reader) where T: AsRef<[u8]> + std::marker::Sync, { ips.par_iter().for_each(|ip| { let result = reader.lookup(*ip).unwrap(); if result.has_data() { let _: geoip2::City = result.decode().unwrap().unwrap(); } }); } const DB_FILE: &str = "GeoLite2-City.mmdb"; pub fn criterion_benchmark(c: &mut Criterion) { let ips = generate_ipv4(100); let reader = open_reader(DB_FILE); c.bench_function("maxminddb", |b| b.iter(|| bench_maxminddb(&ips, &reader))); } pub fn criterion_par_benchmark(c: &mut Criterion) { let ips = generate_ipv4(100); let reader = open_reader(DB_FILE); c.bench_function("maxminddb_par", |b| { b.iter(|| bench_par_maxminddb(&ips, &reader)) }); } criterion_group! { name = benches; config = Criterion::default() .sample_size(20); targets = criterion_benchmark, criterion_par_benchmark } criterion_main!(benches); maxminddb-0.28.1/benches/serde_usage.rs000064400000000000000000000054161046102023000161300ustar 00000000000000use criterion::{criterion_group, criterion_main, Criterion}; use maxminddb::geoip2; use maxminddb::{LookupResult, PathElement, Reader}; use std::hint::black_box; use std::net::IpAddr; mod common; use common::{generate_ipv4, open_reader}; const DB_FILE: &str = "GeoLite2-City.mmdb"; fn cache_lookups<'a, T>(ips: &[IpAddr], reader: &'a Reader) -> Vec> where T: AsRef<[u8]>, { ips.iter() .map(|ip| reader.lookup(*ip).unwrap()) .filter(|r| r.has_data()) .collect() } fn bench_lookup_only(ips: &[IpAddr], reader: &Reader) where T: AsRef<[u8]>, { for ip in ips { let result = reader.lookup(*ip).unwrap(); black_box(result.has_data()); } } fn bench_decode_city_only(results: &[LookupResult<'_, T>]) where T: AsRef<[u8]>, { for result in results { let city: Option> = result.decode().unwrap(); black_box(city); } } fn bench_decode_country_only(results: &[LookupResult<'_, T>]) where T: AsRef<[u8]>, { for result in results { let country: Option> = result.decode().unwrap(); black_box(country); } } fn bench_decode_path_country_iso(results: &[LookupResult<'_, T>]) where T: AsRef<[u8]>, { let path = [PathElement::Key("country"), PathElement::Key("iso_code")]; for result in results { let value: Option<&str> = result.decode_path(&path).unwrap(); black_box(value); } } fn bench_decode_path_city_name(results: &[LookupResult<'_, T>]) where T: AsRef<[u8]>, { let path = [ PathElement::Key("city"), PathElement::Key("names"), PathElement::Key("en"), ]; for result in results { let value: Option<&str> = result.decode_path(&path).unwrap(); black_box(value); } } pub fn serde_usage_benchmark(c: &mut Criterion) { let ips = generate_ipv4(100); let reader = open_reader(DB_FILE); let cached_results = cache_lookups(&ips, &reader); c.bench_function("serde_usage/lookup_only", |b| { b.iter(|| bench_lookup_only(&ips, &reader)) }); c.bench_function("serde_usage/decode_city_only", |b| { b.iter(|| bench_decode_city_only(&cached_results)) }); c.bench_function("serde_usage/decode_country_only", |b| { b.iter(|| bench_decode_country_only(&cached_results)) }); c.bench_function("serde_usage/decode_path_country_iso", |b| { b.iter(|| bench_decode_path_country_iso(&cached_results)) }); c.bench_function("serde_usage/decode_path_city_name", |b| { b.iter(|| bench_decode_path_city_name(&cached_results)) }); } criterion_group! { name = benches; config = Criterion::default().sample_size(20); targets = serde_usage_benchmark } criterion_main!(benches); maxminddb-0.28.1/src/decoder.rs000064400000000000000000001123641046102023000144300ustar 00000000000000//! Binary format decoder for MaxMind DB files. //! //! This module implements deserialization of the MaxMind DB binary format //! into Rust types via serde. The decoder handles all MaxMind DB data types //! including pointers, maps, arrays, and primitive types. //! //! Most users should not need to interact with this module directly. //! Use [`Reader::lookup()`](crate::Reader::lookup) for normal lookups. use log::debug; use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; use serde::forward_to_deserialize_any; use std::convert::TryInto; use crate::error::MaxMindDbError; // MaxMind DB type constants const TYPE_EXTENDED: u8 = 0; pub(crate) const TYPE_POINTER: u8 = 1; const TYPE_STRING: u8 = 2; const TYPE_DOUBLE: u8 = 3; const TYPE_BYTES: u8 = 4; const TYPE_UINT16: u8 = 5; const TYPE_UINT32: u8 = 6; pub(crate) const TYPE_MAP: u8 = 7; const TYPE_INT32: u8 = 8; const TYPE_UINT64: u8 = 9; const TYPE_UINT128: u8 = 10; pub(crate) const TYPE_ARRAY: u8 = 11; const TYPE_BOOL: u8 = 14; const TYPE_FLOAT: u8 = 15; /// Maximum recursion depth for nested data structures. /// This matches the value used in libmaxminddb and the Go reader. const MAXIMUM_DATA_STRUCTURE_DEPTH: u16 = 512; #[inline(always)] fn to_usize(base: u8, bytes: &[u8]) -> usize { bytes .iter() .fold(base as usize, |acc, &b| (acc << 8) | b as usize) } macro_rules! decode_int_like { ($name:ident, $ty:ty, $max_size:expr, $label:literal, $zero:expr) => { fn $name(&mut self, size: usize) -> DecodeResult<$ty> { match size { s if s <= $max_size => { let new_offset = self .current_ptr .checked_add(size) .filter(|&offset| offset <= self.buf.len()) .ok_or_else(|| { self.invalid_db_error(&format!("{} of size {}", $label, size)) })?; let value = self.buf[self.current_ptr..new_offset] .iter() .fold($zero, |acc, &b| (acc << 8) | <$ty>::from(b)); self.current_ptr = new_offset; Ok(value) } s => Err(self.invalid_db_error(&format!("{} of size {}", $label, s))), } } }; } macro_rules! deserialize_direct_scalar { ($name:ident, $expected_type:expr, $label:literal, $visit:ident, $decode:ident) => { fn $name(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { let (size, type_num) = self.size_and_type()?; self.decode_direct(size, type_num, $expected_type, $label, |de, size| { visitor.$visit(de.$decode(size)?) }) } }; } enum Value<'a, 'de> { Any { prev_ptr: usize }, Bytes(&'de [u8]), String(&'de str), Bool(bool), I32(i32), U16(u16), U32(u32), U64(u64), U128(u128), F64(f64), F32(f32), Map(MapAccessor<'a, 'de>), Array(ArrayAccess<'a, 'de>), } /// Decoder for MaxMind DB binary format. /// /// Implements serde's `Deserializer` trait. Handles pointer resolution, /// type coercion, and nested data structures. #[derive(Debug)] pub(crate) struct Decoder<'de> { buf: &'de [u8], current_ptr: usize, depth: u16, } impl<'de> Decoder<'de> { pub(crate) fn new(buf: &'de [u8], start_ptr: usize) -> Decoder<'de> { Decoder { buf, current_ptr: start_ptr, depth: 0, } } /// Check and increment depth, returning error if limit exceeded. #[inline] fn enter_nested(&mut self) -> DecodeResult<()> { if self.depth >= MAXIMUM_DATA_STRUCTURE_DEPTH { return Err(self.invalid_db_error( "exceeded maximum data structure depth; database is likely corrupt", )); } self.depth += 1; Ok(()) } /// Decrement depth when exiting a nested structure. #[inline] fn exit_nested(&mut self) { self.depth = self.depth.saturating_sub(1); } /// Create an InvalidDatabase error with current offset context. #[inline] fn invalid_db_error(&self, msg: &str) -> MaxMindDbError { MaxMindDbError::invalid_database_at(msg, self.current_ptr) } /// Create a Decoding error with current offset context. #[inline] fn decode_error(&self, msg: &str) -> MaxMindDbError { MaxMindDbError::decoding_at(msg, self.current_ptr) } #[inline(always)] fn type_mismatch(&self, label: &str, type_num: u8) -> MaxMindDbError { self.decode_error(&format!("expected {label}, got type {type_num}")) } #[inline] pub(crate) fn offset(&self) -> usize { self.current_ptr } #[inline(always)] fn checked_offset(&self, size: usize, label: &str) -> DecodeResult { let new_offset = self.current_ptr.wrapping_add(size); if new_offset < self.current_ptr || new_offset > self.buf.len() { return Err(self.invalid_db_error(&format!("{label} of size {size}"))); } Ok(new_offset) } #[inline(always)] fn eat_byte(&mut self) -> DecodeResult { let b = *self .buf .get(self.current_ptr) .ok_or_else(|| self.invalid_db_error("unexpected end of buffer"))?; self.current_ptr += 1; Ok(b) } #[inline(always)] fn size_from_ctrl_byte(&mut self, ctrl_byte: u8, type_num: u8) -> DecodeResult { let size = (ctrl_byte & 0x1f) as usize; // Extended type - size field is used differently if type_num == TYPE_EXTENDED { return Ok(size); } match size { s if s < 29 => Ok(s), 29 => Ok(29_usize + self.eat_byte()? as usize), 30 => { let b0 = self.eat_byte()? as usize; let b1 = self.eat_byte()? as usize; Ok(285_usize + (b0 << 8) + b1) } _ => { let b0 = self.eat_byte()? as usize; let b1 = self.eat_byte()? as usize; let b2 = self.eat_byte()? as usize; Ok(65_821_usize + (b0 << 16) + (b1 << 8) + b2) } } } #[inline(always)] fn size_and_type(&mut self) -> DecodeResult<(usize, u8)> { let ctrl_byte = self.eat_byte()?; let mut type_num = ctrl_byte >> 5; // Extended type: type 0 means read next byte for actual type if type_num == TYPE_EXTENDED { type_num = self.eat_byte()? + TYPE_MAP; // Extended types start at 7 } let size = self.size_from_ctrl_byte(ctrl_byte, type_num)?; Ok((size, type_num)) } fn decode_any>(&mut self, visitor: V) -> DecodeResult { match self.decode_any_value()? { Value::Any { prev_ptr } => { // Pointer dereference - track depth self.enter_nested()?; let res = self.decode_any(visitor); self.exit_nested(); self.current_ptr = prev_ptr; res } Value::Bool(x) => visitor.visit_bool(x), Value::Bytes(x) => visitor.visit_borrowed_bytes(x), Value::String(x) => visitor.visit_borrowed_str(x), Value::I32(x) => visitor.visit_i32(x), Value::U16(x) => visitor.visit_u16(x), Value::U32(x) => visitor.visit_u32(x), Value::U64(x) => visitor.visit_u64(x), Value::U128(x) => visitor.visit_u128(x), Value::F64(x) => visitor.visit_f64(x), Value::F32(x) => visitor.visit_f32(x), // Maps and arrays enter_nested in decode_any_value; exit when done Value::Map(x) => { let res = visitor.visit_map(x); self.exit_nested(); res } Value::Array(x) => { let res = visitor.visit_seq(x); self.exit_nested(); res } } } fn deserialize_fixed_size_array(&mut self, len: usize, visitor: V) -> DecodeResult where V: Visitor<'de>, { let (size, type_num) = self.size_and_type()?; self.decode_direct(size, type_num, TYPE_ARRAY, "array", |de, size| { if size != len { return Err(de.decode_error(&format!( "expected tuple of length {len}, got array of length {size}" ))); } de.enter_nested()?; let res = visitor.visit_seq(ArrayAccess { de, count: size }); de.exit_nested(); res }) } #[inline(always)] fn decode_any_value(&mut self) -> DecodeResult> { let (size, type_num) = self.size_and_type()?; Ok(match type_num { TYPE_POINTER => { let new_ptr = self.decode_pointer(size); let prev_ptr = self.current_ptr; self.current_ptr = new_ptr; Value::Any { prev_ptr } } TYPE_STRING => Value::String(self.decode_string(size)?), TYPE_DOUBLE => Value::F64(self.decode_double(size)?), TYPE_BYTES => Value::Bytes(self.decode_bytes(size)?), TYPE_UINT16 => Value::U16(self.decode_uint16(size)?), TYPE_UINT32 => Value::U32(self.decode_uint32(size)?), TYPE_MAP => { self.enter_nested()?; self.decode_map(size) } TYPE_INT32 => Value::I32(self.decode_int(size)?), TYPE_UINT64 => Value::U64(self.decode_uint64(size)?), TYPE_UINT128 => Value::U128(self.decode_uint128(size)?), TYPE_ARRAY => { self.enter_nested()?; self.decode_array(size) } TYPE_BOOL => Value::Bool(self.decode_bool(size)?), TYPE_FLOAT => Value::F32(self.decode_float(size)?), u => return Err(self.invalid_db_error(&format!("unknown data type: {u}"))), }) } fn decode_array(&mut self, size: usize) -> Value<'_, 'de> { Value::Array(ArrayAccess { de: self, count: size, }) } fn decode_bool(&mut self, size: usize) -> DecodeResult { match size { 0 | 1 => Ok(size != 0), s => Err(self.invalid_db_error(&format!("bool of size {s}"))), } } fn decode_bytes(&mut self, size: usize) -> DecodeResult<&'de [u8]> { let new_offset = self.checked_offset(size, "bytes")?; let u8_slice = &self.buf[self.current_ptr..new_offset]; self.current_ptr = new_offset; Ok(u8_slice) } fn decode_float(&mut self, size: usize) -> DecodeResult { let new_offset = self.checked_offset(size, "float")?; let value: [u8; 4] = self.buf[self.current_ptr..new_offset] .try_into() .map_err(|_| self.invalid_db_error(&format!("float of size {size}")))?; self.current_ptr = new_offset; let float_value = f32::from_be_bytes(value); Ok(float_value) } fn decode_double(&mut self, size: usize) -> DecodeResult { let new_offset = self.checked_offset(size, "double")?; let value: [u8; 8] = self.buf[self.current_ptr..new_offset] .try_into() .map_err(|_| self.invalid_db_error(&format!("double of size {size}")))?; self.current_ptr = new_offset; let float_value = f64::from_be_bytes(value); Ok(float_value) } decode_int_like!(decode_uint64, u64, 8, "u64", 0_u64); decode_int_like!(decode_uint128, u128, 16, "u128", 0_u128); #[inline(always)] fn read_u32_be(&mut self, size: usize, label: &str) -> DecodeResult { if size > 4 { return Err(self.invalid_db_error(&format!("{label} of size {size}"))); } let new_offset = self .current_ptr .checked_add(size) .filter(|&offset| offset <= self.buf.len()) .ok_or_else(|| self.invalid_db_error(&format!("{label} of size {}", size)))?; let p = self.current_ptr; let value = match size { 0 => 0, 1 => self.buf[p] as u32, 2 => ((self.buf[p] as u32) << 8) | self.buf[p + 1] as u32, 3 => { ((self.buf[p] as u32) << 16) | ((self.buf[p + 1] as u32) << 8) | self.buf[p + 2] as u32 } _ => { ((self.buf[p] as u32) << 24) | ((self.buf[p + 1] as u32) << 16) | ((self.buf[p + 2] as u32) << 8) | self.buf[p + 3] as u32 } }; self.current_ptr = new_offset; Ok(value) } #[inline(always)] fn decode_uint32(&mut self, size: usize) -> DecodeResult { self.read_u32_be(size, "u32") } #[inline(always)] fn decode_uint16(&mut self, size: usize) -> DecodeResult { if size > 2 { return Err(self.invalid_db_error(&format!("u16 of size {size}"))); } let new_offset = self .current_ptr .checked_add(size) .filter(|&offset| offset <= self.buf.len()) .ok_or_else(|| self.invalid_db_error(&format!("u16 of size {}", size)))?; let p = self.current_ptr; let value = match size { 0 => 0, 1 => self.buf[p] as u16, _ => ((self.buf[p] as u16) << 8) | self.buf[p + 1] as u16, }; self.current_ptr = new_offset; Ok(value) } fn decode_int(&mut self, size: usize) -> DecodeResult { self.read_u32_be(size, "i32").map(|value| value as i32) } fn decode_map(&mut self, size: usize) -> Value<'_, 'de> { Value::Map(MapAccessor { de: self, count: size * 2, }) } #[inline(always)] fn decode_pointer(&mut self, size: usize) -> usize { let pointer_value_offset = [0, 0, 2048, 526_336, 0]; let pointer_size = ((size >> 3) & 0x3) + 1; let p = self.current_ptr; let len = self.buf.len(); let new_offset = match p.checked_add(pointer_size) { Some(offset) if offset <= len => offset, _ => { // Clamp to the end of the buffer so the next decode step fails // with a normal bounds error instead of panicking here. self.current_ptr = len; return len; } }; let pointer_bytes = &self.buf[p..new_offset]; self.current_ptr = new_offset; let base = if pointer_size == 4 { 0 } else { (size & 0x7) as u8 }; let unpacked = to_usize(base, pointer_bytes); unpacked + pointer_value_offset[pointer_size] } #[cfg(feature = "unsafe-str-decode")] fn decode_string(&mut self, size: usize) -> DecodeResult<&'de str> { use std::str::from_utf8_unchecked; let new_offset = self.checked_offset(size, "string")?; let bytes = &self.buf[self.current_ptr..new_offset]; self.current_ptr = new_offset; // SAFETY: // A corrupt maxminddb will cause undefined behaviour. // If the caller has verified the integrity of their database and trusts their upstream // provider, they can opt-into the performance gains provided by this unsafe function via // the `unsafe-str-decode` feature flag. // This can provide around 20% performance increase in the lookup benchmark. let v = unsafe { from_utf8_unchecked(bytes) }; Ok(v) } #[cfg(not(feature = "unsafe-str-decode"))] fn decode_string(&mut self, size: usize) -> DecodeResult<&'de str> { #[cfg(feature = "simdutf8")] use simdutf8::basic::from_utf8; #[cfg(not(feature = "simdutf8"))] use std::str::from_utf8; use std::str::from_utf8_unchecked; let new_offset = self.checked_offset(size, "string")?; let bytes = &self.buf[self.current_ptr..new_offset]; self.current_ptr = new_offset; if bytes.is_ascii() { // ASCII is valid UTF-8, so this avoids the full validator fast path. // SAFETY: `is_ascii()` guarantees UTF-8 validity. let v = unsafe { from_utf8_unchecked(bytes) }; return Ok(v); } match from_utf8(bytes) { Ok(v) => Ok(v), Err(_) => Err(self.invalid_db_error("invalid UTF-8 in string")), } } // ========== Navigation methods for path decoding and verification ========== /// Peeks at the type and size without consuming it. /// Returns (size, type_num) and restores the position. pub(crate) fn peek_type(&mut self) -> DecodeResult<(usize, u8)> { let saved_ptr = self.current_ptr; let result = self.size_and_type_following_pointers()?; self.current_ptr = saved_ptr; Ok(result) } /// Consumes a map header, returning its size. Follows pointers. pub(crate) fn consume_map_header(&mut self) -> DecodeResult { self.consume_typed_header(TYPE_MAP, "map") } /// Consumes an array header, returning its size. Follows pointers. pub(crate) fn consume_array_header(&mut self) -> DecodeResult { self.consume_typed_header(TYPE_ARRAY, "array") } /// Consumes a header of the expected type, following one pointer. fn consume_typed_header(&mut self, expected_type: u8, label: &str) -> DecodeResult { let (size, type_num) = self.size_and_type()?; if type_num == TYPE_POINTER { self.current_ptr = self.decode_pointer(size); let (size, type_num) = self.size_and_type()?; if type_num == TYPE_POINTER { return Err(self.invalid_db_error("pointer points to another pointer")); } if type_num == expected_type { return Ok(size); } return Err(self.decode_error(&format!("expected {label}, got type {type_num}"))); } if type_num == expected_type { Ok(size) } else { Err(self.decode_error(&format!("expected {label}, got type {type_num}"))) } } /// Gets size and type, following any pointers. fn size_and_type_following_pointers(&mut self) -> DecodeResult<(usize, u8)> { let (size, type_num) = self.size_and_type()?; if type_num != TYPE_POINTER { return Ok((size, type_num)); } self.current_ptr = self.decode_pointer(size); let (size, type_num) = self.size_and_type()?; if type_num == TYPE_POINTER { return Err(self.invalid_db_error("pointer points to another pointer")); } Ok((size, type_num)) } #[inline(always)] fn decode_direct( &mut self, size: usize, type_num: u8, expected_type: u8, label: &str, decode: F, ) -> DecodeResult where F: FnOnce(&mut Self, usize) -> DecodeResult, { match type_num { TYPE_POINTER => { let new_ptr = self.decode_pointer(size); let saved_ptr = self.current_ptr; self.current_ptr = new_ptr; self.enter_nested()?; let result = (|| { let (size, type_num) = self.size_and_type()?; if type_num == TYPE_POINTER { return Err(self.invalid_db_error("pointer points to another pointer")); } if type_num != expected_type { return Err(self.type_mismatch(label, type_num)); } decode(self, size) })(); self.exit_nested(); self.current_ptr = saved_ptr; result } t if t == expected_type => decode(self, size), _ => Err(self.type_mismatch(label, type_num)), } } #[inline(always)] fn read_string_bytes(&mut self, size: usize) -> DecodeResult<&'de [u8]> { let new_offset = self .current_ptr .checked_add(size) .ok_or_else(|| self.invalid_db_error("string length exceeds buffer"))?; if new_offset > self.buf.len() { return Err(self.invalid_db_error("string length exceeds buffer")); } let bytes = &self.buf[self.current_ptr..new_offset]; self.current_ptr = new_offset; Ok(bytes) } /// Reads a string's bytes directly, following pointers if needed. /// Does NOT validate UTF-8. pub(crate) fn read_str_as_bytes(&mut self) -> DecodeResult<&'de [u8]> { let (size, type_num) = self.size_and_type()?; match type_num { TYPE_POINTER => { let new_ptr = self.decode_pointer(size); let saved_ptr = self.current_ptr; self.current_ptr = new_ptr; let (size, type_num) = self.size_and_type()?; let result = if type_num == TYPE_POINTER { Err(self.invalid_db_error("pointer points to another pointer")) } else if type_num == TYPE_STRING { self.read_string_bytes(size) } else { Err(self.invalid_db_error(&format!("expected string, got type {type_num}"))) }; self.current_ptr = saved_ptr; result } TYPE_STRING => self.read_string_bytes(size), _ => Err(self.invalid_db_error(&format!("expected string, got type {type_num}"))), } } /// Fast-path identifier decoding: /// - Returns `Ok(Some(bytes))` and consumes the identifier when it is a string. /// - Returns `Ok(None)` and restores `current_ptr` when the next value is not a string. /// - Returns `Err` for malformed pointer chains or invalid string lengths. fn try_read_identifier_bytes(&mut self) -> DecodeResult> { let saved_ptr = self.current_ptr; let (size, type_num) = self.size_and_type()?; match type_num { TYPE_STRING => self.read_string_bytes(size).map(Some), TYPE_POINTER => { let new_ptr = self.decode_pointer(size); let after_pointer = self.current_ptr; self.current_ptr = new_ptr; let (inner_size, inner_type) = self.size_and_type()?; let result = if inner_type == TYPE_POINTER { Err(self.invalid_db_error("pointer points to another pointer")) } else if inner_type == TYPE_STRING { self.read_string_bytes(inner_size).map(Some) } else { Ok(None) }; // decode_pointer(size) temporarily dereferences by moving current_ptr // to new_ptr; after size_and_type/read_string_bytes on the pointed // value, restoring current_ptr = after_pointer resumes parsing right // after the original pointer bytes. When result is Ok(None), also // reset current_ptr = saved_ptr so the non-string identifier can be // parsed normally by the caller without consuming the pointer token. self.current_ptr = after_pointer; if matches!(result, Ok(None)) { self.current_ptr = saved_ptr; } result } _ => { self.current_ptr = saved_ptr; Ok(None) } } } /// Skips the current value, following pointers. pub(crate) fn skip_value(&mut self) -> DecodeResult<()> { let (size, type_num) = self.size_and_type()?; self.skip_value_inner(size, type_num, true) } /// Skips the current value without following pointers (for verification). pub(crate) fn skip_value_for_verification(&mut self) -> DecodeResult<()> { let (size, type_num) = self.size_and_type()?; self.skip_value_inner(size, type_num, false) } fn skip_value_inner( &mut self, size: usize, type_num: u8, follow_pointers: bool, ) -> DecodeResult<()> { match type_num { TYPE_POINTER => { let new_ptr = self.decode_pointer(size); if follow_pointers { let saved_ptr = self.current_ptr; self.current_ptr = new_ptr; self.skip_value()?; self.current_ptr = saved_ptr; } Ok(()) } TYPE_STRING | TYPE_BYTES => { // String or Bytes - skip size bytes if follow_pointers { self.current_ptr += size; } else { let label = if type_num == TYPE_STRING { "string" } else { "bytes" }; self.current_ptr = self.checked_offset(size, label)?; } Ok(()) } TYPE_DOUBLE => { // Double - must be exactly 8 bytes if size != 8 { return Err(self.invalid_db_error(&format!("double of size {size}"))); } if follow_pointers { self.current_ptr += size; } else { self.current_ptr = self.checked_offset(size, "double")?; } Ok(()) } TYPE_FLOAT => { // Float - must be exactly 4 bytes if size != 4 { return Err(self.invalid_db_error(&format!("float of size {size}"))); } if follow_pointers { self.current_ptr += size; } else { self.current_ptr = self.checked_offset(size, "float")?; } Ok(()) } TYPE_UINT16 | TYPE_UINT32 | TYPE_INT32 | TYPE_UINT64 | TYPE_UINT128 => { // Numeric types - skip size bytes if follow_pointers { self.current_ptr += size; } else { let label = match type_num { TYPE_UINT16 => "u16", TYPE_UINT32 => "u32", TYPE_INT32 => "i32", TYPE_UINT64 => "u64", TYPE_UINT128 => "u128", _ => unreachable!(), }; self.current_ptr = self.checked_offset(size, label)?; } Ok(()) } TYPE_BOOL => { // Boolean - size field IS the value, no data bytes to skip Ok(()) } TYPE_MAP => { // Map - skip size key-value pairs for _ in 0..size { self.skip_value_inner_with_follow(follow_pointers)?; // key self.skip_value_inner_with_follow(follow_pointers)?; // value } Ok(()) } TYPE_ARRAY => { // Array - skip size elements for _ in 0..size { self.skip_value_inner_with_follow(follow_pointers)?; } Ok(()) } u => Err(self.invalid_db_error(&format!("unknown data type: {u}"))), } } fn skip_value_inner_with_follow(&mut self, follow_pointers: bool) -> DecodeResult<()> { let (size, type_num) = self.size_and_type()?; self.skip_value_inner(size, type_num, follow_pointers) } } pub type DecodeResult = Result; impl<'de: 'a, 'a> de::Deserializer<'de> for &'a mut Decoder<'de> { type Error = MaxMindDbError; fn deserialize_any(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { debug!("deserialize_any"); self.decode_any(visitor) } fn deserialize_option(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { debug!("deserialize_option"); visitor.visit_some(self) } deserialize_direct_scalar!(deserialize_bool, TYPE_BOOL, "bool", visit_bool, decode_bool); deserialize_direct_scalar!( deserialize_u16, TYPE_UINT16, "u16", visit_u16, decode_uint16 ); deserialize_direct_scalar!( deserialize_u32, TYPE_UINT32, "u32", visit_u32, decode_uint32 ); deserialize_direct_scalar!( deserialize_u64, TYPE_UINT64, "u64", visit_u64, decode_uint64 ); deserialize_direct_scalar!( deserialize_u128, TYPE_UINT128, "u128", visit_u128, decode_uint128 ); deserialize_direct_scalar!(deserialize_i32, TYPE_INT32, "i32", visit_i32, decode_int); deserialize_direct_scalar!( deserialize_f32, TYPE_FLOAT, "float", visit_f32, decode_float ); deserialize_direct_scalar!( deserialize_f64, TYPE_DOUBLE, "double", visit_f64, decode_double ); deserialize_direct_scalar!( deserialize_str, TYPE_STRING, "string", visit_borrowed_str, decode_string ); fn deserialize_string(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { self.deserialize_str(visitor) } deserialize_direct_scalar!( deserialize_bytes, TYPE_BYTES, "bytes", visit_borrowed_bytes, decode_bytes ); fn deserialize_byte_buf(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { self.deserialize_bytes(visitor) } fn deserialize_seq(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { let (size, type_num) = self.size_and_type()?; self.decode_direct(size, type_num, TYPE_ARRAY, "array", |de, size| { de.enter_nested()?; let res = visitor.visit_seq(ArrayAccess { de, count: size }); de.exit_nested(); res }) } fn deserialize_tuple(self, len: usize, visitor: V) -> DecodeResult where V: Visitor<'de>, { self.deserialize_fixed_size_array(len, visitor) } fn deserialize_tuple_struct( self, _name: &'static str, len: usize, visitor: V, ) -> DecodeResult where V: Visitor<'de>, { self.deserialize_fixed_size_array(len, visitor) } fn deserialize_map(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { let (size, type_num) = self.size_and_type()?; self.decode_direct(size, type_num, TYPE_MAP, "map", |de, size| { de.enter_nested()?; let res = visitor.visit_map(MapAccessor { de, count: size * 2, }); de.exit_nested(); res }) } fn deserialize_struct( self, _name: &'static str, _fields: &'static [&'static str], visitor: V, ) -> DecodeResult where V: Visitor<'de>, { self.deserialize_map(visitor) } fn is_human_readable(&self) -> bool { false } fn deserialize_ignored_any(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { self.skip_value()?; visitor.visit_unit() } fn deserialize_enum( self, _name: &'static str, _variants: &'static [&'static str], visitor: V, ) -> DecodeResult where V: Visitor<'de>, { visitor.visit_enum(EnumAccessor { de: self }) } fn deserialize_identifier(self, visitor: V) -> DecodeResult where V: Visitor<'de>, { match self.try_read_identifier_bytes()? { Some(bytes) => visitor.visit_borrowed_bytes(bytes), None => self.decode_any(visitor), } } forward_to_deserialize_any! { i8 i16 i64 i128 u8 char unit unit_struct newtype_struct } } struct ArrayAccess<'a, 'de: 'a> { de: &'a mut Decoder<'de>, count: usize, } // `SeqAccess` is provided to the `Visitor` to give it the ability to iterate // through elements of the sequence. impl<'de> SeqAccess<'de> for ArrayAccess<'_, 'de> { type Error = MaxMindDbError; fn size_hint(&self) -> Option { Some(self.count) } fn next_element_seed(&mut self, seed: T) -> DecodeResult> where T: DeserializeSeed<'de>, { // Check if there are no more elements. if self.count == 0 { return Ok(None); } self.count -= 1; // Deserialize an array element. seed.deserialize(&mut *self.de).map(Some) } } struct MapAccessor<'a, 'de: 'a> { de: &'a mut Decoder<'de>, count: usize, } // `MapAccess` is provided to the `Visitor` to give it the ability to iterate // through entries of the map. impl<'de> MapAccess<'de> for MapAccessor<'_, 'de> { type Error = MaxMindDbError; fn size_hint(&self) -> Option { Some(self.count / 2) } fn next_key_seed(&mut self, seed: K) -> DecodeResult> where K: DeserializeSeed<'de>, { // Check if there are no more entries. if self.count == 0 { return Ok(None); } self.count -= 1; // Deserialize a map key. seed.deserialize(&mut *self.de).map(Some) } fn next_value_seed(&mut self, seed: V) -> DecodeResult where V: DeserializeSeed<'de>, { // Check if there are no more entries. if self.count == 0 { return Err(self.de.decode_error("no more entries")); } self.count -= 1; // Deserialize a map value. seed.deserialize(&mut *self.de) } } struct EnumAccessor<'a, 'de: 'a> { de: &'a mut Decoder<'de>, } impl<'de> de::EnumAccess<'de> for EnumAccessor<'_, 'de> { type Error = MaxMindDbError; type Variant = Self; fn variant_seed(self, seed: V) -> DecodeResult<(V::Value, Self::Variant)> where V: DeserializeSeed<'de>, { // Deserialize the variant identifier (string) let variant = seed.deserialize(&mut *self.de)?; Ok((variant, self)) } } impl<'de> de::VariantAccess<'de> for EnumAccessor<'_, 'de> { type Error = MaxMindDbError; fn unit_variant(self) -> DecodeResult<()> { Ok(()) } fn newtype_variant_seed(self, seed: T) -> DecodeResult where T: DeserializeSeed<'de>, { seed.deserialize(&mut *self.de) } fn tuple_variant(self, len: usize, visitor: V) -> DecodeResult where V: Visitor<'de>, { self.de.deserialize_fixed_size_array(len, visitor) } fn struct_variant( self, _fields: &'static [&'static str], visitor: V, ) -> DecodeResult where V: Visitor<'de>, { de::Deserializer::deserialize_map(&mut *self.de, visitor) } } #[cfg(test)] mod tests { use crate::Reader; #[test] fn test_decoder_accepts_tuple_with_matching_length() { #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleRecord { array: (u32, u32, u32), } #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleStructRecord { array: TupleStruct, } #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleStruct(u32, u32, u32); let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap(); let lookup = reader.lookup("1.1.1.0".parse().unwrap()).unwrap(); let tuple = lookup.decode::().unwrap().unwrap(); assert_eq!(tuple.array, (1, 2, 3)); let tuple_struct = lookup.decode::().unwrap().unwrap(); assert_eq!(tuple_struct.array.0, 1); assert_eq!(tuple_struct.array.1, 2); assert_eq!(tuple_struct.array.2, 3); } #[test] fn test_decoder_rejects_tuple_length_mismatch() { #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleRecord { array: (u32, u32), } #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleStructRecord { array: TupleStruct, } #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct TupleStruct(u32, u32); let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap(); let lookup = reader.lookup("1.1.1.0".parse().unwrap()).unwrap(); let tuple_err = lookup.decode::().unwrap_err(); assert!(tuple_err .to_string() .contains("expected tuple of length 2, got array of length 3")); let tuple_struct_err = lookup.decode::().unwrap_err(); assert!(tuple_struct_err .to_string() .contains("expected tuple of length 2, got array of length 3")); } } maxminddb-0.28.1/src/error.rs000064400000000000000000000145001046102023000141450ustar 00000000000000//! Error types for MaxMind DB operations. use std::fmt::Display; use std::io; use ipnetwork::IpNetworkError; use serde::de; use thiserror::Error; /// Error returned by MaxMind DB operations. #[derive(Error, Debug)] #[non_exhaustive] pub enum MaxMindDbError { /// The database file is invalid or corrupted. #[error("{}", format_invalid_database(.message, .offset))] InvalidDatabase { /// Description of what is invalid. message: String, /// Byte offset in the database where the error was detected. offset: Option, }, /// An I/O error occurred while reading the database. #[error("i/o error: {0}")] Io( #[from] #[source] io::Error, ), /// Memory mapping failed. #[cfg(feature = "mmap")] #[error("memory map error: {0}")] Mmap(#[source] io::Error), /// Error decoding data from the database. #[error("{}", format_decoding_error(.message, .offset, .path.as_deref()))] Decoding { /// Description of the decoding error. message: String, /// Byte offset in the data section where the error occurred. offset: Option, /// JSON-pointer-like path to the field (e.g., "/city/names/en"). path: Option, }, /// The provided network/CIDR is invalid. #[error("invalid network: {0}")] InvalidNetwork( #[from] #[source] IpNetworkError, ), /// The provided input is invalid for this operation. #[error("invalid input: {message}")] InvalidInput { /// Description of what is invalid about the input. message: String, }, } fn format_invalid_database(message: &str, offset: &Option) -> String { match offset { Some(off) => format!("invalid database at offset {off}: {message}"), None => format!("invalid database: {message}"), } } fn format_decoding_error(message: &str, offset: &Option, path: Option<&str>) -> String { match (offset, path) { (Some(off), Some(p)) => format!("decoding error at offset {off} (path: {p}): {message}"), (Some(off), None) => format!("decoding error at offset {off}: {message}"), (None, Some(p)) => format!("decoding error (path: {p}): {message}"), (None, None) => format!("decoding error: {message}"), } } impl MaxMindDbError { /// Creates an InvalidDatabase error with just a message. pub fn invalid_database(message: impl Into) -> Self { MaxMindDbError::InvalidDatabase { message: message.into(), offset: None, } } /// Creates an InvalidDatabase error with message and offset. pub fn invalid_database_at(message: impl Into, offset: usize) -> Self { MaxMindDbError::InvalidDatabase { message: message.into(), offset: Some(offset), } } /// Creates a Decoding error with just a message. pub fn decoding(message: impl Into) -> Self { MaxMindDbError::Decoding { message: message.into(), offset: None, path: None, } } /// Creates a Decoding error with message and offset. pub fn decoding_at(message: impl Into, offset: usize) -> Self { MaxMindDbError::Decoding { message: message.into(), offset: Some(offset), path: None, } } /// Creates a Decoding error with message, offset, and path. pub fn decoding_at_path( message: impl Into, offset: usize, path: impl Into, ) -> Self { MaxMindDbError::Decoding { message: message.into(), offset: Some(offset), path: Some(path.into()), } } /// Creates an InvalidInput error. pub fn invalid_input(message: impl Into) -> Self { MaxMindDbError::InvalidInput { message: message.into(), } } } impl de::Error for MaxMindDbError { fn custom(msg: T) -> Self { MaxMindDbError::decoding(msg.to_string()) } } #[cfg(test)] mod tests { use super::*; use std::io::{Error, ErrorKind}; #[test] fn test_error_display() { // Error without offset assert_eq!( format!( "{}", MaxMindDbError::invalid_database("something went wrong") ), "invalid database: something went wrong".to_owned(), ); // Error with offset assert_eq!( format!( "{}", MaxMindDbError::invalid_database_at("something went wrong", 42) ), "invalid database at offset 42: something went wrong".to_owned(), ); let io_err = Error::new(ErrorKind::NotFound, "file not found"); assert_eq!( format!("{}", MaxMindDbError::from(io_err)), "i/o error: file not found".to_owned(), ); #[cfg(feature = "mmap")] { let mmap_io_err = Error::new(ErrorKind::PermissionDenied, "mmap failed"); assert_eq!( format!("{}", MaxMindDbError::Mmap(mmap_io_err)), "memory map error: mmap failed".to_owned(), ); } // Decoding error without offset assert_eq!( format!("{}", MaxMindDbError::decoding("unexpected type")), "decoding error: unexpected type".to_owned(), ); // Decoding error with offset assert_eq!( format!("{}", MaxMindDbError::decoding_at("unexpected type", 100)), "decoding error at offset 100: unexpected type".to_owned(), ); // Decoding error with offset and path assert_eq!( format!( "{}", MaxMindDbError::decoding_at_path("unexpected type", 100, "/city/names/en") ), "decoding error at offset 100 (path: /city/names/en): unexpected type".to_owned(), ); let net_err = IpNetworkError::InvalidPrefix; assert_eq!( format!("{}", MaxMindDbError::from(net_err)), "invalid network: invalid prefix".to_owned(), ); // InvalidInput error assert_eq!( format!("{}", MaxMindDbError::invalid_input("bad address")), "invalid input: bad address".to_owned(), ); } } maxminddb-0.28.1/src/geoip2.rs000064400000000000000000000737341046102023000142170ustar 00000000000000//! GeoIP2 and GeoLite2 database record structures //! //! This module provides strongly-typed Rust structures that correspond to the //! various GeoIP2 and GeoLite2 database record formats. //! //! # Record Types //! //! - [`City`] - Complete city-level geolocation data (most comprehensive) //! - [`Country`] - Country-level geolocation data //! - [`Enterprise`] - Enterprise database with additional confidence scores //! - [`Isp`] - Internet Service Provider information //! - [`AnonymousIp`] - Anonymous proxy and VPN detection //! - [`ConnectionType`] - Connection type classification //! - [`Domain`] - Domain information //! - [`Asn`] - Autonomous System Number data //! - [`DensityIncome`] - Population density and income data //! //! # Usage Examples //! //! ```rust //! use maxminddb::{Reader, geoip2}; //! use std::net::IpAddr; //! //! # fn main() -> Result<(), maxminddb::MaxMindDbError> { //! let reader = Reader::open_readfile( //! "test-data/test-data/GeoIP2-City-Test.mmdb")?; //! let ip: IpAddr = "89.160.20.128".parse().unwrap(); //! //! // City lookup - nested structs are always present (default to empty) //! let result = reader.lookup(ip)?; //! if let Some(city) = result.decode::()? { //! // Direct access to nested structs - no Option unwrapping needed //! if let Some(name) = city.city.names.english { //! println!("City: {}", name); //! } //! if let Some(code) = city.country.iso_code { //! println!("Country: {}", code); //! } //! // Subdivisions is a Vec, empty if not present //! for sub in &city.subdivisions { //! if let Some(code) = sub.iso_code { //! println!("Subdivision: {}", code); //! } //! } //! } //! //! // Country-only lookup (smaller/faster) //! let result = reader.lookup(ip)?; //! if let Some(country) = result.decode::()? { //! if let Some(name) = country.country.names.english { //! println!("Country: {}", name); //! } //! } //! # Ok(()) //! # } //! ``` use serde::{Deserialize, Serialize}; /// Localized names for geographic entities. /// /// Contains name translations in the languages supported by MaxMind databases. /// Access names directly via fields like `names.english` or `names.german`. /// Each field is `Option<&str>` - `None` if not available in that language. /// /// # Example /// /// ``` /// use maxminddb::{Reader, geoip2}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// let result = reader.lookup(ip).unwrap(); /// /// if let Some(city) = result.decode::().unwrap() { /// // Access names directly - Option<&str> /// if let Some(name) = city.city.names.english { /// println!("City (en): {}", name); /// } /// if let Some(name) = city.city.names.german { /// println!("City (de): {}", name); /// } /// } /// ``` #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)] pub struct Names<'a> { /// German name (de) #[serde( borrow, rename = "de", default, skip_serializing_if = "Option::is_none" )] pub german: Option<&'a str>, /// English name (en) #[serde(rename = "en", default, skip_serializing_if = "Option::is_none")] pub english: Option<&'a str>, /// Spanish name (es) #[serde(rename = "es", default, skip_serializing_if = "Option::is_none")] pub spanish: Option<&'a str>, /// French name (fr) #[serde(rename = "fr", default, skip_serializing_if = "Option::is_none")] pub french: Option<&'a str>, /// Japanese name (ja) #[serde(rename = "ja", default, skip_serializing_if = "Option::is_none")] pub japanese: Option<&'a str>, /// Brazilian Portuguese name (pt-BR) #[serde(rename = "pt-BR", default, skip_serializing_if = "Option::is_none")] pub brazilian_portuguese: Option<&'a str>, /// Russian name (ru) #[serde(rename = "ru", default, skip_serializing_if = "Option::is_none")] pub russian: Option<&'a str>, /// Simplified Chinese name (zh-CN) #[serde(rename = "zh-CN", default, skip_serializing_if = "Option::is_none")] pub simplified_chinese: Option<&'a str>, } impl Names<'_> { /// Returns true if all name fields are `None`. #[must_use] pub fn is_empty(&self) -> bool { self.german.is_none() && self.english.is_none() && self.spanish.is_none() && self.french.is_none() && self.japanese.is_none() && self.brazilian_portuguese.is_none() && self.russian.is_none() && self.simplified_chinese.is_none() } } macro_rules! impl_is_empty_via_default { ($ty:ty) => { impl $ty { /// Returns true if all fields are empty/None. #[must_use] pub fn is_empty(&self) -> bool { *self == Self::default() } } }; } /// GeoIP2/GeoLite2 Country database record. /// /// Contains country-level geolocation data for an IP address. This is the /// simplest geolocation record type, suitable when you only need country /// information. #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct Country<'a> { /// Continent data for the IP address. #[serde(borrow, default, skip_serializing_if = "country::Continent::is_empty")] pub continent: country::Continent<'a>, /// Country where MaxMind believes the IP is located. #[serde(default, skip_serializing_if = "country::Country::is_empty")] pub country: country::Country<'a>, /// Country where the ISP has registered the IP block. /// May differ from `country` (e.g., for mobile networks or VPNs). #[serde(default, skip_serializing_if = "country::Country::is_empty")] pub registered_country: country::Country<'a>, /// Country represented by users of this IP (e.g., military base or embassy). #[serde(default, skip_serializing_if = "country::RepresentedCountry::is_empty")] pub represented_country: country::RepresentedCountry<'a>, /// Various traits associated with the IP address. #[serde(default, skip_serializing_if = "country::Traits::is_empty")] pub traits: country::Traits, } /// GeoIP2/GeoLite2 City database record. /// /// Contains city-level geolocation data including location coordinates, /// postal code, subdivisions (states/provinces), and country information. /// This is the most comprehensive free geolocation record type. #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct City<'a> { /// City data for the IP address. #[serde(borrow, default, skip_serializing_if = "city::City::is_empty")] pub city: city::City<'a>, /// Continent data for the IP address. #[serde(default, skip_serializing_if = "city::Continent::is_empty")] pub continent: city::Continent<'a>, /// Country where MaxMind believes the IP is located. #[serde(default, skip_serializing_if = "city::Country::is_empty")] pub country: city::Country<'a>, /// Location data including coordinates and time zone. #[serde(default, skip_serializing_if = "city::Location::is_empty")] pub location: city::Location<'a>, /// Postal code data for the IP address. #[serde(default, skip_serializing_if = "city::Postal::is_empty")] pub postal: city::Postal<'a>, /// Country where the ISP has registered the IP block. #[serde(default, skip_serializing_if = "city::Country::is_empty")] pub registered_country: city::Country<'a>, /// Country represented by users of this IP (e.g., military base or embassy). #[serde(default, skip_serializing_if = "city::RepresentedCountry::is_empty")] pub represented_country: city::RepresentedCountry<'a>, /// Subdivisions (states, provinces, etc.) ordered from largest to smallest. /// For example, Oxford, UK would have England first, then Oxfordshire. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub subdivisions: Vec>, /// Various traits associated with the IP address. #[serde(default, skip_serializing_if = "city::Traits::is_empty")] pub traits: city::Traits, } /// GeoIP2 Enterprise database record. /// /// Contains all City data plus additional confidence scores and traits. /// Enterprise records include confidence values (0-100) indicating MaxMind's /// certainty about the accuracy of each field. #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct Enterprise<'a> { /// City data with confidence score. #[serde(borrow, default, skip_serializing_if = "enterprise::City::is_empty")] pub city: enterprise::City<'a>, /// Continent data for the IP address. #[serde(default, skip_serializing_if = "enterprise::Continent::is_empty")] pub continent: enterprise::Continent<'a>, /// Country data with confidence score. #[serde(default, skip_serializing_if = "enterprise::Country::is_empty")] pub country: enterprise::Country<'a>, /// Location data including coordinates and time zone. #[serde(default, skip_serializing_if = "enterprise::Location::is_empty")] pub location: enterprise::Location<'a>, /// Postal code data with confidence score. #[serde(default, skip_serializing_if = "enterprise::Postal::is_empty")] pub postal: enterprise::Postal<'a>, /// Country where the ISP has registered the IP block. #[serde(default, skip_serializing_if = "enterprise::Country::is_empty")] pub registered_country: enterprise::Country<'a>, /// Country represented by users of this IP (e.g., military base or embassy). #[serde( default, skip_serializing_if = "enterprise::RepresentedCountry::is_empty" )] pub represented_country: enterprise::RepresentedCountry<'a>, /// Subdivisions with confidence scores, ordered from largest to smallest. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub subdivisions: Vec>, /// Extended traits including ISP, organization, and connection information. #[serde(default, skip_serializing_if = "enterprise::Traits::is_empty")] pub traits: enterprise::Traits<'a>, } /// GeoIP2 ISP database record. /// /// Contains Internet Service Provider and organization information for an IP. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Isp<'a> { /// The autonomous system number (ASN) for the IP address. #[serde(skip_serializing_if = "Option::is_none")] pub autonomous_system_number: Option, /// The organization associated with the registered ASN. #[serde(skip_serializing_if = "Option::is_none")] pub autonomous_system_organization: Option<&'a str>, /// The name of the ISP associated with the IP address. #[serde(skip_serializing_if = "Option::is_none")] pub isp: Option<&'a str>, /// The mobile country code (MCC) associated with the IP. /// See . #[serde(skip_serializing_if = "Option::is_none")] pub mobile_country_code: Option<&'a str>, /// The mobile network code (MNC) associated with the IP. /// See . #[serde(skip_serializing_if = "Option::is_none")] pub mobile_network_code: Option<&'a str>, /// The name of the organization associated with the IP address. #[serde(skip_serializing_if = "Option::is_none")] pub organization: Option<&'a str>, } /// GeoIP2 Connection-Type database record. /// /// Contains the connection type for an IP address. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct ConnectionType<'a> { /// The connection type. Possible values include "Dialup", "Cable/DSL", /// "Corporate", "Cellular", and "Satellite". Additional values may be /// added in the future. #[serde(skip_serializing_if = "Option::is_none")] pub connection_type: Option<&'a str>, } /// GeoIP2 Anonymous IP database record. /// /// Contains information about whether an IP address is associated with /// anonymous or proxy services. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct AnonymousIp { /// True if the IP belongs to any sort of anonymous network. #[serde(skip_serializing_if = "Option::is_none")] pub is_anonymous: Option, /// True if the IP is registered to an anonymous VPN provider. /// Note: If a VPN provider does not register subnets under names associated /// with them, we will likely only flag their IP ranges using `is_hosting_provider`. #[serde(skip_serializing_if = "Option::is_none")] pub is_anonymous_vpn: Option, /// True if the IP belongs to a hosting or VPN provider. #[serde(skip_serializing_if = "Option::is_none")] pub is_hosting_provider: Option, /// True if the IP belongs to a public proxy. #[serde(skip_serializing_if = "Option::is_none")] pub is_public_proxy: Option, /// True if the IP is on a suspected anonymizing network and belongs to /// a residential ISP. #[serde(skip_serializing_if = "Option::is_none")] pub is_residential_proxy: Option, /// True if the IP is a Tor exit node. #[serde(skip_serializing_if = "Option::is_none")] pub is_tor_exit_node: Option, } /// GeoIP2 DensityIncome database record. /// /// Contains population density and income data for an IP address location. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct DensityIncome { /// The average income in US dollars associated with the IP address. #[serde(skip_serializing_if = "Option::is_none")] pub average_income: Option, /// The estimated number of people per square kilometer. #[serde(skip_serializing_if = "Option::is_none")] pub population_density: Option, } /// GeoIP2 Domain database record. /// /// Contains the second-level domain associated with an IP address. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Domain<'a> { /// The second-level domain associated with the IP address /// (e.g., "example.com"). #[serde(skip_serializing_if = "Option::is_none")] pub domain: Option<&'a str>, } /// GeoLite2 ASN database record. /// /// Contains Autonomous System Number (ASN) data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Asn<'a> { /// The autonomous system number for the IP address. #[serde(skip_serializing_if = "Option::is_none")] pub autonomous_system_number: Option, /// The organization associated with the registered ASN. #[serde(skip_serializing_if = "Option::is_none")] pub autonomous_system_organization: Option<&'a str>, } /// Country/City database model structs. /// /// These structs are used by both [`super::Country`] and [`super::City`] records. pub mod country { use super::Names; use serde::{Deserialize, Serialize}; /// Continent data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Continent<'a> { /// Two-character continent code (e.g., "NA" for North America, "EU" for Europe). #[serde(default, skip_serializing_if = "Option::is_none")] pub code: Option<&'a str>, /// GeoNames ID for the continent. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// Localized continent names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(Continent<'_>); /// Country data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Country<'a> { /// GeoNames ID for the country. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// True if the country is a member state of the European Union. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_in_european_union: Option, /// Two-character ISO 3166-1 alpha-2 country code. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub iso_code: Option<&'a str>, /// Localized country names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(Country<'_>); /// Represented country data. /// /// The represented country is the country represented by something like a /// military base or embassy. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct RepresentedCountry<'a> { /// GeoNames ID for the represented country. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// True if the represented country is a member state of the European Union. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_in_european_union: Option, /// Two-character ISO 3166-1 alpha-2 country code. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub iso_code: Option<&'a str>, /// Localized country names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, /// Type of entity representing the country (e.g., "military"). #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] pub representation_type: Option<&'a str>, } impl_is_empty_via_default!(RepresentedCountry<'_>); /// Traits data for Country/City records. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Traits { /// True if the IP belongs to an anycast network. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub is_anycast: Option, } impl_is_empty_via_default!(Traits); } /// City database model structs. /// /// City-specific structs. Country-level structs are re-exported from [`super::country`]. pub mod city { use super::Names; use serde::{Deserialize, Serialize}; pub use super::country::{Continent, Country, RepresentedCountry, Traits}; /// City data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct City<'a> { /// GeoNames ID for the city. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// Localized city names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(City<'_>); /// Location data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Location<'a> { /// Approximate accuracy radius in kilometers around the coordinates. /// This is the radius where we have a 67% confidence that the device /// using the IP address resides within. #[serde(default, skip_serializing_if = "Option::is_none")] pub accuracy_radius: Option, /// Approximate latitude of the location. This value is not precise and /// should not be used to identify a particular address or household. #[serde(default, skip_serializing_if = "Option::is_none")] pub latitude: Option, /// Approximate longitude of the location. This value is not precise and /// should not be used to identify a particular address or household. #[serde(default, skip_serializing_if = "Option::is_none")] pub longitude: Option, /// Metro code for the location, used for targeting advertisements. /// /// **Deprecated:** Metro codes are no longer maintained and should not be used. #[serde(default, skip_serializing_if = "Option::is_none")] pub metro_code: Option, /// Time zone associated with the location, as specified by the /// IANA Time Zone Database (e.g., "America/New_York"). #[serde(default, skip_serializing_if = "Option::is_none")] pub time_zone: Option<&'a str>, } impl_is_empty_via_default!(Location<'_>); /// Postal data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Postal<'a> { /// Postal code for the location. Not available for all countries. /// In some countries, this will only contain part of the postal code. #[serde(default, skip_serializing_if = "Option::is_none")] pub code: Option<&'a str>, } impl_is_empty_via_default!(Postal<'_>); /// Subdivision (state, province, etc.) data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Subdivision<'a> { /// GeoNames ID for the subdivision. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// ISO 3166-2 subdivision code (up to 3 characters). /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub iso_code: Option<&'a str>, /// Localized subdivision names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(Subdivision<'_>); } /// Enterprise database model structs. /// /// Enterprise-specific structs with confidence scores. Some structs are /// re-exported from [`super::country`]. pub mod enterprise { use super::Names; use serde::{Deserialize, Serialize}; pub use super::country::{Continent, RepresentedCountry}; /// City data with confidence score. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct City<'a> { /// Confidence score (0-100) indicating MaxMind's certainty that the /// city is correct. #[serde(default, skip_serializing_if = "Option::is_none")] pub confidence: Option, /// GeoNames ID for the city. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// Localized city names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(City<'_>); /// Country data with confidence score. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Country<'a> { /// Confidence score (0-100) indicating MaxMind's certainty that the /// country is correct. #[serde(default, skip_serializing_if = "Option::is_none")] pub confidence: Option, /// GeoNames ID for the country. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// True if the country is a member state of the European Union. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_in_european_union: Option, /// Two-character ISO 3166-1 alpha-2 country code. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub iso_code: Option<&'a str>, /// Localized country names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(Country<'_>); /// Location data for an IP address. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Location<'a> { /// Approximate accuracy radius in kilometers around the coordinates. /// This is the radius where we have a 67% confidence that the device /// using the IP address resides within. #[serde(default, skip_serializing_if = "Option::is_none")] pub accuracy_radius: Option, /// Approximate latitude of the location. This value is not precise and /// should not be used to identify a particular address or household. #[serde(default, skip_serializing_if = "Option::is_none")] pub latitude: Option, /// Approximate longitude of the location. This value is not precise and /// should not be used to identify a particular address or household. #[serde(default, skip_serializing_if = "Option::is_none")] pub longitude: Option, /// Metro code for the location, used for targeting advertisements. /// /// **Deprecated:** Metro codes are no longer maintained and should not be used. #[serde(default, skip_serializing_if = "Option::is_none")] pub metro_code: Option, /// Time zone associated with the location, as specified by the /// IANA Time Zone Database (e.g., "America/New_York"). #[serde(default, skip_serializing_if = "Option::is_none")] pub time_zone: Option<&'a str>, } impl_is_empty_via_default!(Location<'_>); /// Postal data with confidence score. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Postal<'a> { /// Postal code for the location. Not available for all countries. /// In some countries, this will only contain part of the postal code. #[serde(default, skip_serializing_if = "Option::is_none")] pub code: Option<&'a str>, /// Confidence score (0-100) indicating MaxMind's certainty that the /// postal code is correct. #[serde(default, skip_serializing_if = "Option::is_none")] pub confidence: Option, } impl_is_empty_via_default!(Postal<'_>); /// Subdivision data with confidence score. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Subdivision<'a> { /// Confidence score (0-100) indicating MaxMind's certainty that the /// subdivision is correct. #[serde(default, skip_serializing_if = "Option::is_none")] pub confidence: Option, /// GeoNames ID for the subdivision. #[serde(default, skip_serializing_if = "Option::is_none")] pub geoname_id: Option, /// ISO 3166-2 subdivision code (up to 3 characters). /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub iso_code: Option<&'a str>, /// Localized subdivision names. #[serde(borrow, default, skip_serializing_if = "Names::is_empty")] pub names: Names<'a>, } impl_is_empty_via_default!(Subdivision<'_>); /// Extended traits data for Enterprise records. /// /// Contains ISP, organization, connection type, and anonymity information. #[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] pub struct Traits<'a> { /// The autonomous system number (ASN) for the IP address. #[serde(default, skip_serializing_if = "Option::is_none")] pub autonomous_system_number: Option, /// The organization associated with the registered ASN. #[serde(default, skip_serializing_if = "Option::is_none")] pub autonomous_system_organization: Option<&'a str>, /// The connection type. Possible values include "Dialup", "Cable/DSL", /// "Corporate", "Cellular", and "Satellite". #[serde(default, skip_serializing_if = "Option::is_none")] pub connection_type: Option<&'a str>, /// The second-level domain associated with the IP address /// (e.g., "example.com"). #[serde(default, skip_serializing_if = "Option::is_none")] pub domain: Option<&'a str>, /// True if the IP belongs to any sort of anonymous network. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_anonymous: Option, /// True if the IP is registered to an anonymous VPN provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_anonymous_vpn: Option, /// True if the IP belongs to an anycast network. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub is_anycast: Option, /// True if the IP belongs to a hosting or VPN provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_hosting_provider: Option, /// The name of the ISP associated with the IP address. #[serde(default, skip_serializing_if = "Option::is_none")] pub isp: Option<&'a str>, /// True if the IP belongs to a public proxy. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_public_proxy: Option, /// True if the IP is on a suspected anonymizing network and belongs to /// a residential ISP. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_residential_proxy: Option, /// True if the IP is a Tor exit node. #[serde(default, skip_serializing_if = "Option::is_none")] pub is_tor_exit_node: Option, /// The mobile country code (MCC) associated with the IP. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub mobile_country_code: Option<&'a str>, /// The mobile network code (MNC) associated with the IP. /// See . #[serde(default, skip_serializing_if = "Option::is_none")] pub mobile_network_code: Option<&'a str>, /// The name of the organization associated with the IP address. #[serde(default, skip_serializing_if = "Option::is_none")] pub organization: Option<&'a str>, /// The user type associated with the IP address. Possible values include /// "business", "cafe", "cellular", "college", "government", "hosting", /// "library", "military", "residential", "router", "school", /// "search_engine_spider", and "traveler". #[serde(default, skip_serializing_if = "Option::is_none")] pub user_type: Option<&'a str>, } impl_is_empty_via_default!(Traits<'_>); } maxminddb-0.28.1/src/lib.rs000064400000000000000000000353211046102023000135660ustar 00000000000000#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)] //! # MaxMind DB Reader //! //! This library reads the MaxMind DB format, including the GeoIP2 and GeoLite2 databases. //! //! ## Features //! //! This crate provides several optional features for performance and functionality: //! //! - **`mmap`** (default: disabled): Enable memory-mapped file access for //! better performance in long-running applications //! - **`simdutf8`** (default: disabled): Use SIMD instructions for faster //! UTF-8 validation during string decoding //! - **`unsafe-str-decode`** (default: disabled): Skip UTF-8 validation //! entirely for maximum performance (~20% faster lookups) //! //! **Note**: `simdutf8` and `unsafe-str-decode` are mutually exclusive. //! //! ## Database Compatibility //! //! This library supports all MaxMind DB format databases: //! - **GeoIP2** databases (City, Country, Enterprise, ISP, etc.) //! - **GeoLite2** databases (free versions) //! - Custom MaxMind DB format databases //! //! ## Thread Safety //! //! The `Reader` is `Send` and `Sync`, making it safe to share across threads. //! This makes it ideal for web servers and other concurrent applications. //! //! ## Quick Start //! //! ```rust //! use maxminddb::{Reader, geoip2}; //! use std::net::IpAddr; //! //! fn main() -> Result<(), Box> { //! // Open database file //! # let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?; //! # /* //! let reader = Reader::open_readfile("/path/to/GeoIP2-City.mmdb")?; //! # */ //! //! // Look up an IP address //! let ip: IpAddr = "89.160.20.128".parse()?; //! let result = reader.lookup(ip)?; //! //! if let Some(city) = result.decode::()? { //! // Access nested structs directly - no Option unwrapping needed //! println!("Country: {}", city.country.iso_code.unwrap_or("Unknown")); //! } //! //! Ok(()) //! } //! ``` //! //! ## Selective Field Access //! //! Use `decode_path` to extract specific fields without deserializing the entire record: //! //! ```rust //! use maxminddb::{path, Reader}; //! use std::net::IpAddr; //! //! let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); //! let ip: IpAddr = "89.160.20.128".parse().unwrap(); //! //! let result = reader.lookup(ip).unwrap(); //! let country_code: Option = result.decode_path(&path!["country", "iso_code"]).unwrap(); //! //! println!("Country: {:?}", country_code); //! ``` #[cfg(all(feature = "simdutf8", feature = "unsafe-str-decode"))] compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusive"); mod decoder; mod error; pub mod geoip2; mod metadata; mod reader; mod result; mod within; // Re-export public types pub use error::MaxMindDbError; pub use metadata::Metadata; pub use reader::Reader; pub use result::{LookupResult, PathElement}; pub use within::{Within, WithinOptions}; #[cfg(feature = "mmap")] pub use memmap2::Mmap; #[cfg(test)] mod reader_test; #[cfg(test)] mod tests { use super::*; use std::net::IpAddr; #[test] fn test_lookup_network() { use std::collections::HashMap; struct TestCase { ip: &'static str, db_file: &'static str, expected_network: &'static str, expected_found: bool, } let test_cases = [ // IPv4 address in IPv6 database - not found, returns containing network TestCase { ip: "1.1.1.1", db_file: "test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb", expected_network: "1.0.0.0/8", expected_found: false, }, // IPv6 exact match TestCase { ip: "::1:ffff:ffff", db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb", expected_network: "::1:ffff:ffff/128", expected_found: true, }, // IPv6 network match (not exact) TestCase { ip: "::2:0:1", db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb", expected_network: "::2:0:0/122", expected_found: true, }, // IPv4 exact match TestCase { ip: "1.1.1.1", db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb", expected_network: "1.1.1.1/32", expected_found: true, }, // IPv4 network match (not exact) TestCase { ip: "1.1.1.3", db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb", expected_network: "1.1.1.2/31", expected_found: true, }, // IPv4 in decoder test database TestCase { ip: "1.1.1.3", db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb", expected_network: "1.1.1.0/24", expected_found: true, }, // IPv4-mapped IPv6 address - preserves IPv6 form TestCase { ip: "::ffff:1.1.1.128", db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb", expected_network: "::ffff:1.1.1.0/120", expected_found: true, }, // IPv4-compatible IPv6 address - uses compressed IPv6 notation TestCase { ip: "::1.1.1.128", db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb", expected_network: "::101:100/120", expected_found: true, }, // No IPv4 search tree - IPv4 address returns ::/64 TestCase { ip: "200.0.2.1", db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb", expected_network: "::/64", expected_found: true, }, // No IPv4 search tree - IPv6 address in IPv4 range TestCase { ip: "::200.0.2.1", db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb", expected_network: "::/64", expected_found: true, }, // No IPv4 search tree - IPv6 address at boundary of IPv4 space TestCase { ip: "0:0:0:0:ffff:ffff:ffff:ffff", db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb", expected_network: "::/64", expected_found: true, }, // No IPv4 search tree - high IPv6 address not found TestCase { ip: "ef00::", db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb", expected_network: "8000::/1", expected_found: false, }, ]; // Cache readers to avoid reopening the same file multiple times let mut readers: HashMap<&str, Reader>> = HashMap::new(); for test in &test_cases { let reader = readers .entry(test.db_file) .or_insert_with(|| Reader::open_readfile(test.db_file).unwrap()); let ip: IpAddr = test.ip.parse().unwrap(); let result = reader.lookup(ip).unwrap(); assert_eq!( result.has_data(), test.expected_found, "IP {} in {}: expected has_data={}, got has_data={}", test.ip, test.db_file, test.expected_found, result.has_data() ); let network = result.network().unwrap(); assert_eq!( network.to_string(), test.expected_network, "IP {} in {}: expected network {}, got {}", test.ip, test.db_file, test.expected_network, network ); } } #[test] fn test_lookup_with_geoip_data() { let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); let ip: IpAddr = "89.160.20.128".parse().unwrap(); let result = reader.lookup(ip).unwrap(); assert!(result.has_data(), "lookup should find known IP"); // Decode the data let city: geoip2::City = result.decode().unwrap().unwrap(); assert!(!city.city.is_empty(), "Expected city data"); // Check full network (not just prefix) let network = result.network().unwrap(); assert_eq!( network.to_string(), "89.160.20.128/25", "Expected network 89.160.20.128/25" ); // Check offset is available for caching assert!( result.offset().is_some(), "Expected offset to be Some for found IP" ); } #[test] fn test_lookup_network_uses_measured_ipv4_subtree_depth() { let mut reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb").unwrap(); assert_eq!(reader.metadata.ip_version, 6); // Simulate a valid IPv6 database whose IPv4 subtree starts somewhere // other than bit 96. Using a shallow subtree depth keeps the combined // prefix length <= 32, which would be ambiguous without an explicit // Lookup vs Iter source flag. reader.ipv4_start_bit_depth = 16; let result = reader.lookup("1.1.1.1".parse().unwrap()).unwrap(); assert_eq!(result.network().unwrap().to_string(), "1.0.0.0/8"); } #[test] fn test_lookup_offset_is_stable_for_shared_record() { let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); let first = reader.lookup("89.160.20.128".parse().unwrap()).unwrap(); let second = reader.lookup("89.160.20.129".parse().unwrap()).unwrap(); assert!(first.has_data()); assert!(second.has_data()); assert_eq!(first.network().unwrap(), second.network().unwrap()); assert_eq!( first.offset(), second.offset(), "IPs in the same record should share a cacheable offset" ); } #[test] fn test_decode_path() { let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); let ip: IpAddr = "89.160.20.128".parse().unwrap(); let result = reader.lookup(ip).unwrap(); // Navigate to country.iso_code let iso_code: Option = result .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")]) .unwrap(); assert_eq!(iso_code, Some("SE".to_owned())); // Navigate to non-existent path let missing: Option = result .decode_path(&[PathElement::Key("nonexistent")]) .unwrap(); assert!(missing.is_none()); } #[test] fn test_decode_path_on_not_found_lookup() { let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); let ip: IpAddr = "2c0f:ff00::1".parse().unwrap(); let result = reader.lookup(ip).unwrap(); assert!(!result.has_data()); assert!(result.offset().is_none()); assert!(result.decode::().unwrap().is_none()); let country_code: Option = result .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")]) .unwrap(); assert!(country_code.is_none()); } #[test] fn test_ipv6_in_ipv4_database() { let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb").unwrap(); let ip: IpAddr = "2001::".parse().unwrap(); let result = reader.lookup(ip); match result { Err(MaxMindDbError::InvalidInput { message }) => { assert!( message.contains("IPv6") && message.contains("IPv4"), "Expected error message about IPv6 in IPv4 database, got: {}", message ); } Err(e) => panic!( "Expected InvalidInput error for IPv6 in IPv4 database, got: {:?}", e ), Ok(_) => panic!("Expected error for IPv6 lookup in IPv4-only database"), } } #[test] fn test_decode_path_comprehensive() { let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap(); let ip: IpAddr = "::1.1.1.0".parse().unwrap(); let result = reader.lookup(ip).unwrap(); assert!(result.has_data()); // Test simple path: uint16 let u16_val: Option = result.decode_path(&[PathElement::Key("uint16")]).unwrap(); assert_eq!(u16_val, Some(100)); // Test array access: first element let arr_first: Option = result .decode_path(&[PathElement::Key("array"), PathElement::Index(0)]) .unwrap(); assert_eq!(arr_first, Some(1)); // Test array access: last element (index 2) let arr_last: Option = result .decode_path(&[PathElement::Key("array"), PathElement::Index(2)]) .unwrap(); assert_eq!(arr_last, Some(3)); // Test array access: out of bounds (index 3) returns None let arr_oob: Option = result .decode_path(&[PathElement::Key("array"), PathElement::Index(3)]) .unwrap(); assert!(arr_oob.is_none()); // Test IndexFromEnd: 0 means last element let arr_last: Option = result .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(0)]) .unwrap(); assert_eq!(arr_last, Some(3)); // Test IndexFromEnd: 2 means first element (array has 3 elements) let arr_first: Option = result .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(2)]) .unwrap(); assert_eq!(arr_first, Some(1)); // Test nested path: map.mapX.arrayX[1] let nested: Option = result .decode_path(&[ PathElement::Key("map"), PathElement::Key("mapX"), PathElement::Key("arrayX"), PathElement::Index(1), ]) .unwrap(); assert_eq!(nested, Some(8)); // Test non-existent key returns None let missing: Option = result .decode_path(&[PathElement::Key("does-not-exist"), PathElement::Index(1)]) .unwrap(); assert!(missing.is_none()); // Test utf8_string path let utf8: Option = result .decode_path(&[PathElement::Key("utf8_string")]) .unwrap(); assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned())); } } maxminddb-0.28.1/src/metadata.rs000064400000000000000000000031131046102023000145720ustar 00000000000000//! Database metadata types. use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// Metadata about the MaxMind DB file. #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] pub struct Metadata { /// Major version of the binary format (always 2). pub binary_format_major_version: u16, /// Minor version of the binary format (always 0). pub binary_format_minor_version: u16, /// Unix timestamp when the database was built. pub build_epoch: u64, /// Database type (e.g., "GeoIP2-City", "GeoLite2-Country"). pub database_type: String, /// Map of language codes to database descriptions. pub description: BTreeMap, /// IP version supported (4 or 6). pub ip_version: u16, /// Languages available in the database. pub languages: Vec, /// Number of nodes in the search tree. pub node_count: u32, /// Size of each record in bits (24, 28, or 32). pub record_size: u16, } impl Metadata { /// Returns the database build time as a `SystemTime`. /// /// This converts the `build_epoch` Unix timestamp to a `SystemTime`. /// /// # Example /// /// ``` /// use maxminddb::Reader; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let build_time = reader.metadata.build_time(); /// println!("Database built: {:?}", build_time); /// ``` #[must_use] pub fn build_time(&self) -> std::time::SystemTime { std::time::UNIX_EPOCH + std::time::Duration::from_secs(self.build_epoch) } } maxminddb-0.28.1/src/reader.rs000064400000000000000000000752621046102023000142720ustar 00000000000000//! MaxMind DB reader implementation. use std::collections::HashSet; use std::fs; use std::net::IpAddr; use std::path::Path; use ipnetwork::IpNetwork; use serde::Deserialize; #[cfg(feature = "mmap")] pub use memmap2::Mmap; #[cfg(feature = "mmap")] use memmap2::MmapOptions; #[cfg(feature = "mmap")] use std::fs::File; use crate::decoder; use crate::error::MaxMindDbError; use crate::metadata::Metadata; use crate::result::{LookupResult, LookupSource, NetworkKind}; use crate::within::{IpInt, Within, WithinNode, WithinOptions}; /// Size of the data section separator (16 zero bytes). const DATA_SECTION_SEPARATOR_SIZE: usize = 16; /// A reader for the MaxMind DB format. The lifetime `'data` is tied to the /// lifetime of the underlying buffer holding the contents of the database file. /// /// The `Reader` supports both file-based and memory-mapped access to MaxMind /// DB files, including GeoIP2 and GeoLite2 databases. /// /// # Features /// /// - **`mmap`**: Enable memory-mapped file access for better performance /// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string /// decoding) /// - **`unsafe-str-decode`**: Skip UTF-8 validation entirely (unsafe, but /// ~20% faster) pub struct Reader> { pub(crate) buf: S, /// Database metadata. pub metadata: Metadata, record_size: u16, /// Cached `Metadata::node_count` for `Reader` search-tree traversal. /// Use this instead of `metadata.node_count`, which is publicly mutable. node_count: usize, /// Cached bytes per node derived from `Metadata::record_size` for `Reader`. /// Use this instead of `metadata.record_size` in lookup hot paths. node_byte_size: usize, pub(crate) ipv4_start: usize, /// Bit depth at which ipv4_start was found (0-96). Used to calculate /// correct prefix lengths for IPv4 lookups in IPv6 databases. pub(crate) ipv4_start_bit_depth: usize, pub(crate) pointer_base: usize, } impl> std::fmt::Debug for Reader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Reader") .field("buf_len", &self.buf.as_ref().len()) .field("metadata", &self.metadata) .field("ipv4_start", &self.ipv4_start) .field("ipv4_start_bit_depth", &self.ipv4_start_bit_depth) .field("pointer_base", &self.pointer_base) .finish_non_exhaustive() } } #[cfg(feature = "mmap")] impl Reader { /// Open a MaxMind DB database file by memory mapping it. /// /// # Safety /// /// The caller must ensure that the database file is not modified or /// truncated while the `Reader` exists. Modifying or truncating the /// file while it is memory-mapped will result in undefined behavior. /// /// # Example /// /// ``` /// # #[cfg(feature = "mmap")] /// # { /// // SAFETY: The database file will not be modified while the reader exists. /// let reader = unsafe { /// maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb") /// }.unwrap(); /// # } /// ``` pub unsafe fn open_mmap>(database: P) -> Result, MaxMindDbError> { let file_read = File::open(database)?; let mmap = MmapOptions::new() .map(&file_read) .map_err(MaxMindDbError::Mmap)?; Reader::from_source(mmap) } } impl Reader> { /// Open a MaxMind DB database file by loading it into memory. /// /// # Example /// /// ``` /// let reader = maxminddb::Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// ``` pub fn open_readfile>(database: P) -> Result>, MaxMindDbError> { let buf: Vec = fs::read(&database)?; // IO error converted via #[from] Reader::from_source(buf) } } impl<'de, S: AsRef<[u8]>> Reader { /// Open a MaxMind DB database from anything that implements AsRef<[u8]> /// /// # Example /// /// ``` /// use std::fs; /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let reader = maxminddb::Reader::from_source(buf).unwrap(); /// ``` pub fn from_source(buf: S) -> Result, MaxMindDbError> { let metadata_start = find_metadata_start(buf.as_ref())?; let mut type_decoder = decoder::Decoder::new(&buf.as_ref()[metadata_start..], 0); let metadata = Metadata::deserialize(&mut type_decoder)?; validate_record_size(metadata.record_size)?; let search_tree_size = search_tree_size_bytes(metadata.node_count as usize, metadata.record_size as usize)?; let record_size = metadata.record_size; let node_count = metadata.node_count as usize; let node_byte_size = record_size as usize / 4; let pointer_base = search_tree_size .checked_add(DATA_SECTION_SEPARATOR_SIZE) .ok_or_else(|| { MaxMindDbError::invalid_database( "the MaxMind DB file's search tree extends beyond the file", ) })?; validate_search_tree_layout(pointer_base, metadata_start)?; let mut reader = Reader { buf, record_size, node_count, node_byte_size, pointer_base, metadata, ipv4_start: 0, ipv4_start_bit_depth: 0, }; let (ipv4_start, ipv4_start_bit_depth) = reader.find_ipv4_start(); reader.ipv4_start = ipv4_start; reader.ipv4_start_bit_depth = ipv4_start_bit_depth; Ok(reader) } /// Lookup an IP address in the database. /// /// Returns a [`LookupResult`] that can be used to: /// - Check if data exists with [`has_data()`](LookupResult::has_data) /// - Get the network containing the IP with [`network()`](LookupResult::network) /// - Decode the full record with [`decode()`](LookupResult::decode) /// - Decode a specific path with [`decode_path()`](LookupResult::decode_path) /// - Get a low-level decoder with [`decoder()`](LookupResult::decoder) /// /// # Examples /// /// Basic city lookup: /// ``` /// # use maxminddb::geoip2; /// # use std::net::IpAddr; /// # fn main() -> Result<(), maxminddb::MaxMindDbError> { /// let reader = maxminddb::Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb")?; /// /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// let result = reader.lookup(ip)?; /// /// if let Some(city) = result.decode::()? { /// // Access nested structs directly - no Option unwrapping needed /// if let Some(name) = city.city.names.english { /// println!("City: {}", name); /// } /// } else { /// println!("No data found for IP {}", ip); /// } /// # Ok(()) /// # } /// ``` /// /// Selective field access: /// ``` /// # use maxminddb::{path, Reader}; /// # use std::net::IpAddr; /// # fn main() -> Result<(), maxminddb::MaxMindDbError> { /// let reader = Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb")?; /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// /// let result = reader.lookup(ip)?; /// let country_code: Option = result.decode_path(&path!["country", "iso_code"])?; /// /// println!("Country: {:?}", country_code); /// # Ok(()) /// # } /// ``` pub fn lookup(&'de self, address: IpAddr) -> Result, MaxMindDbError> { match address { IpAddr::V4(v4) => { let (pointer, prefix_len) = self.find_address_in_tree_v4(v4.into()); // For IPv4 addresses in IPv6 databases, adjust prefix_len to reflect // the actual bit depth in the tree. The ipv4_start_bit_depth tells us // how deep in the IPv6 tree we were when we found the IPv4 subtree. let prefix_len = if self.metadata.ip_version == 6 { self.ipv4_start_bit_depth + prefix_len } else { prefix_len }; self.lookup_result(pointer, prefix_len as u8, address) } IpAddr::V6(v6) => { if self.metadata.ip_version == 4 { return Err(MaxMindDbError::invalid_input( "cannot look up IPv6 address in IPv4-only database", )); } let (pointer, prefix_len) = self.find_address_in_tree_v6(v6.into()); self.lookup_result(pointer, prefix_len as u8, address) } } } /// Iterate over all networks in the database. /// /// This is a convenience method equivalent to calling [`within()`](Self::within) /// with `0.0.0.0/0` for IPv4-only databases or `::/0` for IPv6 databases. /// /// # Arguments /// /// * `options` - Controls which networks are yielded. Use [`Default::default()`] /// for standard behavior. /// /// # Examples /// /// Iterate over all networks with default options: /// ``` /// use maxminddb::{geoip2, Reader}; /// /// let reader = Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// /// let mut count = 0; /// for result in reader.networks(Default::default()).unwrap() { /// let lookup = result.unwrap(); /// count += 1; /// if count >= 10 { break; } /// } /// ``` pub fn networks(&'de self, options: WithinOptions) -> Result, MaxMindDbError> { let cidr = if self.metadata.ip_version == 6 { IpNetwork::V6("::/0".parse().unwrap()) } else { IpNetwork::V4("0.0.0.0/0".parse().unwrap()) }; self.within(cidr, options) } /// Iterate over IP networks within a CIDR range. /// /// Returns an iterator that yields [`LookupResult`] for each network in the /// database that falls within the specified CIDR range. /// /// # Arguments /// /// * `cidr` - The CIDR range to iterate over. /// * `options` - Controls which networks are yielded. Use [`Default::default()`] /// for standard behavior (skip aliases, skip networks without data, include /// empty values). /// /// # Examples /// /// Iterate over all IPv4 networks: /// ``` /// use ipnetwork::IpNetwork; /// use maxminddb::{geoip2, Reader}; /// /// let reader = Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap()); /// let mut count = 0; /// for result in reader.within(ipv4_all, Default::default()).unwrap() { /// let lookup = result.unwrap(); /// let network = lookup.network().unwrap(); /// let city: geoip2::City = lookup.decode().unwrap().unwrap(); /// let city_name = city.city.names.english; /// println!("Network: {}, City: {:?}", network, city_name); /// count += 1; /// if count >= 10 { break; } // Limit output for example /// } /// ``` /// /// Search within a specific subnet: /// ``` /// use ipnetwork::IpNetwork; /// use maxminddb::{geoip2, Reader}; /// /// let reader = Reader::open_readfile( /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap()); /// for result in reader.within(subnet, Default::default()).unwrap() { /// match result { /// Ok(lookup) => { /// let network = lookup.network().unwrap(); /// println!("Found: {}", network); /// } /// Err(e) => eprintln!("Error: {}", e), /// } /// } /// ``` /// /// Include networks without data: /// ``` /// use ipnetwork::IpNetwork; /// use maxminddb::{Reader, WithinOptions}; /// /// let reader = Reader::open_readfile( /// "test-data/test-data/MaxMind-DB-test-mixed-24.mmdb").unwrap(); /// /// let opts = WithinOptions::default().include_networks_without_data(); /// for result in reader.within("1.0.0.0/8".parse().unwrap(), opts).unwrap() { /// let lookup = result.unwrap(); /// if !lookup.has_data() { /// println!("Network {} has no data", lookup.network().unwrap()); /// } /// } /// ``` pub fn within( &'de self, cidr: IpNetwork, options: WithinOptions, ) -> Result, MaxMindDbError> { if self.metadata.ip_version == 4 && matches!(cidr, IpNetwork::V6(_)) { return Err(MaxMindDbError::invalid_input( "cannot iterate IPv6 network in IPv4-only database", )); } let ip_address = cidr.network(); let prefix_len = cidr.prefix() as usize; let ip_int = IpInt::new(ip_address); let bit_count = ip_int.bit_count(); let mut node = self.start_node(bit_count); let node_count = self.node_count; let mut stack: Vec = Vec::with_capacity(bit_count - prefix_len); // Traverse down the tree to the level that matches the cidr mark let mut depth = 0_usize; for i in 0..prefix_len { let bit = ip_int.get_bit(i); node = self.read_node(node, bit as usize); depth = i + 1; // We've now traversed i+1 bits (bits 0 through i) if node >= node_count { // We've hit a data node or dead end before we exhausted our prefix. // This means the requested CIDR is contained in a single record. break; } } // Always push the node - it could be: // - A data node (> node_count): will be yielded as a single record // - The empty node (== node_count): will be skipped unless include_networks_without_data // - An internal node (< node_count): will be traversed to find all contained records stack.push(WithinNode { node, ip_int, prefix_len: depth, }); let within = Within { reader: self, node_count, stack, options, }; Ok(within) } // Pointer 0 means "not found" because normalize_lookup_result collapses both // the placeholder empty node (`node == node_count`) and an unfinished internal // terminal (`node < node_count`, i.e. bits exhausted while still on a tree // node) into 0, so neither path reaches resolve_data_pointer with a non-data // value. #[inline(always)] fn lookup_result( &'de self, pointer: usize, prefix_len: u8, address: IpAddr, ) -> Result, MaxMindDbError> { let network_kind = match address { IpAddr::V4(_) if self.metadata.ip_version == 6 && self.has_ipv4_subtree() => { NetworkKind::V4InV6Subtree } IpAddr::V4(_) if self.metadata.ip_version == 6 => NetworkKind::V6, IpAddr::V4(_) => NetworkKind::V4, IpAddr::V6(_) => NetworkKind::V6, }; if pointer == 0 { Ok(LookupResult::new_not_found( self, prefix_len, address, LookupSource::Lookup, network_kind, )) } else { let data_offset = self.resolve_data_pointer(pointer)?; Ok(LookupResult::new_found( self, data_offset, prefix_len, address, LookupSource::Lookup, network_kind, )) } } #[inline(always)] fn find_address_in_tree_v4(&self, ip: u32) -> (usize, usize) { let buf = self.buf.as_ref(); let node_count = self.node_count; match self.record_size { 24 => find_address_in_tree_v4::(buf, self.ipv4_start, node_count, ip), 28 => find_address_in_tree_v4::(buf, self.ipv4_start, node_count, ip), 32 => find_address_in_tree_v4::(buf, self.ipv4_start, node_count, ip), _ => unreachable!("record_size is validated in Reader::from_source"), } } #[inline(always)] fn find_address_in_tree_v6(&self, ip: u128) -> (usize, usize) { let buf = self.buf.as_ref(); let node_count = self.node_count; match self.record_size { 24 => find_address_in_tree_v6::(buf, node_count, ip), 28 => find_address_in_tree_v6::(buf, node_count, ip), 32 => find_address_in_tree_v6::(buf, node_count, ip), _ => unreachable!("record_size is validated in Reader::from_source"), } } #[inline] fn start_node(&self, length: usize) -> usize { if length == 128 { 0 } else { self.ipv4_start } } #[inline] pub(crate) fn has_ipv4_subtree(&self) -> bool { self.metadata.ip_version == 6 && self.ipv4_start < self.node_count } /// Find the IPv4 start node and the bit depth at which it was found. /// Returns (node, depth) where depth is how far into the tree we traversed. fn find_ipv4_start(&self) -> (usize, usize) { if self.metadata.ip_version != 6 { return (0, 0); } // We are looking up an IPv4 address in an IPv6 tree. Skip over the // first 96 nodes. let mut node: usize = 0; let mut depth: usize = 0; for i in 0_u8..96 { if node >= self.node_count { depth = i as usize; break; } node = self.read_node(node, 0); depth = (i + 1) as usize; } (node, depth) } #[inline(always)] pub(crate) fn read_node(&self, node_number: usize, index: usize) -> usize { let buf = self.buf.as_ref(); let base_offset = node_number * self.node_byte_size; match self.record_size { 24 => { let offset = base_offset + index * 3; (buf[offset] as usize) << 16 | (buf[offset + 1] as usize) << 8 | buf[offset + 2] as usize } 28 => { let middle = if index != 0 { buf[base_offset + 3] & 0x0F } else { (buf[base_offset + 3] & 0xF0) >> 4 }; let offset = base_offset + index * 4; (middle as usize) << 24 | (buf[offset] as usize) << 16 | (buf[offset + 1] as usize) << 8 | buf[offset + 2] as usize } 32 => { let offset = base_offset + index * 4; (buf[offset] as usize) << 24 | (buf[offset + 1] as usize) << 16 | (buf[offset + 2] as usize) << 8 | buf[offset + 3] as usize } _ => unreachable!("record_size is validated in Reader::from_source"), } } /// Resolves a pointer from the search tree to an offset in the data section. #[inline] pub(crate) fn resolve_data_pointer(&self, pointer: usize) -> Result { let resolved = pointer .checked_sub(self.node_count) .and_then(|p| p.checked_sub(DATA_SECTION_SEPARATOR_SIZE)) .ok_or_else(|| { MaxMindDbError::invalid_database( "the MaxMind DB file's data pointer resolves to an invalid location", ) })?; let data_section_len = self .buf .as_ref() .len() .checked_sub(self.pointer_base) .ok_or_else(|| { MaxMindDbError::invalid_database( "the MaxMind DB file's data pointer resolves to an invalid location", ) })?; // Check bounds using pointer_base which marks the start of the data section if resolved >= data_section_len { return Err(MaxMindDbError::invalid_database( "the MaxMind DB file's data pointer resolves to an invalid location", )); } Ok(resolved) } /// Performs comprehensive validation of the MaxMind DB file. /// /// This method validates: /// - Metadata section: format versions, required fields, and value constraints /// - Search tree: traverses all networks to verify tree structure integrity /// - Data section separator: validates the 16-byte separator between tree and data /// - Data section: verifies all data records referenced by the search tree /// /// The verifier is stricter than the MaxMind DB specification and may return /// errors on some databases that are still readable by normal operations. /// This method is useful for: /// - Validating database files after download or generation /// - Debugging database corruption issues /// - Ensuring database integrity in critical applications /// /// Note: Verification traverses the entire database and may be slow on large files. /// The method is thread-safe and can be called on an active Reader. /// /// # Example /// /// ``` /// use maxminddb::Reader; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// reader.verify().expect("Database should be valid"); /// ``` pub fn verify(&self) -> Result<(), MaxMindDbError> { let metadata_start = find_metadata_start(self.buf.as_ref())?; self.verify_metadata(metadata_start)?; self.verify_database(metadata_start) } fn verify_metadata(&self, metadata_start: usize) -> Result<(), MaxMindDbError> { let m = &self.metadata; if m.binary_format_major_version != 2 { return Err(MaxMindDbError::invalid_database(format!( "binary_format_major_version - Expected: 2 Actual: {}", m.binary_format_major_version ))); } if m.binary_format_minor_version != 0 { return Err(MaxMindDbError::invalid_database(format!( "binary_format_minor_version - Expected: 0 Actual: {}", m.binary_format_minor_version ))); } if m.database_type.is_empty() { return Err(MaxMindDbError::invalid_database( "database_type - Expected: non-empty string Actual: \"\"", )); } if m.description.is_empty() { return Err(MaxMindDbError::invalid_database( "description - Expected: non-empty map Actual: {}", )); } if m.ip_version != 4 && m.ip_version != 6 { return Err(MaxMindDbError::invalid_database(format!( "ip_version - Expected: 4 or 6 Actual: {}", m.ip_version ))); } validate_record_size(m.record_size)?; if m.node_count == 0 { return Err(MaxMindDbError::invalid_database( "node_count - Expected: positive integer Actual: 0", )); } validate_search_tree_layout(self.pointer_base, metadata_start)?; Ok(()) } fn verify_database(&self, metadata_start: usize) -> Result<(), MaxMindDbError> { let offsets = self.verify_search_tree()?; self.verify_data_section_separator()?; self.verify_data_section(offsets, metadata_start) } fn verify_search_tree(&self) -> Result, MaxMindDbError> { let mut offsets = HashSet::new(); let opts = WithinOptions::default().include_networks_without_data(); // Maximum number of networks we can expect in a valid database. // A database with N nodes can have at most 2N data entries (each leaf node // can have data). We add some margin for safety. let max_iterations = self.node_count.saturating_mul(3); let mut iteration_count = 0usize; for result in self.networks(opts)? { let lookup = result?; if let Some(offset) = lookup.offset() { offsets.insert(offset); } iteration_count += 1; if iteration_count > max_iterations { return Err(MaxMindDbError::invalid_database(format!( "search tree appears to have a cycle or invalid structure (exceeded {max_iterations} iterations)" ))); } } Ok(offsets) } fn verify_data_section_separator(&self) -> Result<(), MaxMindDbError> { let separator_start = self.node_count * self.node_byte_size; let separator_end = separator_start + DATA_SECTION_SEPARATOR_SIZE; if separator_end > self.buf.as_ref().len() { return Err(MaxMindDbError::invalid_database_at( "data section separator extends past end of file", separator_start, )); } let separator = &self.buf.as_ref()[separator_start..separator_end]; for &b in separator { if b != 0 { return Err(MaxMindDbError::invalid_database_at( format!("unexpected byte in data separator: {separator:?}"), separator_start, )); } } Ok(()) } fn verify_data_section( &self, offsets: HashSet, metadata_start: usize, ) -> Result<(), MaxMindDbError> { let data_section = &self.buf.as_ref()[self.pointer_base..metadata_start]; // Verify each offset from the search tree points to valid, decodable data for &offset in &offsets { if offset >= data_section.len() { return Err(MaxMindDbError::invalid_database_at( format!( "search tree pointer is beyond data section (len: {})", data_section.len() ), offset, )); } let mut dec = decoder::Decoder::new(data_section, offset); // Try to skip/decode the value to verify it's valid if let Err(e) = dec.skip_value_for_verification() { return Err(MaxMindDbError::invalid_database_at( format!("decoding error: {e}"), offset, )); } } Ok(()) } } fn validate_record_size(record_size: u16) -> Result<(), MaxMindDbError> { if matches!(record_size, 24 | 28 | 32) { Ok(()) } else { Err(MaxMindDbError::invalid_database(format!( "record_size - Expected: 24, 28, or 32 Actual: {}", record_size ))) } } fn search_tree_size_bytes(node_count: usize, record_size: usize) -> Result { node_count .checked_mul(record_size) .map(|size| size / 4) .ok_or_else(|| { MaxMindDbError::invalid_database( "search tree size calculation overflowed or is impossibly large", ) }) } fn validate_search_tree_layout( pointer_base: usize, metadata_start: usize, ) -> Result<(), MaxMindDbError> { if pointer_base > metadata_start { return Err(MaxMindDbError::invalid_database( "the MaxMind DB file's search tree extends beyond the metadata section", )); } Ok(()) } trait SearchTreeRecord { fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize; } struct RecordSize24; impl SearchTreeRecord for RecordSize24 { #[inline(always)] fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize { let offset = node_number * 6 + index * 3; (buf[offset] as usize) << 16 | (buf[offset + 1] as usize) << 8 | buf[offset + 2] as usize } } struct RecordSize28; impl SearchTreeRecord for RecordSize28 { #[inline(always)] fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize { let base_offset = node_number * 7; let middle = if index == 0 { (buf[base_offset + 3] & 0xF0) >> 4 } else { buf[base_offset + 3] & 0x0F }; let offset = base_offset + index * 4; (middle as usize) << 24 | (buf[offset] as usize) << 16 | (buf[offset + 1] as usize) << 8 | buf[offset + 2] as usize } } struct RecordSize32; impl SearchTreeRecord for RecordSize32 { #[inline(always)] fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize { let offset = node_number * 8 + index * 4; (buf[offset] as usize) << 24 | (buf[offset + 1] as usize) << 16 | (buf[offset + 2] as usize) << 8 | buf[offset + 3] as usize } } #[inline(always)] fn find_address_in_tree_v4( buf: &[u8], start_node: usize, node_count: usize, ip: u32, ) -> (usize, usize) { let mut node = start_node; let mut prefix_len = 32; for i in 0..32 { if node >= node_count { prefix_len = i; break; } let bit = ((ip >> (31 - i)) & 1) as usize; node = R::read_node(buf, node, bit); } normalize_lookup_result(node, node_count, prefix_len) } #[inline(always)] fn find_address_in_tree_v6( buf: &[u8], node_count: usize, ip: u128, ) -> (usize, usize) { let mut node = 0; let mut prefix_len = 128; for i in 0..128 { if node >= node_count { prefix_len = i; break; } let bit = ((ip >> (127 - i)) & 1) as usize; node = R::read_node(buf, node, bit); } normalize_lookup_result(node, node_count, prefix_len) } // Map both "not found" outcomes onto pointer 0: // - `node == node_count`: the placeholder empty terminal in the search tree. // - `node < node_count`: bits exhausted while still on an internal node // (a partially-specified address that did not reach a record). // Anything strictly greater than `node_count` is a data-section pointer that // the caller must resolve via `resolve_data_pointer`. #[inline(always)] fn normalize_lookup_result(node: usize, node_count: usize, prefix_len: usize) -> (usize, usize) { if node <= node_count { (0, prefix_len) } else { (node, prefix_len) } } fn find_metadata_start(buf: &[u8]) -> Result { const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com"; memchr::memmem::rfind(buf, METADATA_START_MARKER) .map(|x| x + METADATA_START_MARKER.len()) .ok_or_else(|| { MaxMindDbError::invalid_database("could not find MaxMind DB metadata in file") }) } maxminddb-0.28.1/src/reader_test.rs000064400000000000000000001216141046102023000153220ustar 00000000000000use std::net::IpAddr; use ipnetwork::IpNetwork; use serde::Deserialize; use serde_json::json; use crate::geoip2; use crate::{MaxMindDbError, Reader, Within, WithinOptions}; const TEST_DATABASE_CONFIGS: &[(usize, usize)] = &[(24, 4), (28, 4), (32, 4), (24, 6), (28, 6), (32, 6)]; const TEST_RECORD_SIZES: &[usize] = &[24, 28, 32]; fn init_logger() { let _ = env_logger::try_init(); } fn open_test_data_reader(database: &str) -> Reader> { Reader::open_readfile(format!("test-data/test-data/{database}")) .unwrap_or_else(|e| panic!("failed to open test database '{database}': {e}")) } fn collect_networks>(iter: Within<'_, S>) -> Vec { iter.map(|result| { result .unwrap_or_else(|e| panic!("unexpected iterator error: {e}")) .network() .unwrap_or_else(|e| panic!("failed to build network from lookup result: {e}")) .to_string() }) .collect() } #[allow(clippy::float_cmp)] #[test] fn test_decoder() { init_logger(); #[allow(non_snake_case)] #[derive(Deserialize, Debug, Eq, PartialEq)] struct MapXType { arrayX: Vec, utf8_stringX: String, } #[allow(non_snake_case)] #[derive(Deserialize, Debug, Eq, PartialEq)] struct MapType { mapX: MapXType, } #[derive(Deserialize, Debug)] struct TestType<'a> { array: Vec, boolean: bool, bytes: &'a [u8], double: f64, float: f32, int32: i32, map: MapType, uint16: u16, uint32: u32, uint64: u64, uint128: u128, utf8_string: String, } let r = open_test_data_reader("MaxMind-DB-test-decoder.mmdb"); let ip: IpAddr = "1.1.1.0".parse().unwrap(); let lookup = r.lookup(ip).unwrap(); assert!(lookup.has_data(), "Expected IP to be found"); let result: TestType = lookup.decode().unwrap().unwrap(); assert_eq!(result.array, vec![1_u32, 2_u32, 3_u32]); assert!(result.boolean); assert_eq!(result.bytes, vec![0_u8, 0_u8, 0_u8, 42_u8]); assert_eq!(result.double, 42.123_456); assert_eq!(result.float, 1.1); assert_eq!(result.int32, -268_435_456); assert_eq!( result.map, MapType { mapX: MapXType { arrayX: vec![7, 8, 9], utf8_stringX: "hello".to_string(), }, } ); assert_eq!(result.uint16, 100); assert_eq!(result.uint32, 268_435_456); assert_eq!(result.uint64, 1_152_921_504_606_846_976); assert_eq!( result.uint128, 1_329_227_995_784_915_872_903_807_060_280_344_576 ); assert_eq!( result.utf8_string, "unicode! \u{262f} - \u{266b}".to_string() ); } #[test] fn test_pointers_in_metadata() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-metadata-pointers.mmdb"); assert_eq!( reader.metadata.database_type, "Lots of pointers in metadata" ); assert_eq!( reader.metadata.description["en"], "Lots of pointers in metadata" ); assert_eq!( reader.metadata.description["es"], "Lots of pointers in metadata" ); assert_eq!( reader.metadata.description["zh"], "Lots of pointers in metadata" ); reader.verify().unwrap(); } #[test] fn test_broken_database() { init_logger(); let r = open_test_data_reader("GeoIP2-City-Test-Broken-Double-Format.mmdb"); let ip: IpAddr = "2001:220::".parse().unwrap(); #[derive(Deserialize, Debug)] struct TestType {} let lookup = r.lookup(ip).unwrap(); if lookup.has_data() { match lookup.decode::() { Err(e) => assert!(matches!( e, MaxMindDbError::InvalidDatabase { .. } // Check variant, message might vary slightly )), Ok(_) => panic!("Unexpected success with broken data"), } } else { panic!("Expected IP to be found (with broken data)"); } } #[test] fn test_missing_database() { init_logger(); let r = Reader::open_readfile("file-does-not-exist.mmdb"); match r { Ok(_) => panic!("Received Reader when opening non-existent file"), Err(e) => assert!(matches!(e, MaxMindDbError::Io(_))), // Specific message might vary by OS/locale } } #[test] fn test_non_database() { init_logger(); let r = Reader::open_readfile("README.md"); match r { Ok(_) => panic!("Received Reader when opening a non-MMDB file"), Err(e) => assert!( matches!(&e, MaxMindDbError::InvalidDatabase { message, .. } if message == "could not find MaxMind DB metadata in file"), "Expected InvalidDatabase error with specific message, but got: {:?}", e ), } } #[test] fn test_invalid_node_count_database() { init_logger(); let r = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb"); match r { Ok(_) => panic!("Received Reader when opening database with invalid node count"), Err(e) => assert!( matches!(&e, MaxMindDbError::InvalidDatabase { message, .. } if message == "the MaxMind DB file's search tree extends beyond the metadata section"), "Expected InvalidDatabase error about search tree layout, but got: {:?}", e ), } } /// Create Reader by explicitly reading the entire file into a buffer. #[test] fn test_reader_readfile() { init_logger(); for (record_size, ip_version) in TEST_DATABASE_CONFIGS { let reader = open_test_data_reader(&format!( "MaxMind-DB-test-ipv{ip_version}-{record_size}.mmdb" )); check_metadata(&reader, *ip_version, *record_size); check_ip(&reader, *ip_version); } } #[test] #[cfg(feature = "mmap")] fn test_reader_mmap() { init_logger(); for (record_size, ip_version) in TEST_DATABASE_CONFIGS { let filename = format!("test-data/test-data/MaxMind-DB-test-ipv{ip_version}-{record_size}.mmdb"); // SAFETY: The test database file will not be modified during the test. let reader = unsafe { Reader::open_mmap(filename) }.unwrap(); check_metadata(&reader, *ip_version, *record_size); check_ip(&reader, *ip_version); } } #[test] fn test_lookup_city() { init_logger(); let reader = open_test_data_reader("GeoIP2-City-Test.mmdb"); let ip: IpAddr = "89.160.20.112".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let city: geoip2::City = lookup.decode().unwrap().unwrap(); let iso_code = city.country.iso_code; assert_eq!(iso_code, Some("SE")); } #[test] fn test_lookup_country() { init_logger(); let reader = open_test_data_reader("GeoIP2-Country-Test.mmdb"); let ip: IpAddr = "89.160.20.112".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let country: geoip2::Country = lookup.decode().unwrap().unwrap(); assert_eq!(country.country.iso_code, Some("SE")); assert_eq!(country.country.is_in_european_union, Some(true)); } #[test] fn test_lookup_connection_type() { init_logger(); let reader = open_test_data_reader("GeoIP2-Connection-Type-Test.mmdb"); let ip: IpAddr = "96.1.20.112".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let connection_type: geoip2::ConnectionType = lookup.decode().unwrap().unwrap(); assert_eq!(connection_type.connection_type, Some("Cable/DSL")); } #[test] fn test_lookup_annonymous_ip() { init_logger(); let reader = open_test_data_reader("GeoIP2-Anonymous-IP-Test.mmdb"); let ip: IpAddr = "81.2.69.123".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let anonymous_ip: geoip2::AnonymousIp = lookup.decode().unwrap().unwrap(); assert_eq!(anonymous_ip.is_anonymous, Some(true)); assert_eq!(anonymous_ip.is_public_proxy, Some(true)); assert_eq!(anonymous_ip.is_anonymous_vpn, Some(true)); assert_eq!(anonymous_ip.is_hosting_provider, Some(true)); assert_eq!(anonymous_ip.is_tor_exit_node, Some(true)) } #[test] fn test_lookup_density_income() { init_logger(); let reader = open_test_data_reader("GeoIP2-DensityIncome-Test.mmdb"); let ip: IpAddr = "5.83.124.123".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let density_income: geoip2::DensityIncome = lookup.decode().unwrap().unwrap(); assert_eq!(density_income.average_income, Some(32323)); assert_eq!(density_income.population_density, Some(1232)) } #[test] fn test_lookup_domain() { init_logger(); let reader = open_test_data_reader("GeoIP2-Domain-Test.mmdb"); let ip: IpAddr = "66.92.80.123".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let domain: geoip2::Domain = lookup.decode().unwrap().unwrap(); assert_eq!(domain.domain, Some("speakeasy.net")); } #[test] fn test_lookup_isp() { init_logger(); let reader = open_test_data_reader("GeoIP2-ISP-Test.mmdb"); let ip: IpAddr = "12.87.118.123".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let isp: geoip2::Isp = lookup.decode().unwrap().unwrap(); assert_eq!(isp.autonomous_system_number, Some(7018)); assert_eq!(isp.isp, Some("AT&T Services")); assert_eq!(isp.organization, Some("AT&T Worldnet Services")); } #[test] fn test_lookup_asn() { init_logger(); let reader = open_test_data_reader("GeoLite2-ASN-Test.mmdb"); let ip: IpAddr = "1.128.0.123".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let asn: geoip2::Asn = lookup.decode().unwrap().unwrap(); assert_eq!(asn.autonomous_system_number, Some(1221)); assert_eq!(asn.autonomous_system_organization, Some("Telstra Pty Ltd")); } #[test] fn test_lookup_network() { init_logger(); let reader = open_test_data_reader("GeoIP2-City-Test.mmdb"); // --- IPv4 Check (Known) --- let ip: IpAddr = "89.160.20.128".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data(), "Expected Some(City) for known IPv4"); let network = lookup.network().unwrap(); assert_eq!(network.prefix(), 25); let city: geoip2::City = lookup.decode().unwrap().unwrap(); assert!(!city.country.is_empty()); // --- IPv4 Check (Last Host, Known) --- let ip_last: IpAddr = "89.160.20.254".parse().unwrap(); let lookup_last = reader.lookup(ip_last).unwrap(); assert!(lookup_last.has_data(), "Expected Some(City) for last host"); assert_eq!(lookup_last.network().unwrap().prefix(), 25); // Should be same network // --- IPv6 Check (Not Found in Data) --- // This IP might resolve to a node in the tree, but that node might not point to data. let ip_v6_not_found: IpAddr = "2c0f:ff00::1".parse().unwrap(); let lookup_nf = reader.lookup(ip_v6_not_found).unwrap(); assert!( !lookup_nf.has_data(), "Expected not found for non-existent IP 2c0f:ff00::1" ); assert_eq!( lookup_nf.network().unwrap().prefix(), 6, "Expected valid prefix length for not-found IPv6" ); // --- IPv6 Check (Known Data) --- let ip_v6_known: IpAddr = "2001:218:85a3:0:0:8a2e:370:7334".parse().unwrap(); let lookup_v6 = reader.lookup(ip_v6_known).unwrap(); assert!(lookup_v6.has_data(), "Expected Some(City) for known IPv6"); assert_eq!( lookup_v6.network().unwrap().prefix(), 32, "Prefix length mismatch for known IPv6" ); let city_v6: geoip2::City = lookup_v6.decode().unwrap().unwrap(); assert!(!city_v6.country.is_empty()); } #[test] fn test_within_city() { init_logger(); let reader = open_test_data_reader("GeoIP2-City-Test.mmdb"); // --- Test iteration over entire DB ("::/0") --- let ip_net_all = IpNetwork::V6("::/0".parse().unwrap()); let mut iter_all: Within<_> = reader.within(ip_net_all, Default::default()).unwrap(); // Get the first item let first_item_result = iter_all.next(); assert!( first_item_result.is_some(), "Iterator over ::/0 yielded no items" ); let _first_lookup = first_item_result.unwrap().unwrap(); // Count the remaining items to check total count let mut n = 1; // Start at 1 since we already took the first item for item_result in iter_all { assert!(item_result.is_ok()); n += 1; } assert_eq!(n, 250); // --- Test iteration over a specific smaller network --- let specific = IpNetwork::V4("81.2.69.0/24".parse().unwrap()); let mut iter_specific: Within<_> = reader.within(specific, Default::default()).unwrap(); let expected = vec![ // In order of iteration: IpNetwork::V4("81.2.69.142/31".parse().unwrap()), IpNetwork::V4("81.2.69.144/28".parse().unwrap()), IpNetwork::V4("81.2.69.160/27".parse().unwrap()), IpNetwork::V4("81.2.69.192/28".parse().unwrap()), ]; let mut found_count = 0; // Use into_iter() to consume the vector for expected_net in expected.into_iter() { let item_res = iter_specific.next(); assert!( item_res.is_some(), "Expected more items in specific iterator" ); let lookup = item_res.unwrap().unwrap(); let network = lookup.network().unwrap(); assert_eq!( network, expected_net, "Mismatch in specific network iteration" ); // Check associated data for one of them if network.prefix() == 31 { // 81.2.69.142/31 let city: geoip2::City = lookup.decode().unwrap().unwrap(); assert!(!city.city.is_empty()); assert_eq!(city.city.geoname_id, Some(2643743)); // London } found_count += 1; } assert!( iter_specific.next().is_none(), "Specific iterator should be exhausted after expected items" ); assert_eq!( found_count, 4, "Expected exactly 4 networks in 81.2.69.0/24" ); } fn check_metadata>(reader: &Reader, ip_version: usize, record_size: usize) { let metadata = &reader.metadata; assert_eq!(metadata.binary_format_major_version, 2_u16); assert_eq!(metadata.binary_format_minor_version, 0_u16); assert!(metadata.build_epoch >= 1_397_457_605); assert_eq!(metadata.database_type, "Test".to_string()); assert_eq!( *metadata.description[&"en".to_string()], "Test Database".to_string() ); assert_eq!( *metadata.description[&"zh".to_string()], "Test Database Chinese".to_string() ); assert_eq!(metadata.ip_version, ip_version as u16); assert_eq!(metadata.languages, vec!["en".to_string(), "zh".to_string()]); if ip_version == 4 { assert_eq!(metadata.node_count, 163) } else { assert_eq!(metadata.node_count, 415) } assert_eq!(metadata.record_size, record_size as u16) } #[test] fn test_lookup_uses_cached_record_size_after_metadata_mutation() { init_logger(); let mut reader = open_test_data_reader("MaxMind-DB-test-ipv4-24.mmdb"); reader.metadata.record_size = 0; let lookup = reader.lookup("1.1.1.1".parse().unwrap()).unwrap(); assert!(lookup.has_data()); assert_eq!(lookup.network().unwrap().to_string(), "1.1.1.1/32"); } #[test] fn test_resolve_data_pointer_rejects_small_pointer() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-ipv4-24.mmdb"); let err = reader .resolve_data_pointer(reader.metadata.node_count as usize) .unwrap_err(); assert!(matches!(err, MaxMindDbError::InvalidDatabase { .. })); } fn check_ip>(reader: &Reader, ip_version: usize) { let subnets = match ip_version { 6 => [ "::1:ffff:ffff", "::2:0:0", "::2:0:0", "::2:0:0", "::2:0:0", "::2:0:40", "::2:0:40", "::2:0:40", "::2:0:50", "::2:0:50", "::2:0:50", "::2:0:58", "::2:0:58", ], _ => [ "1.1.1.1", "1.1.1.2", "1.1.1.2", "1.1.1.4", "1.1.1.4", "1.1.1.4", "1.1.1.4", "1.1.1.8", "1.1.1.8", "1.1.1.8", "1.1.1.16", "1.1.1.16", "1.1.1.16", ], }; #[derive(Deserialize, Debug, PartialEq)] struct IpType { ip: String, } // Test lookups that are expected to succeed for subnet in &subnets { let ip: IpAddr = subnet.parse().unwrap(); let lookup = reader.lookup(ip); assert!( lookup.is_ok(), "Lookup failed unexpectedly for {}: {:?}", subnet, lookup.err() ); let lookup = lookup.unwrap(); assert!( lookup.has_data(), "Lookup for {} returned not found unexpectedly", subnet ); let value: IpType = lookup.decode().unwrap().unwrap(); // The value stored is often the network address, not the specific IP looked up // We need to parse the found IP and the subnet IP to check containment or equality. // For the specific MaxMind-DB-test-ipv* files, the stored value IS the looked-up IP string. assert_eq!(value.ip, *subnet); } // Test lookups that are expected to return "not found" let no_record = ["1.1.1.33", "255.254.253.123", "89fa::"]; for &address in &no_record { if ip_version == 4 && address == "89fa::" { continue; // Skip IPv6 address if testing IPv4 db } if ip_version == 6 && address != "89fa::" { continue; // Skip IPv4 addresses if testing IPv6 db } let ip: IpAddr = address.parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!( !lookup.has_data(), "Expected not found for address {}, but it was found", address ); } } #[test] fn test_json_serialize() { init_logger(); let reader = open_test_data_reader("GeoIP2-City-Test.mmdb"); let ip: IpAddr = "89.160.20.112".parse().unwrap(); let lookup = reader.lookup(ip).unwrap(); assert!(lookup.has_data()); let city: geoip2::City = lookup.decode().unwrap().unwrap(); let json_value = json!(city); let json_string = json_value.to_string(); let expected_json_str = r#"{"city":{"geoname_id":2694762,"names":{"de":"Linköping","en":"Linköping","fr":"Linköping","ja":"リンシェーピング","zh-CN":"林雪平"}},"continent":{"code":"EU","geoname_id":6255148,"names":{"de":"Europa","en":"Europe","es":"Europa","fr":"Europe","ja":"ヨーロッパ","pt-BR":"Europa","ru":"Европа","zh-CN":"欧洲"}},"country":{"geoname_id":2661886,"is_in_european_union":true,"iso_code":"SE","names":{"de":"Schweden","en":"Sweden","es":"Suecia","fr":"Suède","ja":"スウェーデン王国","pt-BR":"Suécia","ru":"Швеция","zh-CN":"瑞典"}},"location":{"accuracy_radius":76,"latitude":58.4167,"longitude":15.6167,"time_zone":"Europe/Stockholm"},"registered_country":{"geoname_id":2921044,"is_in_european_union":true,"iso_code":"DE","names":{"de":"Deutschland","en":"Germany","es":"Alemania","fr":"Allemagne","ja":"ドイツ連邦共和国","pt-BR":"Alemanha","ru":"Германия","zh-CN":"德国"}},"subdivisions":[{"geoname_id":2685867,"iso_code":"E","names":{"en":"Östergötland County","fr":"Comté d'Östergötland"}}]}"#; let expected_value: serde_json::Value = serde_json::from_str(expected_json_str).unwrap(); assert_eq!(json_value, expected_value); assert_eq!(json_string, expected_json_str); } // ============================================================================ // Iteration Options Tests // ============================================================================ /// Test networks() method iterates over entire database #[test] fn test_networks() { init_logger(); for (record_size, ip_version) in TEST_DATABASE_CONFIGS { let reader = open_test_data_reader(&format!( "MaxMind-DB-test-ipv{ip_version}-{record_size}.mmdb" )); for result in reader.networks(Default::default()).unwrap() { let lookup = result.unwrap(); assert!( lookup.has_data(), "networks() should only yield found records by default" ); #[derive(Deserialize)] struct IpRecord { ip: String, } let record: IpRecord = lookup.decode().unwrap().unwrap(); let network = lookup.network().unwrap(); assert_eq!( record.ip, network.ip().to_string(), "record IP should match network IP" ); } } } /// Test that default options skip aliased networks #[test] fn test_default_skips_aliases() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-mixed-24.mmdb"); // Without IncludeAliasedNetworks, iterating over ::/0 should yield IPv4 networks only once let ip_net_all = IpNetwork::V6("::/0".parse().unwrap()); let expected_without_aliases = vec![ "1.1.1.1/32", "1.1.1.2/31", "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", "1.1.1.32/32", "::1:ffff:ffff/128", "::2:0:0/122", "::2:0:40/124", "::2:0:50/125", "::2:0:58/127", ]; let networks = collect_networks(reader.within(ip_net_all, Default::default()).unwrap()); assert_eq!(networks, expected_without_aliases); } /// Test IncludeAliasedNetworks option #[test] fn test_include_aliased_networks() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-mixed-24.mmdb"); let ip_net_all = IpNetwork::V6("::/0".parse().unwrap()); let opts = WithinOptions::default().include_aliased_networks(); // With IncludeAliasedNetworks, we should see IPv4 networks via various IPv6 prefixes let expected_with_aliases = vec![ "1.1.1.1/32", "1.1.1.2/31", "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", "1.1.1.32/32", "::1:ffff:ffff/128", "::2:0:0/122", "::2:0:40/124", "::2:0:50/125", "::2:0:58/127", "::ffff:1.1.1.1/128", "::ffff:1.1.1.2/127", "::ffff:1.1.1.4/126", "::ffff:1.1.1.8/125", "::ffff:1.1.1.16/124", "::ffff:1.1.1.32/128", "2001:0:101:101::/64", "2001:0:101:102::/63", "2001:0:101:104::/62", "2001:0:101:108::/61", "2001:0:101:110::/60", "2001:0:101:120::/64", "2002:101:101::/48", "2002:101:102::/47", "2002:101:104::/46", "2002:101:108::/45", "2002:101:110::/44", "2002:101:120::/48", ]; let networks = collect_networks(reader.within(ip_net_all, opts).unwrap()); assert_eq!(networks, expected_with_aliases); } /// Test IncludeNetworksWithoutData option #[test] fn test_include_networks_without_data() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-mixed-24.mmdb"); // Using 1.0.0.0/8 like the Go tests let cidr: IpNetwork = "1.0.0.0/8".parse().unwrap(); let opts = WithinOptions::default().include_networks_without_data(); let expected = vec![ "1.0.0.0/16", "1.1.0.0/24", "1.1.1.0/32", "1.1.1.1/32", "1.1.1.2/31", "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", "1.1.1.32/32", "1.1.1.33/32", "1.1.1.34/31", "1.1.1.36/30", "1.1.1.40/29", "1.1.1.48/28", "1.1.1.64/26", "1.1.1.128/25", "1.1.2.0/23", "1.1.4.0/22", "1.1.8.0/21", "1.1.16.0/20", "1.1.32.0/19", "1.1.64.0/18", "1.1.128.0/17", "1.2.0.0/15", "1.4.0.0/14", "1.8.0.0/13", "1.16.0.0/12", "1.32.0.0/11", "1.64.0.0/10", "1.128.0.0/9", ]; let mut networks: Vec = Vec::new(); let mut found_count = 0; let mut not_found_count = 0; for result in reader.within(cidr, opts).unwrap() { let lookup = result.unwrap(); networks.push(lookup.network().unwrap().to_string()); if lookup.has_data() { found_count += 1; } else { not_found_count += 1; } } assert_eq!(networks, expected); assert!( not_found_count > 0, "Should have some networks without data" ); assert!(found_count > 0, "Should have some networks with data"); } /// Test SkipEmptyValues option #[test] fn test_skip_empty_values() { init_logger(); let reader = open_test_data_reader("GeoIP2-Anonymous-IP-Test.mmdb"); // Count networks without SkipEmptyValues let mut count_without_skip = 0; let mut empty_count = 0; for result in reader.networks(Default::default()).unwrap() { let lookup = result.unwrap(); count_without_skip += 1; if lookup.has_data() { let data: std::collections::BTreeMap = lookup.decode().unwrap().unwrap(); if data.is_empty() { empty_count += 1; } } } // Count networks with SkipEmptyValues let mut count_with_skip = 0; let opts = WithinOptions::default().skip_empty_values(); for result in reader.networks(opts).unwrap() { let lookup = result.unwrap(); count_with_skip += 1; if lookup.has_data() { let data: std::collections::BTreeMap = lookup.decode().unwrap().unwrap(); assert!( !data.is_empty(), "Should not see empty maps with skip_empty_values" ); } } // Verify the option works assert!( empty_count > 0, "Test database should have empty values, found {} empty out of {}", empty_count, count_without_skip ); assert_eq!( count_without_skip - empty_count, count_with_skip, "SkipEmptyValues should skip exactly the empty values" ); } /// Test SkipEmptyValues with other options combined #[test] fn test_skip_empty_values_with_other_options() { init_logger(); let reader = open_test_data_reader("GeoIP2-Anonymous-IP-Test.mmdb"); // Test with IncludeNetworksWithoutData - should still skip empty maps let opts = WithinOptions::default() .include_networks_without_data() .skip_empty_values(); let mut count = 0; for result in reader.networks(opts).unwrap() { let lookup = result.unwrap(); count += 1; if lookup.has_data() { let data: std::collections::BTreeMap = lookup.decode().unwrap().unwrap(); assert!( !data.is_empty(), "Should not see empty maps even with other options" ); } } assert!(count > 0, "Should have some networks"); } /// Test various NetworksWithin scenarios matching Go tests #[test] fn test_networks_within_scenarios() { init_logger(); struct TestCase { network: &'static str, database: &'static str, expected: Vec<&'static str>, } let test_cases = vec![ TestCase { network: "0.0.0.0/0", database: "ipv4", expected: vec![ "1.1.1.1/32", "1.1.1.2/31", "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", "1.1.1.32/32", ], }, TestCase { network: "1.1.1.1/30", database: "ipv4", expected: vec!["1.1.1.1/32", "1.1.1.2/31"], }, TestCase { network: "1.1.1.2/31", database: "ipv4", expected: vec!["1.1.1.2/31"], }, TestCase { network: "1.1.1.1/32", database: "ipv4", expected: vec!["1.1.1.1/32"], }, TestCase { network: "1.1.1.2/32", database: "ipv4", expected: vec!["1.1.1.2/31"], }, TestCase { network: "1.1.1.3/32", database: "ipv4", expected: vec!["1.1.1.2/31"], }, TestCase { network: "1.1.1.19/32", database: "ipv4", expected: vec!["1.1.1.16/28"], }, TestCase { network: "255.255.255.0/24", database: "ipv4", expected: vec![], }, TestCase { network: "1.1.1.1/32", database: "mixed", expected: vec!["1.1.1.1/32"], }, TestCase { network: "255.255.255.0/24", database: "mixed", expected: vec![], }, TestCase { network: "::1:ffff:ffff/128", database: "ipv6", expected: vec!["::1:ffff:ffff/128"], }, TestCase { network: "::/0", database: "ipv6", expected: vec![ "::1:ffff:ffff/128", "::2:0:0/122", "::2:0:40/124", "::2:0:50/125", "::2:0:58/127", ], }, TestCase { network: "::2:0:40/123", database: "ipv6", expected: vec!["::2:0:40/124", "::2:0:50/125", "::2:0:58/127"], }, TestCase { network: "0:0:0:0:0:ffff:ffff:ff00/120", database: "ipv6", expected: vec![], }, TestCase { network: "0.0.0.0/0", database: "mixed", expected: vec![ "1.1.1.1/32", "1.1.1.2/31", "1.1.1.4/30", "1.1.1.8/29", "1.1.1.16/28", "1.1.1.32/32", ], }, TestCase { network: "1.1.1.16/28", database: "mixed", expected: vec!["1.1.1.16/28"], }, TestCase { network: "1.1.1.4/30", database: "ipv4", expected: vec!["1.1.1.4/30"], }, ]; for record_size in TEST_RECORD_SIZES { for test in &test_cases { let reader = open_test_data_reader(&format!( "MaxMind-DB-test-{}-{}.mmdb", test.database, record_size )); let cidr: IpNetwork = test.network.parse().unwrap(); let networks = collect_networks(reader.within(cidr, Default::default()).unwrap()); let expected: Vec = test.expected.iter().map(|s| s.to_string()).collect(); assert_eq!( networks, expected, "Mismatch for {} in {}-{}: expected {:?}, got {:?}", test.network, test.database, record_size, expected, networks ); } } } /// Test GeoIP database-specific NetworksWithin #[test] fn test_geoip_networks_within() { init_logger(); let reader = open_test_data_reader("GeoIP2-Country-Test.mmdb"); let cidr: IpNetwork = "81.2.69.128/26".parse().unwrap(); let expected = vec!["81.2.69.142/31", "81.2.69.144/28", "81.2.69.160/27"]; let networks = collect_networks(reader.within(cidr, Default::default()).unwrap()); assert_eq!(networks, expected); } #[test] fn test_within_rejects_ipv6_cidr_for_ipv4_database() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-ipv4-24.mmdb"); for cidr in ["::/0", "::ffff:0.0.0.0/96", "2001::/16"] { let cidr: IpNetwork = cidr.parse().unwrap(); let result = reader.within(cidr, Default::default()); assert!( matches!( result, Err(MaxMindDbError::InvalidInput { ref message }) if message == "cannot iterate IPv6 network in IPv4-only database" ), "Expected InvalidInput for IPv6 CIDR in IPv4 database, got {:?}", result ); } } /// Test that verify() succeeds on valid databases (matching Go's TestVerifyOnGoodDatabases) #[test] fn test_verify_good_databases() { init_logger(); let databases = [ "GeoIP2-Anonymous-IP-Test.mmdb", "GeoIP2-City-Test.mmdb", "GeoIP2-Connection-Type-Test.mmdb", "GeoIP2-Country-Test.mmdb", "GeoIP2-Domain-Test.mmdb", "GeoIP2-ISP-Test.mmdb", "GeoIP2-Precision-Enterprise-Test.mmdb", "MaxMind-DB-no-ipv4-search-tree.mmdb", "MaxMind-DB-string-value-entries.mmdb", "MaxMind-DB-test-decoder.mmdb", "MaxMind-DB-test-ipv4-24.mmdb", "MaxMind-DB-test-ipv4-28.mmdb", "MaxMind-DB-test-ipv4-32.mmdb", "MaxMind-DB-test-ipv6-24.mmdb", "MaxMind-DB-test-ipv6-28.mmdb", "MaxMind-DB-test-ipv6-32.mmdb", "MaxMind-DB-test-metadata-pointers.mmdb", "MaxMind-DB-test-mixed-24.mmdb", "MaxMind-DB-test-mixed-28.mmdb", "MaxMind-DB-test-mixed-32.mmdb", "MaxMind-DB-test-nested.mmdb", ]; for database in &databases { let reader = open_test_data_reader(database); reader .verify() .unwrap_or_else(|e| panic!("verify() failed for {}: {}", database, e)); } } /// Test that verify() returns errors on broken databases (matching Go's TestVerifyOnBrokenDatabases) #[test] fn test_verify_broken_double_format() { init_logger(); let reader = open_test_data_reader("GeoIP2-City-Test-Broken-Double-Format.mmdb"); let result = reader.verify(); assert!( result.is_err(), "Expected verify() to return error for Broken-Double-Format, but it succeeded" ); } #[test] fn test_verify_broken_pointers() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-broken-pointers-24.mmdb"); let result = reader.verify(); assert!( matches!( result, Err(MaxMindDbError::InvalidDatabase { ref message, .. }) if message == "the MaxMind DB file's data pointer resolves to an invalid location" ), "Expected specific InvalidDatabase error for broken-pointers, got {:?}", result ); } #[test] fn test_verify_broken_search_tree() { init_logger(); let reader = open_test_data_reader("MaxMind-DB-test-broken-search-tree-24.mmdb"); let result = reader.verify(); assert!( matches!( result, Err(MaxMindDbError::InvalidDatabase { ref message, .. }) if message.contains("search tree appears to have a cycle or invalid structure") ), "Expected specific InvalidDatabase error for broken-search-tree, got {:?}", result ); } #[test] fn test_verify_rejects_truncated_scalar_value() { init_logger(); let source_path = "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb"; let reader = open_test_data_reader("MaxMind-DB-test-ipv4-24.mmdb"); let lookup = reader.lookup("1.1.1.32".parse().unwrap()).unwrap(); let data_offset = lookup.offset().expect("expected data offset"); let mut bytes = std::fs::read(source_path).unwrap(); let record_start = reader.pointer_base + data_offset; let string_value = b"1.1.1.32"; let relative_value_offset = bytes[record_start..] .windows(string_value.len()) .position(|window| window == string_value) .expect("expected terminal string payload in fixture record"); let string_ctrl_offset = record_start + relative_value_offset - 1; assert_eq!( bytes[string_ctrl_offset], 0x48, "unexpected string control byte in source fixture" ); // Inflate the terminal string from length 8 to length 28 without adding // bytes, so verification must catch the truncated payload. bytes[string_ctrl_offset] = 0x5c; let reader = Reader::from_source(bytes).unwrap(); let result = reader.verify(); assert!( matches!(result, Err(MaxMindDbError::InvalidDatabase { .. })), "Expected InvalidDatabase error for truncated scalar payload, got {:?}", result ); } /// Test that size hints are properly returned for sequences and maps #[test] fn test_size_hints() { use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor}; use std::fmt; init_logger(); // Wrapper that captures size_hint for sequences struct SeqSizeHint { hint: Option, values: Vec, } impl<'de> Deserialize<'de> for SeqSizeHint { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct V; impl<'de> Visitor<'de> for V { type Value = SeqSizeHint; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("sequence") } fn visit_seq>(self, mut seq: A) -> Result { let hint = seq.size_hint(); let mut values = Vec::new(); while let Some(v) = seq.next_element()? { values.push(v); } Ok(SeqSizeHint { hint, values }) } } deserializer.deserialize_seq(V) } } // Wrapper that captures size_hint for maps struct MapSizeHint { hint: Option, len: usize, } impl<'de> Deserialize<'de> for MapSizeHint { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct V; impl<'de> Visitor<'de> for V { type Value = MapSizeHint; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("map") } fn visit_map>(self, mut map: A) -> Result { let hint = map.size_hint(); let mut len = 0; while map.next_entry::()?.is_some() { len += 1; } Ok(MapSizeHint { hint, len }) } } deserializer.deserialize_map(V) } } #[derive(Deserialize)] struct TestType { array: SeqSizeHint, map: MapSizeHint, } let r = open_test_data_reader("MaxMind-DB-test-decoder.mmdb"); let ip: IpAddr = "1.1.1.0".parse().unwrap(); let lookup = r.lookup(ip).unwrap(); assert!(lookup.has_data()); let result: TestType = lookup.decode().unwrap().unwrap(); // Verify array size hint matches actual length assert_eq!(result.array.hint, Some(3)); assert_eq!(result.array.values, vec![1, 2, 3]); // Verify map size hint matches actual entry count assert_eq!(result.map.hint, Some(result.map.len)); assert!(result.map.len > 0, "Map should have entries"); } /// Test that deserialize_ignored_any efficiently skips values #[test] fn test_ignored_any() { use serde::de::IgnoredAny; init_logger(); // Struct that only reads some fields, ignoring others via IgnoredAny #[allow(dead_code)] #[derive(Deserialize, Debug)] struct PartialRead { utf8_string: String, // These fields use IgnoredAny to skip decoding array: IgnoredAny, map: IgnoredAny, uint128: IgnoredAny, } let r = open_test_data_reader("MaxMind-DB-test-decoder.mmdb"); let ip: IpAddr = "1.1.1.0".parse().unwrap(); let lookup = r.lookup(ip).unwrap(); assert!(lookup.has_data()); let result: PartialRead = lookup.decode().unwrap().unwrap(); assert_eq!(result.utf8_string, "unicode! ☯ - ♫"); } /// Test that string values can be deserialized into enums #[test] fn test_enum_deserialization() { init_logger(); #[derive(Deserialize, Debug, PartialEq)] enum ConnType { #[serde(rename = "Cable/DSL")] CableDsl, } #[derive(Deserialize)] struct Record { connection_type: ConnType, } let r = open_test_data_reader("GeoIP2-Connection-Type-Test.mmdb"); let ip: IpAddr = "96.1.20.112".parse().unwrap(); let lookup = r.lookup(ip).unwrap(); assert!(lookup.has_data()); let result: Record = lookup.decode().unwrap().unwrap(); assert_eq!(result.connection_type, ConnType::CableDsl); } /// Test serde flatten attribute with HashMap /// /// Real-world GeoIP2/GeoLite2 databases don't contain u128 values, so /// `#[serde(flatten)]` works without issues. #[test] fn test_serde_flatten() { use serde::de::IgnoredAny; init_logger(); #[derive(Deserialize, Debug)] struct PartialCountry { continent: Continent, #[serde(flatten)] _rest: std::collections::HashMap, } #[derive(Deserialize, Debug)] struct Continent { code: String, } let r = open_test_data_reader("GeoIP2-Country-Test.mmdb"); let ip: IpAddr = "81.2.69.160".parse().unwrap(); let lookup = r.lookup(ip).unwrap(); assert!(lookup.has_data()); let result: PartialCountry = lookup.decode().unwrap().unwrap(); assert_eq!(result.continent.code, "EU"); } maxminddb-0.28.1/src/result.rs000064400000000000000000000567101046102023000143430ustar 00000000000000//! Lookup result types for deferred decoding. //! //! This module provides `LookupResult`, which enables lazy decoding of //! MaxMind DB records. Instead of immediately deserializing data, you //! get a lightweight handle that can be decoded later or navigated //! selectively via paths. use std::net::IpAddr; use ipnetwork::IpNetwork; use serde::Deserialize; use crate::decoder::{TYPE_ARRAY, TYPE_MAP}; use crate::error::MaxMindDbError; use crate::reader::Reader; /// The result of looking up an IP address in a MaxMind DB. /// /// This is a lightweight handle (~40 bytes) that stores the lookup result /// without immediately decoding the data. You can: /// /// - Check if data exists with [`has_data()`](Self::has_data) /// - Get the network containing the IP with [`network()`](Self::network) /// - Decode the full record with [`decode()`](Self::decode) /// - Decode a specific path with [`decode_path()`](Self::decode_path) /// /// # Example /// /// ``` /// use maxminddb::{geoip2, path, Reader}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// /// let result = reader.lookup(ip).unwrap(); /// /// if result.has_data() { /// // Full decode /// let city: geoip2::City = result.decode().unwrap().unwrap(); /// /// // Or selective decode via path /// let country_code: Option = result /// .decode_path(&path!["country", "iso_code"]) /// .unwrap(); /// println!("Country: {:?}", country_code); /// } /// ``` #[derive(Debug, Clone, Copy)] pub struct LookupResult<'a, S: AsRef<[u8]>> { reader: &'a Reader, /// Offset into the data section, or None if not found. data_offset: Option, prefix_len: u8, ip: IpAddr, source: LookupSource, network_kind: NetworkKind, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum LookupSource { Lookup, Iter, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum NetworkKind { V4, V6, V4InV6Subtree, } impl<'a, S: AsRef<[u8]>> LookupResult<'a, S> { #[inline] fn decoder(&self, offset: usize) -> super::decoder::Decoder<'a> { let buf = &self.reader.buf.as_ref()[self.reader.pointer_base..]; super::decoder::Decoder::new(buf, offset) } /// Creates a new LookupResult for a found IP. pub(crate) fn new_found( reader: &'a Reader, data_offset: usize, prefix_len: u8, ip: IpAddr, source: LookupSource, network_kind: NetworkKind, ) -> Self { LookupResult { reader, data_offset: Some(data_offset), prefix_len, ip, source, network_kind, } } /// Creates a new LookupResult for an IP not in the database. pub(crate) fn new_not_found( reader: &'a Reader, prefix_len: u8, ip: IpAddr, source: LookupSource, network_kind: NetworkKind, ) -> Self { LookupResult { reader, data_offset: None, prefix_len, ip, source, network_kind, } } /// Returns true if the database contains data for this IP address. /// /// Note that `false` means the database has no data for this IP, /// which is different from an error during lookup. #[inline] pub fn has_data(&self) -> bool { self.data_offset.is_some() } /// Returns the network containing the looked-up IP address. /// /// This is the most specific network in the database that contains /// the IP, regardless of whether data was found. /// /// The returned network preserves the IP version of the original lookup: /// - IPv4 lookups return IPv4 networks (unless the match occurs before the /// IPv4 subtree begins, see below) /// - IPv6 lookups return IPv6 networks (including IPv4-mapped addresses) /// /// Special case: If an IPv4 address is looked up in an IPv6 database but /// the matching record is above the IPv4 subtree (e.g., a database with /// no IPv4 subtree), an IPv6 network is returned since there's no valid /// IPv4 representation. pub fn network(&self) -> Result { let (ip, prefix) = match (self.source, self.network_kind, self.ip) { (_, NetworkKind::V4, IpAddr::V4(v4)) => (IpAddr::V4(v4), self.prefix_len), (_, NetworkKind::V4InV6Subtree, IpAddr::V4(v4)) => ( IpAddr::V4(v4), self.prefix_len - self.reader.ipv4_start_bit_depth as u8, ), (LookupSource::Lookup, NetworkKind::V6, IpAddr::V4(_)) => { use std::net::Ipv6Addr; (IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.prefix_len) } (_, NetworkKind::V6, IpAddr::V6(v6)) => (IpAddr::V6(v6), self.prefix_len), (_, _, ip) => unreachable!("unexpected lookup result state for network: {ip:?}"), }; // Mask the IP to the network address let network_ip = mask_ip(ip, prefix); IpNetwork::new(network_ip, prefix).map_err(MaxMindDbError::InvalidNetwork) } /// Returns the data section offset if found, for use as a cache key. /// /// Multiple IP addresses often point to the same data record. This /// offset can be used to deduplicate decoding or cache results. /// /// Returns `None` if the IP was not found. #[inline] pub fn offset(&self) -> Option { self.data_offset } /// Decodes the full record into the specified type. /// /// Returns: /// - `Ok(Some(T))` if found and successfully decoded /// - `Ok(None)` if the IP was not found in the database /// - `Err(...)` if decoding fails /// /// # Example /// /// ``` /// use maxminddb::{Reader, geoip2}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// /// let result = reader.lookup(ip).unwrap(); /// if let Some(city) = result.decode::()? { /// println!("Found city data"); /// } /// # Ok::<(), maxminddb::MaxMindDbError>(()) /// ``` pub fn decode(&self) -> Result, MaxMindDbError> where T: Deserialize<'a>, { let Some(offset) = self.data_offset else { return Ok(None); }; let mut decoder = self.decoder(offset); T::deserialize(&mut decoder).map(Some) } /// Decodes a value at a specific path within the record. /// /// Returns: /// - `Ok(Some(T))` if the path exists and was successfully decoded /// - `Ok(None)` if the path doesn't exist (key missing, index out of bounds) /// - `Err(...)` if there's a type mismatch during navigation (e.g., `Key` on an array) /// /// If `has_data() == false`, returns `Ok(None)`. /// /// # Path Elements /// /// - `PathElement::Key("name")` - Navigate into a map by key /// - `PathElement::Index(0)` - Navigate into an array by index (0 = first element) /// - `PathElement::IndexFromEnd(0)` - Navigate from the end (0 = last element) /// /// # Example /// /// ``` /// use maxminddb::{path, Reader}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// /// let result = reader.lookup(ip).unwrap(); /// /// // Navigate to country.iso_code /// let iso_code: Option = result /// .decode_path(&path!["country", "iso_code"]) /// .unwrap(); /// /// // Navigate to subdivisions[0].names.en /// let subdiv_name: Option = result /// .decode_path(&path!["subdivisions", 0, "names", "en"]) /// .unwrap(); /// ``` pub fn decode_path(&self, path: &[PathElement<'_>]) -> Result, MaxMindDbError> where T: Deserialize<'a>, { let Some(offset) = self.data_offset else { return Ok(None); }; let mut decoder = self.decoder(offset); // Navigate through the path, tracking position for error context for (i, element) in path.iter().enumerate() { // Closure to add path context to errors during navigation. // Shows path up to and including the current element where the error occurred. let with_path = |e| add_path_context(e, &path[..=i]); match *element { PathElement::Key(key) => { let (_, type_num) = decoder.peek_type().map_err(with_path)?; if type_num != TYPE_MAP { return Err(MaxMindDbError::decoding_at_path( format!("expected map for Key(\"{key}\"), got type {type_num}"), decoder.offset(), render_path(&path[..=i]), )); } // Consume the map header and get size let size = decoder.consume_map_header().map_err(with_path)?; let mut found = false; let key_bytes = key.as_bytes(); for _ in 0..size { let k = decoder.read_str_as_bytes().map_err(with_path)?; if k == key_bytes { found = true; break; } else { decoder.skip_value().map_err(with_path)?; } } if !found { return Ok(None); } } PathElement::Index(idx) | PathElement::IndexFromEnd(idx) => { let (_, type_num) = decoder.peek_type().map_err(with_path)?; if type_num != TYPE_ARRAY { let elem = match *element { PathElement::Index(i) => format!("Index({i})"), PathElement::IndexFromEnd(i) => format!("IndexFromEnd({i})"), PathElement::Key(_) => unreachable!(), }; return Err(MaxMindDbError::decoding_at_path( format!("expected array for {elem}, got type {type_num}"), decoder.offset(), render_path(&path[..=i]), )); } // Consume the array header and get size let size = decoder.consume_array_header().map_err(with_path)?; if idx >= size { return Ok(None); // Out of bounds } let actual_idx = match *element { PathElement::Index(i) => i, PathElement::IndexFromEnd(i) => size - 1 - i, PathElement::Key(_) => unreachable!(), }; // Skip to the target index for _ in 0..actual_idx { decoder.skip_value().map_err(with_path)?; } } } } // Decode the value at the current position T::deserialize(&mut decoder) .map(Some) .map_err(|e| add_path_context(e, path)) } } /// Adds path context to a Decoding error if it doesn't already have one. fn add_path_context(err: MaxMindDbError, path: &[PathElement<'_>]) -> MaxMindDbError { match err { MaxMindDbError::Decoding { message, offset, path: None, } => MaxMindDbError::Decoding { message, offset, path: Some(render_path(path)), }, _ => err, } } /// Renders path elements as a JSON-pointer-like string (e.g., "/city/names/0"). fn render_path(path: &[PathElement<'_>]) -> String { use std::fmt::Write; let mut s = String::new(); for elem in path { s.push('/'); match elem { PathElement::Key(k) => s.push_str(k), PathElement::Index(i) => write!(s, "{i}").unwrap(), PathElement::IndexFromEnd(i) => write!(s, "{}", -((*i as isize) + 1)).unwrap(), } } s } /// A path element for navigating into nested data structures. /// /// Used with [`LookupResult::decode_path()`] to selectively decode /// specific fields without parsing the entire record. /// /// # Creating Path Elements /// /// You can create path elements directly or use the [`path!`](crate::path) macro /// for a more convenient syntax: /// /// ``` /// use maxminddb::{path, PathElement}; /// /// // Direct construction /// let path = [PathElement::Key("country"), PathElement::Key("iso_code")]; /// /// // Using the macro - string literals become Keys, integers become Indexes /// let path = path!["country", "iso_code"]; /// let path = path!["subdivisions", 0, "names"]; // Mixed keys and indexes /// let path = path!["array", -1]; // Negative indexes count from the end /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum PathElement<'a> { /// Navigate into a map by key. Key(&'a str), /// Navigate into an array by index (0-based from the start). /// /// - `Index(0)` - first element /// - `Index(1)` - second element Index(usize), /// Navigate into an array by index from the end. /// /// - `IndexFromEnd(0)` - last element /// - `IndexFromEnd(1)` - second-to-last element IndexFromEnd(usize), } impl<'a> From<&'a str> for PathElement<'a> { fn from(s: &'a str) -> Self { PathElement::Key(s) } } impl From for PathElement<'_> { /// Converts an integer to a path element. /// /// - Non-negative values become `Index(n)` /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element fn from(n: i32) -> Self { signed_index_to_path_element(n as isize) } } impl From for PathElement<'_> { fn from(n: usize) -> Self { PathElement::Index(n) } } impl From for PathElement<'_> { /// Converts a signed integer to a path element. /// /// - Non-negative values become `Index(n)` /// - Negative values become `IndexFromEnd(-n - 1)`, so `-1` is the last element /// - `isize::MIN` saturates to `IndexFromEnd(usize::MAX)` because its /// absolute value is unrepresentable as `isize` fn from(n: isize) -> Self { signed_index_to_path_element(n) } } fn signed_index_to_path_element<'a>(n: isize) -> PathElement<'a> { if n >= 0 { PathElement::Index(n as usize) } else { let index = n .checked_neg() .and_then(|n| n.checked_sub(1)) .map(|n| n as usize) .unwrap_or(usize::MAX); PathElement::IndexFromEnd(index) } } /// Creates a path for use with [`LookupResult::decode_path()`](crate::LookupResult::decode_path). /// /// This macro provides a convenient way to construct paths with mixed string keys /// and integer indexes. /// /// # Syntax /// /// - String literals become [`PathElement::Key`] /// - Non-negative integers become [`PathElement::Index`] /// - Negative integers become [`PathElement::IndexFromEnd`] (e.g., `-1` is the last element) /// /// # Examples /// /// ``` /// use maxminddb::{Reader, path}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); /// let result = reader.lookup(ip).unwrap(); /// /// // Navigate to country.iso_code /// let iso_code: Option = result.decode_path(&path!["country", "iso_code"]).unwrap(); /// /// // Navigate to subdivisions[0].names.en /// let subdiv: Option = result.decode_path(&path!["subdivisions", 0, "names", "en"]).unwrap(); /// ``` /// /// ``` /// use maxminddb::{Reader, path}; /// use std::net::IpAddr; /// /// let reader = Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap(); /// let ip: IpAddr = "::1.1.1.0".parse().unwrap(); /// let result = reader.lookup(ip).unwrap(); /// /// // Access the last element of an array /// let last: Option = result.decode_path(&path!["array", -1]).unwrap(); /// assert_eq!(last, Some(3)); /// /// // Access the second-to-last element /// let second_to_last: Option = result.decode_path(&path!["array", -2]).unwrap(); /// assert_eq!(second_to_last, Some(2)); /// ``` #[macro_export] macro_rules! path { ($($elem:expr),* $(,)?) => { [$($crate::PathElement::from($elem)),*] }; } /// Masks an IP address to its network address given a prefix length. fn mask_ip(ip: IpAddr, prefix: u8) -> IpAddr { match ip { IpAddr::V4(v4) => { if prefix >= 32 { IpAddr::V4(v4) } else { let int: u32 = v4.into(); let mask = if prefix == 0 { 0 } else { !0u32 << (32 - prefix) }; IpAddr::V4((int & mask).into()) } } IpAddr::V6(v6) => { if prefix >= 128 { IpAddr::V6(v6) } else { let int: u128 = v6.into(); let mask = if prefix == 0 { 0 } else { !0u128 << (128 - prefix) }; IpAddr::V6((int & mask).into()) } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_mask_ipv4() { let ip: IpAddr = "192.168.1.100".parse().unwrap(); assert_eq!(mask_ip(ip, 24), "192.168.1.0".parse::().unwrap()); assert_eq!(mask_ip(ip, 16), "192.168.0.0".parse::().unwrap()); assert_eq!(mask_ip(ip, 32), "192.168.1.100".parse::().unwrap()); assert_eq!(mask_ip(ip, 0), "0.0.0.0".parse::().unwrap()); } #[test] fn test_mask_ipv6() { let ip: IpAddr = "2001:db8:85a3::8a2e:370:7334".parse().unwrap(); assert_eq!( mask_ip(ip, 64), "2001:db8:85a3::".parse::().unwrap() ); assert_eq!(mask_ip(ip, 32), "2001:db8::".parse::().unwrap()); } #[test] fn test_path_element_debug() { assert_eq!(format!("{:?}", PathElement::Key("test")), "Key(\"test\")"); assert_eq!(format!("{:?}", PathElement::Index(5)), "Index(5)"); assert_eq!( format!("{:?}", PathElement::IndexFromEnd(0)), "IndexFromEnd(0)" ); } #[test] fn test_path_element_from_str() { let elem: PathElement = "key".into(); assert_eq!(elem, PathElement::Key("key")); } #[test] fn test_path_element_from_i32() { // Positive values become Index let elem: PathElement = PathElement::from(0i32); assert_eq!(elem, PathElement::Index(0)); let elem: PathElement = PathElement::from(5i32); assert_eq!(elem, PathElement::Index(5)); // Negative values become IndexFromEnd // -1 → IndexFromEnd(0) (last element) let elem: PathElement = PathElement::from(-1i32); assert_eq!(elem, PathElement::IndexFromEnd(0)); // -2 → IndexFromEnd(1) (second-to-last) let elem: PathElement = PathElement::from(-2i32); assert_eq!(elem, PathElement::IndexFromEnd(1)); // -3 → IndexFromEnd(2) let elem: PathElement = PathElement::from(-3i32); assert_eq!(elem, PathElement::IndexFromEnd(2)); } #[test] fn test_path_element_from_usize() { let elem: PathElement = PathElement::from(0usize); assert_eq!(elem, PathElement::Index(0)); let elem: PathElement = PathElement::from(42usize); assert_eq!(elem, PathElement::Index(42)); } #[test] fn test_path_element_from_isize() { let elem: PathElement = PathElement::from(0isize); assert_eq!(elem, PathElement::Index(0)); let elem: PathElement = PathElement::from(-1isize); assert_eq!(elem, PathElement::IndexFromEnd(0)); let elem: PathElement = PathElement::from(isize::MIN); assert_eq!(elem, PathElement::IndexFromEnd(usize::MAX)); } #[test] fn test_path_macro_keys_only() { let p = path!["country", "iso_code"]; assert_eq!(p.len(), 2); assert_eq!(p[0], PathElement::Key("country")); assert_eq!(p[1], PathElement::Key("iso_code")); } #[test] fn test_path_macro_mixed() { let p = path!["subdivisions", 0, "names", "en"]; assert_eq!(p.len(), 4); assert_eq!(p[0], PathElement::Key("subdivisions")); assert_eq!(p[1], PathElement::Index(0)); assert_eq!(p[2], PathElement::Key("names")); assert_eq!(p[3], PathElement::Key("en")); } #[test] fn test_path_macro_negative_indexes() { let p = path!["array", -1]; assert_eq!(p.len(), 2); assert_eq!(p[0], PathElement::Key("array")); assert_eq!(p[1], PathElement::IndexFromEnd(0)); // last element let p = path!["data", -2, "value"]; assert_eq!(p[1], PathElement::IndexFromEnd(1)); // second-to-last } #[test] fn test_path_macro_trailing_comma() { let p = path!["a", "b",]; assert_eq!(p.len(), 2); } #[test] fn test_path_macro_empty() { let p: [PathElement; 0] = path![]; assert_eq!(p.len(), 0); } #[test] fn test_render_path() { assert_eq!(render_path(&[]), ""); assert_eq!(render_path(&[PathElement::Key("city")]), "/city"); assert_eq!( render_path(&[PathElement::Key("city"), PathElement::Key("names")]), "/city/names" ); assert_eq!( render_path(&[PathElement::Key("arr"), PathElement::Index(0)]), "/arr/0" ); assert_eq!( render_path(&[PathElement::Key("arr"), PathElement::Index(42)]), "/arr/42" ); // IndexFromEnd(0) = last = -1, IndexFromEnd(1) = second-to-last = -2 assert_eq!( render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(0)]), "/arr/-1" ); assert_eq!( render_path(&[PathElement::Key("arr"), PathElement::IndexFromEnd(1)]), "/arr/-2" ); } #[test] fn test_decode_path_error_includes_path() { use crate::Reader; let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); let ip: IpAddr = "89.160.20.128".parse().unwrap(); let result = reader.lookup(ip).unwrap(); // Try to navigate with Index on a map (root is a map, not array) let err = result .decode_path::(&[PathElement::Index(0)]) .unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("path: /0"), "error should include path context: {err_str}" ); assert!( err_str.contains("expected array"), "error should mention expected type: {err_str}" ); // Try to navigate deeper and fail at second element let err = result .decode_path::(&[PathElement::Key("city"), PathElement::Index(0)]) .unwrap_err(); let err_str = err.to_string(); assert!( err_str.contains("path: /city/0"), "error should include full path to failure: {err_str}" ); } } maxminddb-0.28.1/src/within.rs000064400000000000000000000251501046102023000143210ustar 00000000000000//! Network iteration types. use std::cmp::Ordering; use std::net::IpAddr; use crate::decoder; use crate::error::MaxMindDbError; use crate::reader::Reader; use crate::result::{LookupResult, LookupSource, NetworkKind}; /// Options for network iteration. /// /// Controls which networks are yielded when iterating over the database /// with [`Reader::within()`] or [`Reader::networks()`]. /// /// # Example /// /// ``` /// use maxminddb::WithinOptions; /// /// // Default options (skip aliases, skip networks without data, include empty values) /// let opts = WithinOptions::default(); /// /// // Include aliased networks (IPv4 networks via IPv6 aliases) /// let opts = WithinOptions::default().include_aliased_networks(); /// /// // Skip empty values and include networks without data /// let opts = WithinOptions::default() /// .skip_empty_values() /// .include_networks_without_data(); /// ``` #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct WithinOptions { /// Include IPv4 networks multiple times when accessed via IPv6 aliases. include_aliased_networks: bool, /// Include networks that have no associated data record. include_networks_without_data: bool, /// Skip networks whose data is an empty map or empty array. skip_empty_values: bool, } impl WithinOptions { /// Include IPv4 networks multiple times when accessed via IPv6 aliases. /// /// In IPv6 databases, IPv4 networks are stored at `::0/96`. However, the /// same data is accessible through several IPv6 prefixes (e.g., /// `::ffff:0:0/96` for IPv4-mapped IPv6). By default, these aliases are /// skipped to avoid yielding the same network multiple times. /// /// When enabled, the iterator will yield these aliased networks. #[must_use] pub fn include_aliased_networks(mut self) -> Self { self.include_aliased_networks = true; self } /// Include networks that have no associated data record. /// /// Some tree nodes point to "no data" (the node_count sentinel). By default /// these are skipped. When enabled, these networks are yielded and /// [`LookupResult::has_data()`] returns `false` for them. #[must_use] pub fn include_networks_without_data(mut self) -> Self { self.include_networks_without_data = true; self } /// Skip networks whose data is an empty map or empty array. /// /// Some databases store empty maps `{}` or empty arrays `[]` for records /// without meaningful data. This option filters them out. #[must_use] pub fn skip_empty_values(mut self) -> Self { self.skip_empty_values = true; self } } #[derive(Debug)] pub(crate) struct WithinNode { pub(crate) node: usize, pub(crate) ip_int: IpInt, pub(crate) prefix_len: usize, } /// Iterator over IP networks within a CIDR range. /// /// Created by [`Reader::within()`](crate::Reader::within) or /// [`Reader::networks()`](crate::Reader::networks). Yields /// [`LookupResult`] for each network in the database that falls /// within the specified range. /// /// Networks are yielded in depth-first order through the search tree. /// Use [`LookupResult::decode()`](crate::LookupResult::decode) to /// deserialize the data for each result. /// /// # Example /// /// ``` /// use maxminddb::{Reader, WithinOptions, geoip2}; /// /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap(); /// for result in reader.within("89.160.20.0/24".parse().unwrap(), Default::default()).unwrap() { /// let lookup = result.unwrap(); /// if let Some(city) = lookup.decode::().unwrap() { /// println!("{}: {:?}", lookup.network().unwrap(), city.city.names.english); /// } /// } /// ``` #[derive(Debug)] pub struct Within<'de, S: AsRef<[u8]>> { pub(crate) reader: &'de Reader, pub(crate) node_count: usize, pub(crate) stack: Vec, pub(crate) options: WithinOptions, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum IpInt { V4(u32), V6(u128), } impl IpInt { pub(crate) fn new(ip_addr: IpAddr) -> Self { match ip_addr { IpAddr::V4(v4) => IpInt::V4(v4.into()), IpAddr::V6(v6) => IpInt::V6(v6.into()), } } #[inline(always)] pub(crate) fn get_bit(&self, index: usize) -> bool { match self { IpInt::V4(ip) => (ip >> (31 - index)) & 1 == 1, IpInt::V6(ip) => (ip >> (127 - index)) & 1 == 1, } } #[inline(always)] pub(crate) fn set_bit(&mut self, index: usize) { match self { IpInt::V4(ip) => *ip |= 1 << (31 - index), IpInt::V6(ip) => *ip |= 1 << (127 - index), } } pub(crate) fn bit_count(&self) -> usize { match self { IpInt::V4(_) => 32, IpInt::V6(_) => 128, } } pub(crate) fn is_ipv4_in_ipv6(&self) -> bool { match self { IpInt::V4(_) => false, IpInt::V6(ip) => *ip <= 0xFFFFFFFF, } } } impl<'de, S: AsRef<[u8]>> Iterator for Within<'de, S> { type Item = Result, MaxMindDbError>; fn next(&mut self) -> Option { while let Some(current) = self.stack.pop() { let bit_count = current.ip_int.bit_count(); // Skip networks that are aliases for the IPv4 network (unless option is set) if !self.options.include_aliased_networks && self.reader.ipv4_start != 0 && current.node == self.reader.ipv4_start && bit_count == 128 && !current.ip_int.is_ipv4_in_ipv6() { continue; } match current.node.cmp(&self.node_count) { Ordering::Greater => { // This is a data node, emit it and we're done (until the following next call) let ip_addr = ip_int_to_addr(¤t.ip_int); // Resolve the pointer to a data offset let data_offset = match self.reader.resolve_data_pointer(current.node) { Ok(offset) => offset, Err(e) => return Some(Err(e)), }; // Check if we should skip empty values if self.options.skip_empty_values { match self.is_empty_value_at(data_offset) { Ok(true) => continue, // Skip empty value Ok(false) => {} // Not empty, proceed Err(e) => return Some(Err(e)), } } let network_kind = match current.ip_int { IpInt::V4(_) => NetworkKind::V4, IpInt::V6(_) if current.ip_int.is_ipv4_in_ipv6() && self.reader.has_ipv4_subtree() && current.prefix_len >= self.reader.ipv4_start_bit_depth => { NetworkKind::V4InV6Subtree } IpInt::V6(_) => NetworkKind::V6, }; return Some(Ok(LookupResult::new_found( self.reader, data_offset, current.prefix_len as u8, ip_addr, LookupSource::Iter, network_kind, ))); } Ordering::Equal => { // Dead end (no data) - include if option is set if self.options.include_networks_without_data { let ip_addr = ip_int_to_addr(¤t.ip_int); let network_kind = match current.ip_int { IpInt::V4(_) => NetworkKind::V4, IpInt::V6(_) if current.ip_int.is_ipv4_in_ipv6() && self.reader.has_ipv4_subtree() && current.prefix_len >= self.reader.ipv4_start_bit_depth => { NetworkKind::V4InV6Subtree } IpInt::V6(_) => NetworkKind::V6, }; return Some(Ok(LookupResult::new_not_found( self.reader, current.prefix_len as u8, ip_addr, LookupSource::Iter, network_kind, ))); } // Otherwise skip (current behavior) } Ordering::Less => { // In order traversal of our children // right/1-bit let mut right_ip_int = current.ip_int; if current.prefix_len < bit_count { right_ip_int.set_bit(current.prefix_len); } self.push_child(current.node, 1, right_ip_int, current.prefix_len + 1); // left/0-bit self.push_child(current.node, 0, current.ip_int, current.prefix_len + 1); } } } None } } impl<'de, S: AsRef<[u8]>> Within<'de, S> { fn push_child( &mut self, parent_node: usize, direction: usize, ip_int: IpInt, prefix_len: usize, ) { let node = self.reader.read_node(parent_node, direction); self.stack.push(WithinNode { node, ip_int, prefix_len, }); } /// Check if the value at the given data offset is an empty map or array. fn is_empty_value_at(&self, data_offset: usize) -> Result { let buf = &self.reader.buf.as_ref()[self.reader.pointer_base..]; let mut dec = decoder::Decoder::new(buf, data_offset); let (size, type_num) = dec.peek_type()?; match type_num { decoder::TYPE_MAP | decoder::TYPE_ARRAY => Ok(size == 0), _ => Ok(false), // Non-container types are never "empty" } } } /// Convert IpInt to IpAddr pub(crate) fn ip_int_to_addr(ip_int: &IpInt) -> IpAddr { match ip_int { IpInt::V4(ip) => IpAddr::V4((*ip).into()), IpInt::V6(ip) => { // Check if this is an IPv4-mapped IPv6 address if *ip <= 0xFFFFFFFF { IpAddr::V4((*ip as u32).into()) } else { IpAddr::V6((*ip).into()) } } } }