mmdbwriter

repository·main·Indexed 41 days ago

https://github.com/maxmind/mmdbwriter

A high-performance Go package for creating and writing MaxMind DB (MMDB) files. It supports IPv4 and IPv6 search trees with customizable record sizes, insertion logic, and tools for auditing tree integrity and unmarshaling cursors via storeDecoder.

Tokens
3.6K
Snippets
12
Records
15
Agent score
50%

What's inside mmdbwriter

  1. Manage memory and references in storeDecoder

    main

    The storeDecoder uses an internal cache and a result buffer to manage valueRef ownership during the decoding process. To prevent memory leaks in the underlying valueStore, follow these rules:

    • UnmarshalMaxMindDBCursor(cursor): Decodes the value at the current cursor. If a result was already stored from a previous call that wasn't taken, it is automatically released.
    • takeResult(): Transfers ownership of the most recently decoded top-level valueRef to the caller. Once called, the decoder's internal result is set to nilValueRef.
    • close(): This is critical. It releases the current result and all references stored in the internal cache (used for deduplication/interning during the decode process). Always call this when the decoder is no longer needed.
  2. Enable reference-count auditing for a Tree

    main

    You can trigger an ownership and reference-count audit of the tree structure to ensure internal invariants are maintained. Auditing is an expensive operation and is intended for testing or verifying tree integrity after complex operations.

    Auditing can be enabled in two ways:

    1. Via Options: Configure the Tree with Options.RefcountAudit during initialization.
    2. Via Environment Variable: Set the MMDBWRITER_REFCOUNT_AUDIT environment variable.

    If auditing is enabled, the Tree will automatically run maybeAuditValueStore() after insert operations reach the store or after a successful load, provided all temporary references have been released. A failure during this process returns a *RefcountAuditError.

  3. Configure the mmdbwriter Tree with Options

    main

    The Options struct allows you to configure the behavior and structure of a MaxMind DB search tree during creation. Key configuration areas include:

    • IP Versioning: Use IPVersion to specify an IPv4 (4) or IPv6 (6) database. IPv6 databases support both IPv4 and IPv6 lookups by default.
    • Tree Structure: Set RecordSize to 24, 28, or 32 bits. A smaller size reduces database size but limits total capacity. The default is 28.
    • Metadata: Define the DatabaseType, a Description (map of language codes to descriptions), and a list of supported Languages (locale codes).
    • Network Behavior:
      • IncludeReservedNetworks: If false, attempts to insert into reserved networks will error, or the reserved portion will be excluded.
      • DisableIPv4Aliasing: If true, disables the mapping of certain IPv6 networks to IPv4 (e.g., ::ffff:0:0/96).
    • Custom Insertion Logic: Provide an Inserter (a PureFunc) to control how existing values are handled during insertion. Leaving it nil defaults to inserter.Replace, which replaces conflicting old values entirely.
    import "github.com/maxmind/mmdbwriter/v2"
    
    opts := mmdbwriter.Options{
    	DatabaseType: "my-custom-type",
    	IPVersion:    6,
    	RecordSize:   28,
    	Languages:    []string{"en", "fr"},
    	Description:  map[string]string{"en": "English Description", "fr": "Description en français"},
    }
  4. Insert data into a Tree

    main

    There are several ways to insert data into a Tree. Note that Tree is not safe for concurrent use; callers must synchronize access.

    Direct Insertion

    Insert(prefix netip.Prefix, value mmdbtype.DataType) replaces any existing value at the specified prefix with the new value.

    Insertion with Custom Logic

    If you need to merge or modify existing data rather than replacing it, use one of the following:

    • InsertFunc(prefix netip.Prefix, value mmdbtype.DataType, insertFunc inserter.Func): Provides the existing value, the new value, and insertion metadata to the callback.
    • InsertPureFunc(prefix netip.Prefix, value mmdbtype.DataType, pureFunc inserter.PureFunc): A version of InsertFunc that does not provide metadata. The function must be pure (its result must depend only on its arguments and not on external state or call order).

    Important: You must never modify a value after insertion. Values may be shared across records via object identity; mutating an object in place will corrupt other records sharing that object.

    // Simple replacement
    err := tree.Insert(netip.MustParsePrefix("192.0.2.0/24"), myData)
    
    // Custom logic using InsertFunc
    err := tree.InsertFunc(netip.MustParsePrefix("192.0.2.0/24"), newData, func(existing, new mmdbtype.DataType, meta inserter.Metadata) mmdbtype.DataType {
    	// logic to merge existing and new
    	return mergedData
    })
  5. Insert a range of IPs using InsertRange

    main

    The InsertRange family of functions allows you to insert data into all subnets within a continuous range of IP addresses defined by a start and end address.

    • InsertRange(start, end netip.Addr, value mmdbtype.DataType): Replaces values in the range.
    • InsertRangeFunc(...): Uses a callback for custom logic.
    • InsertRangePureFunc(...): Uses a pure callback for custom logic.

    Subnets are inserted sequentially. For InsertFunc and InsertRangeFunc, the InsertedNetwork field in the metadata will reflect the specific individual subnet being processed, not the entire range.

    start := netip.MustParseAddr("192.0.2.1")
    end := netip.MustParseAddr("192.0.2.255")
    err := tree.InsertRange(start, end, myData)
  6. Write a Tree to a file using WriteTo()

    main

    The WriteTo(w io.Writer) method serializes the tree into the MaxMind DB binary format and writes it to the provided io.Writer.

    Process:

    1. It automatically calls finalize() if the tree has not been finalized yet.
    2. It writes the node section.
    3. It writes the data section.
    4. It writes the metadata section.

    Returns the total number of bytes written and any error encountered.

    f, _ := os.Create("output.mmdb")
    defer f.Close()
    
    bytesWritten, err := tree.WriteTo(f)
    if err != nil {
    	// handle error
    }
  7. Retrieve data from a Tree using Get()

    main

    Use Get(ip netip.Addr) to look up the value associated with an IP address.

    • Return Values: Returns the netip.Prefix that matched the IP and the mmdbtype.DataType associated with it. If no match is found, it returns an empty prefix and a nil value.
    • Concurrency Warning: Get is not safe to call concurrently with any other Tree method (including other Get calls) because lookups materialize shared views lazily. The caller must provide synchronization.
    • Data Immutability: The returned values are shared, read-only views. If you need to modify the returned value, you must call Copy on it first.
    prefix, value := tree.Get(netip.MustParseAddr("192.0.2.1"))
    if value != nil {
    	// use value
    }
  8. Unmarshal a MaxMind DB cursor using storeDecoder

    main

    The storeDecoder type implements the mmdbdata.CursorUnmarshaler interface. It is used to decode a MaxMind DB mmdbdata.Cursor and intern its values directly into a valueStore. This process avoids constructing intermediate map or slice graphs, making it highly efficient for building or transforming MMDB files.

    Key Workflow

    1. Unmarshal: Call UnmarshalMaxMindDBCursor(cursor) to decode the current value at the cursor's position. This returns the next cursor position and an error.
    2. Retrieve Result: After unmarshaling, use takeResult() to transfer ownership of the most recently decoded top-level value reference (valueRef).
    3. Cleanup: You must call close() on the storeDecoder to release all interned references held in its internal cache and the current result. Failure to call close() will result in memory leaks within the valueStore.

    Important Lifecycle Note

    storeDecoder manages references internally. When UnmarshalMaxMindDBCursor is called repeatedly, it automatically releases the previous result before setting the new one. However, the final result obtained via takeResult() and all cached references must be managed by calling close() to ensure the valueStore remains consistent.

    // Example conceptual usage of the storeDecoder pattern
    // Note: This assumes access to the internal newStoreDecoder and valueStore
    
    decoder := newStoreDecoder(myValueStore)
    defer decoder.close()
    
    // Unmarshal the first element
    nextCursor, err := decoder.UnmarshalMaxMindDBCursor(initialCursor)
    if err != nil {
        return err
    }
    
    // Transfer ownership of the decoded value
    rootRef := decoder.takeResult()
    // Use rootRef as needed...
  9. Load an existing database using Load()

    main

    The Load(path string, opts Options) function allows you to import an existing MaxMind DB file into a Tree.

    Behavioral Notes:

    • It interns records directly into the value store to avoid intermediate memory overhead.
    • If opts are partially provided, they are merged with the metadata found in the existing database file (e.g., DatabaseType, Description, Languages, RecordSize, IPVersion).
    • If a non-nil Options.Inserter is provided, it receives a materialized view of each decoded record. The inserter must treat these values as immutable and copy them before modification.
    tree, err := mmdbwriter.Load("/path/to/database.mmdb", opts)
    if err != nil {
    	// handle error
    }
  10. Handle RefcountAuditError during database auditing

    main

    When performing a reference-count audit on a Tree, the library may return a *RefcountAuditError. This error indicates that the tree's internal invariants have been violated, signaling either a bug within the mmdbwriter library itself or memory corruption.

    Because this error represents a fundamental violation of the database structure rather than invalid user input, retrying the failed operation will not resolve the issue. You should treat this as a critical failure of the tree's integrity.

    var err error
    // ... perform tree operations ...
    if err != nil {
    	if _, ok := err.(*RefcountAuditError); ok {
    		// Handle internal library bug or memory corruption
    	}
    }
  11. Create a new Tree using New()

    main

    Use New(opts Options) to initialize a new MaxMind DB search tree. The function validates the provided options, such as ensuring RecordSize is one of the supported values (24, 28, 32) and that BuildEpoch is non-negative. If IPVersion is 6 and DisableIPv4Aliasing is not set, it automatically handles IPv4 aliases.

    tree, err := mmdbwriter.New(opts)
    if err != nil {
    	// handle error
    }
  12. Implement the mmdbtype.Writer interface with dataWriter

    main

    The dataWriter type implements the mmdbtype.Writer interface, allowing it to be used by mmdbtype.Pointer and mmdbtype.String objects via their WriteTo methods.

    When using these methods, the dataWriter writes the full value and does not record an offset. This is a safety measure to prevent an offset for a reference that the caller later releases from being recycled and pointing at unrelated data.

    To satisfy the interface, use:

    • WriteOrWritePointer(value mmdbtype.DataType) (int64, error)
    • WriteOrWritePointerString(value mmdbtype.String) (int64, error)
    // Example of how mmdbtype.Pointer uses the writer interface
    // (Conceptual usage based on the implementation)
    size, err := dw.WriteOrWritePointer(someDataType)