Skip to content

Conversation

@oschwald
Copy link
Owner

  • Inline byte manipulation in read_node
  • Add inline hints to lookup path functions
  • Add LookupResult API with lazy decoding support
  • Match Go reader network() behavior for IPv4/IPv6
  • Add comprehensive test coverage matching Go reader
  • Add iteration options matching Go's Networks/NetworksWithin
  • Add verify() method for database integrity validation
  • Add serde size hints for efficient collection pre-allocation
  • Refactor: Use type constants throughout decoder
  • Return false for is_human_readable() since MMDB is binary
  • Implement deserialize_ignored_any for efficient value skipping
  • Implement deserialize_enum for string-to-enum deserialization
  • Add test for serde(flatten) attribute support
  • Add changelog entry for serde deserializer improvements
  • Add recursion depth limit matching libmaxminddb
  • Include offset in decoder error messages
  • Restructure error types with structured fields
  • Modernize Rust idioms in codebase
  • Reorganize code into modern module structure

oschwald and others added 30 commits November 26, 2025 11:25
Directly perform byte-to-usize conversion in read_node instead of
calling the separate to_usize function. This makes the byte layout
for each record size explicit and removes the now-unused to_usize
function from the library.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add #[inline] hints to start_node and resolve_data_pointer functions
which are called during the lookup hot path. While the compiler likely
inlines these already, explicit hints make the optimization intent
clear.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
BREAKING CHANGE: The lookup() method now returns LookupResult instead
of Option<T>. Data is only deserialized when decode() is called.

Migration:
- Old: reader.lookup::<City>(ip)? returns Option<City>
- New: reader.lookup(ip)?.decode::<City>()? returns City
- Check if found: reader.lookup(ip)?.found()

Other breaking changes:
- lookup_prefix() removed - use result.network() instead
- Within iterator now yields LookupResult instead of WithinItem<T>

New features:
- LookupResult with found(), network(), offset(), decode(),
  decode_path(), and decoder() methods
- PathElement enum for navigating nested structures with
  Python-style negative indexing
- Low-level Decoder API (Kind, MapReader, ArrayReader) for
  FFI bindings and custom deserialization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- IPv4 lookups return IPv4 networks with correct prefix length
- IPv6 lookups preserve IPv6 form (including IPv4-mapped addresses)
- Handle no-ipv4-search-tree databases by tracking ipv4_start_bit_depth
- Add comprehensive test_lookup_network covering 11 edge cases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add IPv6 in IPv4-only database validation (returns error like Go)
- Add missing network test case for 0:0:0:0:ffff:ffff:ffff:ffff
- Add comprehensive decode_path tests (array indexing, negative index)
- Test nested path navigation (map.mapX.arrayX[1])
- Test non-existent key handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add WithinOptions struct with builder methods to control network
iteration behavior:
- include_aliased_networks(): Include IPv4 via IPv6 aliases
- include_networks_without_data(): Include networks with no data
- skip_empty_values(): Skip empty maps/arrays

Add networks() convenience method for iterating all networks.
Update within() to take options as second parameter.

Breaking change: within(cidr) -> within(cidr, options)

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive database verification matching Go's Verify() method:
- Metadata validation (format version, required fields, value constraints)
- Search tree traversal with cycle detection
- Data section separator verification (16 zero bytes)
- Data record validation at each pointed-to offset
- Type-specific size validation for floats, integers, and booleans

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implement size_hint() for SeqAccess and MapAccess traits in the
decoder. This allows Vec, HashMap, and other collections to
pre-allocate the correct capacity when deserializing, avoiding
reallocations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace magic numbers with named constants for all MaxMind DB type
codes. This improves readability and maintainability.

Constants added:
- TYPE_EXTENDED (0), TYPE_POINTER (1), TYPE_STRING (2)
- TYPE_DOUBLE (3), TYPE_BYTES (4), TYPE_UINT16 (5)
- TYPE_UINT32 (6), TYPE_MAP (7), TYPE_INT32 (8)
- TYPE_UINT64 (9), TYPE_UINT128 (10), TYPE_ARRAY (11)
- TYPE_BOOL (14), TYPE_FLOAT (15)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The serde Deserializer trait's is_human_readable() method defaults to
true, but MMDB is a binary format. This affects how some types like
Duration and IpAddr choose their serialization format.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When deserializing into serde::de::IgnoredAny, we can now skip values
efficiently without fully decoding them. This reuses the existing
skip_value() method which advances the decoder offset based on the
control byte without parsing strings, validating UTF-8, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Users can now deserialize string values from MMDB into Rust enums using
serde's rename attribute:

    #[derive(Deserialize)]
    enum ConnType {
        #[serde(rename = "Cable/DSL")]
        CableDsl,
    }

This adds EnumAccessor implementing EnumAccess and VariantAccess traits,
supporting unit, newtype, tuple, and struct variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Real-world GeoIP2/GeoLite2 databases don't contain u128 values, so
#[serde(flatten)] with HashMap<String, IgnoredAny> works without issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add a maximum data structure depth of 512 when decoding, matching the
limit used in libmaxminddb and the Go reader. This prevents stack
overflow when decoding malformed databases with excessively nested
maps, arrays, or pointer chains.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Decoding errors now include the current offset in the data section,
making it easier to debug malformed or corrupt databases. For example:
"Invalid database: unknown data type: 13 at offset 1234"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Change InvalidDatabase and Decoding variants to use structured fields
  (message, offset, path) instead of a single String
- Add helper constructors: invalid_database(), invalid_database_at(),
  decoding(), decoding_at(), decoding_at_path()
- Add offset() method to Decoder for exposing current position
- Add #[non_exhaustive] attribute to MaxMindDbError for future compat
- Update all error creation sites to use new constructors with offsets
- Follow Rust idiom: lowercase error messages without trailing punctuation

This is a breaking change - pattern matching code must be updated from
InvalidDatabase(msg) to InvalidDatabase { message, .. }.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Use then_some() for Option transformation in LookupResult::offset()
- Replace FromStr::from_str() with .parse() throughout tests
- Simplify test error handling: if let Err + unwrap -> expect()
- Remove unused FromStr import
- Use _ instead of unused variable in match guards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Move from non-standard src/maxminddb/lib.rs to standard src/lib.rs
layout. Split the monolithic lib.rs into focused modules:

- error.rs: MaxMindDbError and helper functions
- metadata.rs: Metadata struct
- reader.rs: Reader struct and all implementations
- within.rs: Within iterator, WithinOptions, and IpInt helper
- result.rs: LookupResult and PathElement (already separate)
- decoder.rs: Binary format decoder (already separate)
- geoip2.rs: GeoIP2 data structures (already separate)

The lib.rs now serves as the crate root with re-exports, following
modern Rust conventions. No public API changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fields should only be modified through builder methods, not directly.
This ensures consistent option construction via the builder pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace usize::MAX sentinel with Option<usize> for clearer semantics.
This is an internal change that doesn't affect the public API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Looking up an IPv6 address in an IPv4-only database is a user input
error, not a database corruption issue. Adding a separate error variant
makes this distinction clear and allows callers to handle these cases
appropriately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Use separate Index(usize) and IndexFromEnd(usize) variants instead of
a signed integer. This is more idiomatic Rust and makes the intent
clearer at the call site.

- Index(0) is the first element
- IndexFromEnd(0) is the last element

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The name has_data() more clearly describes what the method checks:
whether the database contains data for this IP address.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add PartialEq and Eq to Metadata and WithinOptions for easier
comparison and use in collections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Instead of returning Err when the IP isn't found, decode() now returns
Ok(None). This makes the "not found" case consistent with decode_path()
and eliminates the need to check has_data() before decoding.

- Ok(Some(T)) - found and decoded successfully
- Ok(None) - IP not found in database
- Err(e) - decoding error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The macro converts string literals to Key elements, non-negative
integers to Index elements, and negative integers to IndexFromEnd
elements (e.g., -1 becomes the last element).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When decode_path fails during navigation, the error now includes the
path traversed up to the point of failure. This helps users understand
where in a nested structure the error occurred.

For example, decode_path(&["city", "names", 0]) failing at the index
will show "path: /city/names/0" in the error message.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The derived Debug would print the entire database buffer (potentially
gigabytes of binary data). The manual implementation shows only the
metadata, which contains the useful information about the database.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace BTreeMap-based names with typed Names struct using Option<&str>
fields for each language. Make nested struct fields non-optional with
Default, while keeping leaf values as Option<T> to preserve semantics.

This eliminates nested Option unwrapping:
- Old: city.city.as_ref().and_then(|c| c.names.english)
- New: city.city.names.english

Add is_empty() methods using *self == Self::default() pattern so they
don't need updating when fields are added.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
oschwald and others added 7 commits November 28, 2025 09:43
Adds a convenience method to convert the build_epoch Unix timestamp
to a SystemTime, matching the Go v2 library's BuildTime() method.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
These fields have been removed from MaxMind databases. Users should use
the dedicated Anonymous IP database for anonymity detection.

This matches the Go v2 library which also removed these fields.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive documentation for all GeoIP2 structs and fields:
- Document all top-level record types (Country, City, Enterprise, etc.)
- Add field descriptions with semantic meaning
- Include links to Wikipedia for ISO codes and standards
- Mark deprecated fields (metro_code)
- Document confidence scores for Enterprise fields
- Describe possible values for enum-like fields (connection_type, user_type)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Update README with inline example, features section, modern syntax
- Fix Cargo.toml documentation URL to point to docs.rs
- Add feature flag comments to Cargo.toml
- Add module and struct documentation to decoder.rs
- Expand Within iterator documentation with example
- Add file-level docs and comments to example files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Decoder is not exported from the crate, so pub visibility was
misleading. Changed to pub(crate) for clarity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Reorganize changelog with categorized sections
- Add UPGRADING.md with migration guide and code examples
- Bump version to 0.27.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The function wraps memmap2's MmapOptions::map(), which is unsafe
because undefined behavior can occur if the underlying file is
modified while mapped. By marking open_mmap as unsafe, callers must
acknowledge this safety requirement.

Fixes #86

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@oschwald oschwald merged commit bd45717 into main Nov 28, 2025
38 checks passed
@oschwald oschwald deleted the greg/result branch November 28, 2025 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants