Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ CHANGELOG
------------------

* Java 17 or greater is now required.
* Added support for MaxMind DB files larger than 2GB. The library now uses
an internal Buffer abstraction that can handle databases exceeding the
2GB ByteBuffer limit. Files under 2GB continue to use a single ByteBuffer
for optimal performance. Requested by nonetallt. GitHub #154. Fixed by
Silvano Cerza. GitHub #289.
* `Metadata.getBuildDate()` has been replaced with `buildTime()`, which returns
`java.time.Instant` instead of `java.util.Date`. The instant represents the
database build time in UTC.
* `DatabaseRecord`, `Metadata`, `Network`, and internal `DecodedValue` classes
have been converted to records. The following API changes were made:
* `DatabaseRecord.getData()` and `DatabaseRecord.getNetwork()` have been
replaced with record accessor methods `data()` and `network()`.
* Simple getter methods on `Metadata` (e.g., `getBinaryFormatMajorVersion()`,
`getDatabaseType()`, etc.) have been replaced with their corresponding record
accessor methods (e.g., `binaryFormatMajorVersion()`, `databaseType()`, etc.).
* `Network.getNetworkAddress()` and `Network.getPrefixLength()` have been
replaced with record accessor methods `networkAddress()` and `prefixLength()`.

3.2.0 (2025-05-28)
------------------
Expand Down
17 changes: 4 additions & 13 deletions src/main/java/com/maxmind/db/Buffer.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.maxmind.db;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;

Expand All @@ -12,8 +10,11 @@
*
* <p>This interface is designed to provide a long-based API while
* remaining compatible with the limitations of underlying storage.
*
* <p>All underlying {@link java.nio.ByteBuffer}s are read-only to prevent
* accidental modification of shared data.
*/
interface Buffer {
sealed interface Buffer permits SingleBuffer, MultiBuffer {
/**
* Returns the total capacity of this buffer in bytes.
*
Expand Down Expand Up @@ -96,16 +97,6 @@ interface Buffer {
*/
Buffer duplicate();

/**
* Reads data from the given channel into this buffer starting at the
* current position.
*
* @param channel the file channel
* @return the number of bytes read
* @throws IOException if an I/O error occurs
*/
long readFrom(FileChannel channel) throws IOException;

/**
* Decodes the buffer's content into a string using the given decoder.
*
Expand Down
74 changes: 46 additions & 28 deletions src/main/java/com/maxmind/db/BufferHolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

final class BufferHolder {
// DO NOT PASS OUTSIDE THIS CLASS. Doing so will remove thread safety.
Expand All @@ -23,18 +22,48 @@ final class BufferHolder {
FileChannel channel = file.getChannel()) {
long size = channel.size();
if (mode == FileMode.MEMORY) {
Buffer buf;
if (size <= chunkSize) {
buf = new SingleBuffer(size);
// Allocate, read, and make read-only
ByteBuffer buffer = ByteBuffer.allocate((int) size);
if (channel.read(buffer) != size) {
throw new IOException("Unable to read "
+ database.getName()
+ " into memory. Unexpected end of stream.");
}
buffer.flip();
this.buffer = new SingleBuffer(buffer);
} else {
buf = new MultiBuffer(size);
}
if (buf.readFrom(channel) != buf.capacity()) {
throw new IOException("Unable to read "
+ database.getName()
+ " into memory. Unexpected end of stream.");
// Allocate chunks, read, and make read-only
var fullChunks = (int) (size / chunkSize);
var remainder = (int) (size % chunkSize);
var totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
var buffers = new ByteBuffer[totalChunks];

for (int i = 0; i < fullChunks; i++) {
buffers[i] = ByteBuffer.allocate(chunkSize);
}
if (remainder > 0) {
buffers[totalChunks - 1] = ByteBuffer.allocate(remainder);
}

var totalRead = 0L;
for (var buffer : buffers) {
var read = channel.read(buffer);
if (read == -1) {
break;
}
totalRead += read;
buffer.flip();
}

if (totalRead != size) {
throw new IOException("Unable to read "
+ database.getName()
+ " into memory. Unexpected end of stream.");
}

this.buffer = new MultiBuffer(buffers, chunkSize);
}
this.buffer = buf;
} else {
if (size <= chunkSize) {
this.buffer = SingleBuffer.mapFromChannel(channel);
Expand All @@ -45,38 +74,27 @@ final class BufferHolder {
}
}

/**
* Construct a ThreadBuffer from the provided URL.
*
* @param stream the source of my bytes.
* @throws IOException if unable to read from your source.
* @throws NullPointerException if you provide a NULL InputStream
*/
BufferHolder(InputStream stream) throws IOException {
this(stream, MultiBuffer.DEFAULT_CHUNK_SIZE);
}

BufferHolder(InputStream stream, int chunkSize) throws IOException {
if (null == stream) {
throw new NullPointerException("Unable to use a NULL InputStream");
}
List<ByteBuffer> chunks = new ArrayList<>();
long total = 0;
byte[] tmp = new byte[chunkSize];
var chunks = new ArrayList<ByteBuffer>();
var total = 0L;
var tmp = new byte[chunkSize];
int read;

while (-1 != (read = stream.read(tmp))) {
ByteBuffer chunk = ByteBuffer.allocate(read);
var chunk = ByteBuffer.allocate(read);
chunk.put(tmp, 0, read);
chunk.flip();
chunks.add(chunk);
total += read;
}

if (total <= chunkSize) {
byte[] data = new byte[(int) total];
int pos = 0;
for (ByteBuffer chunk : chunks) {
var data = new byte[(int) total];
var pos = 0;
for (var chunk : chunks) {
System.arraycopy(chunk.array(), 0, data, pos, chunk.capacity());
pos += chunk.capacity();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/maxmind/db/CHMCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public CHMCache(int capacity) {

@Override
public DecodedValue get(CacheKey<?> key, Loader loader) throws IOException {
DecodedValue value = cache.get(key);
var value = cache.get(key);
if (value == null) {
value = loader.load(key);
if (!cacheFull) {
Expand Down
3 changes: 0 additions & 3 deletions src/main/java/com/maxmind/db/ClosedDatabaseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
* Signals that the underlying database has been closed.
*/
public class ClosedDatabaseException extends IOException {

private static final long serialVersionUID = 1L;

ClosedDatabaseException() {
super("The MaxMind DB has been closed.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* constructor in the class with the MaxMindDbConstructor annotation.
*/
public class ConstructorNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;

ConstructorNotFoundException(String message) {
super(message);
}
Expand Down
32 changes: 8 additions & 24 deletions src/main/java/com/maxmind/db/DatabaseRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
* lookup.
*
* @param <T> the type to deserialize the returned value to
* @param data the data for the record in the database. The record will be
* {@code null} if there was no data for the address in the
* database.
* @param network the network associated with the record in the database. This is
* the largest network where all of the IPs in the network have the same
* data.
*/
public final class DatabaseRecord<T> {
private final T data;
private final Network network;

public record DatabaseRecord<T>(T data, Network network) {
/**
* Create a new record.
*
Expand All @@ -20,25 +23,6 @@ public final class DatabaseRecord<T> {
* @param prefixLength the network prefix length associated with the record in the database.
*/
public DatabaseRecord(T data, InetAddress ipAddress, int prefixLength) {
this.data = data;
this.network = new Network(ipAddress, prefixLength);
}

/**
* @return the data for the record in the database. The record will be
* <code>null</code> if there was no data for the address in the
* database.
*/
public T getData() {
return data;
}

/**
* @return the network associated with the record in the database. This is
* the largest network where all of the IPs in the network have the same
* data.
*/
public Network getNetwork() {
return network;
this(data, new Network(ipAddress, prefixLength));
}
}
14 changes: 3 additions & 11 deletions src/main/java/com/maxmind/db/DecodedValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@
/**
* {@code DecodedValue} is a wrapper for the decoded value and the number of bytes used
* to decode it.
*
* @param value the decoded value
*/
public final class DecodedValue {
final Object value;

DecodedValue(Object value) {
this.value = value;
}

Object getValue() {
return value;
}
}
record DecodedValue(Object value) {}
Loading
Loading