Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
120bd2c
Inline byte manipulation in read_node
oschwald Nov 26, 2025
de1dece
Add inline hints to lookup path functions
oschwald Nov 26, 2025
5f9833f
Add LookupResult API with lazy decoding support
oschwald Nov 26, 2025
e5a5655
Match Go reader network() behavior for IPv4/IPv6
oschwald Nov 26, 2025
8adb549
Add comprehensive test coverage matching Go reader
oschwald Nov 26, 2025
2bbe79d
Add iteration options matching Go's Networks/NetworksWithin
oschwald Nov 27, 2025
9048887
Add verify() method for database integrity validation
oschwald Nov 27, 2025
121a1cf
Add serde size hints for efficient collection pre-allocation
oschwald Nov 27, 2025
7c52748
Refactor: Use type constants throughout decoder
oschwald Nov 27, 2025
0a1f563
Return false for is_human_readable() since MMDB is binary
oschwald Nov 28, 2025
3ac1328
Implement deserialize_ignored_any for efficient value skipping
oschwald Nov 28, 2025
c21f5b9
Implement deserialize_enum for string-to-enum deserialization
oschwald Nov 28, 2025
f5a7032
Add test for serde(flatten) attribute support
oschwald Nov 28, 2025
c392a8c
Add changelog entry for serde deserializer improvements
oschwald Nov 28, 2025
1385066
Add recursion depth limit matching libmaxminddb
oschwald Nov 28, 2025
20723e1
Include offset in decoder error messages
oschwald Nov 28, 2025
0b1c9ef
Restructure error types with structured fields
oschwald Nov 28, 2025
4a0e759
Modernize Rust idioms in codebase
oschwald Nov 28, 2025
11fc430
Reorganize code into modern module structure
oschwald Nov 28, 2025
de151b0
Make WithinOptions fields private
oschwald Nov 28, 2025
0466e5f
Use Option for LookupResult data offset
oschwald Nov 28, 2025
30455fc
Add InvalidInput error variant for user input errors
oschwald Nov 28, 2025
65df89e
Replace PathElement::Index(isize) with explicit variants
oschwald Nov 28, 2025
2852dda
Rename found() to has_data() for clarity
oschwald Nov 28, 2025
84e8d00
Add PartialEq and Eq trait implementations
oschwald Nov 28, 2025
2de1a3e
Change decode() to return Result<Option<T>>
oschwald Nov 28, 2025
da414ab
Add path! macro for ergonomic path construction
oschwald Nov 28, 2025
89eefd7
Add path context to decode_path errors
oschwald Nov 28, 2025
8707834
Implement Debug for Reader to avoid printing buffer
oschwald Nov 28, 2025
14e7872
Refactor GeoIP2 structs for ergonomic access
oschwald Nov 28, 2025
a0803b0
Add Metadata::build_time() method
oschwald Nov 28, 2025
5d3f620
Remove deprecated is_anonymous_proxy and is_satellite_provider fields
oschwald Nov 28, 2025
736baa2
Improve GeoIP2 struct documentation
oschwald Nov 28, 2025
b306a46
Improve documentation for README, Cargo.toml, and API
oschwald Nov 28, 2025
54a11bd
Make Decoder struct and methods pub(crate)
oschwald Nov 28, 2025
75dd92a
Prepare 0.27.0 release
oschwald Nov 28, 2025
98f0e4f
Mark Reader::open_mmap as unsafe to fix soundness issue
oschwald Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,73 @@
# Change Log

## 0.27.0 - 2025-11-28

This release includes significant API changes. See [UPGRADING.md](UPGRADING.md)
for migration guidance.

### Breaking Changes

#### Lookup API

- `lookup()` now returns `LookupResult` instead of `Option<T>`. The new API
enables lazy decoding - data is only deserialized when explicitly requested.
- `lookup_prefix()` has been removed. Use `lookup(ip)?.network()` instead.

#### Iteration API

- `within()` now requires a second `WithinOptions` parameter. Use
`Default::default()` for the previous behavior.
- `Within` iterator now yields `LookupResult` instead of `WithinItem<T>`.

#### GeoIP2 Structs

- The `names` fields now use a `Names` struct instead of `BTreeMap<&str, &str>`.
Access names directly via language fields (e.g., `names.english`).
- Nested struct fields (`city`, `country`, `location`, etc.) are now
non-optional with `Default`, simplifying access patterns.
- Removed `is_anonymous_proxy` and `is_satellite_provider` from `Traits`.
These fields are no longer present in MaxMind databases.

#### Error Types

- `InvalidDatabase` and `Decoding` variants now use structured fields instead
of a single string. Pattern matching must be updated.
- New `InvalidInput` variant for user input errors (e.g., IPv6 lookup in
IPv4-only database).

#### Memory Mapping

- `Reader::open_mmap` is now `unsafe`. The caller must ensure the database
file is not modified or truncated while the `Reader` exists. This fixes a
soundness issue. Reported by paolobarbolini. GitHub #86.

### Added

- `LookupResult` type with lazy decoding support:
- `has_data()` - Check if data exists for this IP
- `network()` - Get the network containing the IP
- `offset()` - Get data offset for caching/deduplication
- `decode()` - Deserialize full record
- `decode_path()` - Selectively decode specific fields by path
- `PathElement` enum and `path!` macro for navigating nested structures.
- `WithinOptions` to control network iteration behavior:
- `include_aliased_networks()` - Include IPv4 via IPv6 aliases
- `include_networks_without_data()` - Include networks without data records
- `skip_empty_values()` - Skip empty maps/arrays
- `networks()` method for iterating over all networks in the database.
- `verify()` method for comprehensive database validation.
- `Metadata::build_time()` to convert `build_epoch` to `SystemTime`.
- `PartialEq` and `Eq` implementations for `Metadata` and `WithinOptions`.

### Changed

- Error messages now include byte offsets when available.
- `decode_path()` errors include path context showing where navigation failed.
- Added recursion depth limit (512) matching libmaxminddb and Go reader.
- Serde deserializer improvements: size hints, `is_human_readable()` returns
false, `deserialize_ignored_any`, and `deserialize_enum` support.
- `MaxMindDbError` is now `#[non_exhaustive]`.

## 0.26.0 - 2025-03-28

- **BREAKING CHANGE:** The `lookup` and `lookup_prefix` methods now return
Expand Down
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
[package]
name = "maxminddb"
version = "0.26.0"
version = "0.27.0"
authors = [ "Gregory J. Oschwald <oschwald@gmail.com>" ]
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 = "http://oschwald.github.io/maxminddb-rust/maxminddb/struct.Reader.html"
documentation = "https://docs.rs/maxminddb"
repository = "https://github.com/oschwald/maxminddb-rust"
license = "ISC"
include = ["/Cargo.toml", "/benches/*.rs", "/src/**/*.rs", "/README.md", "/LICENSE"]
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"
path = "src/maxminddb/lib.rs"
name = "maxminddb"

[dependencies]
ipnetwork = "0.21.1"
Expand Down
48 changes: 37 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,52 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
maxminddb = "0.26"
maxminddb = "0.27"
```

and this to your crate root:
## Example

```rust
extern crate maxminddb;
use maxminddb::{geoip2, Reader};
use std::net::IpAddr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let reader = Reader::open_readfile("/path/to/GeoLite2-City.mmdb")?;

let ip: IpAddr = "89.160.20.128".parse()?;
let result = reader.lookup(ip)?;

if let Some(city) = result.decode::<geoip2::City>()? {
println!("Country: {}", city.country.iso_code.unwrap_or("N/A"));
println!("City: {}", city.city.names.english.unwrap_or("N/A"));
}

Ok(())
}
```

## API Documentation
See the [examples](examples/) directory for more usage patterns.

The API docs are on [Docs.rs](https://docs.rs/maxminddb/latest/maxminddb/struct.Reader.html).
## Features

## Example
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`:

See [`examples/lookup.rs`](https://github.com/oschwald/maxminddb-rust/blob/main/examples/lookup.rs) for a basic example.
```toml
[dependencies]
maxminddb = { version = "0.27", features = ["mmap"] }
```

Note: `simdutf8` and `unsafe-str-decode` are mutually exclusive.

## Documentation

[API documentation on docs.rs](https://docs.rs/maxminddb)

## Benchmarks

Expand All @@ -64,10 +94,6 @@ 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`.

Result of doing 100 random IP lookups:

![](/assets/pdf_small.svg)

## Contributing

Contributions welcome! Please fork the repository and open a pull request
Expand Down
203 changes: 203 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# 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<geoip2::City> = 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::<geoip2::City>()? {
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::<geoip2::City>()?;
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::<geoip2::City>(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::<geoip2::City>()? {
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<T>`.

### 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::<T>()?`
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
Loading
Loading