Skip to content

Commit 9d31fc8

Browse files
committed
Add iteration mechanism
1 parent 004265d commit 9d31fc8

File tree

4 files changed

+642
-4
lines changed

4 files changed

+642
-4
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.maxmind.db;
2+
3+
import java.net.InetAddress;
4+
5+
public class BadVersionException extends Exception {
6+
public BadVersionException(InetAddress ip) {
7+
super("you attempted to use an IPv6 network in an IPv4-only database: " + ip.toString());
8+
}
9+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package com.maxmind.db;
2+
3+
import java.io.IOException;
4+
import java.net.Inet4Address;
5+
import java.net.InetAddress;
6+
import java.nio.ByteBuffer;
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Iterator;
10+
11+
public class Networks<T> implements Iterator<DatabaseRecord<T>> {
12+
private final Reader reader;
13+
private ArrayList<NetworkNode> nodes;
14+
private NetworkNode lastNode;
15+
private boolean skipAliasedNetworks;
16+
private Exception err;
17+
private ByteBuffer buffer; /* Stores the buffer for Next() calls */
18+
private Class<T> typeParameterClass;
19+
20+
/**
21+
* Constructs a Networks instance.
22+
* @param reader The reader object.
23+
* @param skipAliasedNetworks The boolean to skip aliased networks.
24+
* @throws ClosedDatabaseException Exception for a closed database.
25+
*/
26+
Networks(Reader reader, boolean skipAliasedNetworks)
27+
throws ClosedDatabaseException {
28+
this(reader, skipAliasedNetworks, new NetworkNode[]{});
29+
}
30+
31+
/**
32+
* Constructs a Networks instance.
33+
* @param reader The reader object.
34+
* @param skipAliasedNetworks The boolean to skip aliased networks.
35+
* @param nodes The initial nodes array to start Networks iterator with.
36+
* @throws ClosedDatabaseException Exception for a closed database.
37+
*/
38+
Networks(Reader reader, boolean skipAliasedNetworks, NetworkNode[] nodes)
39+
throws ClosedDatabaseException {
40+
this.reader = reader;
41+
this.skipAliasedNetworks = skipAliasedNetworks;
42+
this.nodes = new ArrayList<NetworkNode>(Arrays.asList(nodes));
43+
this.buffer = reader.getBufferHolder().get();
44+
}
45+
46+
/**
47+
* Constructs a Networks instance with skipAliasedNetworks set to false by default.
48+
* @param reader The reader object.
49+
*/
50+
Networks(Reader reader) throws ClosedDatabaseException {
51+
this(reader, false);
52+
}
53+
54+
/**
55+
* Returns if Networks had any errors.
56+
* @return Exception The exception to the Networks iteration.
57+
*/
58+
public Exception getErr() {
59+
return this.err;
60+
}
61+
62+
/**
63+
* Sets the Class for the data type in DataRecord.
64+
* @param cls The class object. ( For example, Map.class )
65+
*/
66+
public void setDataClass(Class<T> cls) {
67+
this.typeParameterClass = cls;
68+
}
69+
70+
/**
71+
* Returns the next NetworksItem. You need to set the class using
72+
* prepareForClass before calling next.
73+
* For example,
74+
* networks.prepareForClass(Map.Class);
75+
* Map test = networks.next();
76+
*/
77+
@Override
78+
public DatabaseRecord<T> next() {
79+
if (this.err != null) {
80+
return null;
81+
}
82+
83+
try {
84+
T data = this.reader.resolveDataPointer(
85+
this.buffer, this.lastNode.pointer, this.typeParameterClass);
86+
87+
byte[] ip = this.lastNode.ip;
88+
int prefixLength = this.lastNode.prefix;
89+
90+
// We do this because uses of SkipAliasedNetworks expect the IPv4 networks
91+
// to be returned as IPv4 networks. If we are not skipping aliased
92+
// networks, then the user will get IPv4 networks from the ::FFFF:0:0/96
93+
// network.
94+
if (this.skipAliasedNetworks && isInIpv4Subtree(ip)) {
95+
ip = Arrays.copyOfRange(ip, 12, ip.length);
96+
prefixLength -= 96;
97+
}
98+
99+
// If the ip is in ipv6 form, drop the prefix manually
100+
// as InetAddress converts it to ipv4.
101+
InetAddress ipAddr = InetAddress.getByAddress(ip);
102+
if (ipAddr instanceof Inet4Address && ip.length > 4
103+
&& ip[10] == -1 && ip[11] == -1 && prefixLength > 32) {
104+
prefixLength -= 96;
105+
}
106+
107+
return new DatabaseRecord<T>(data, InetAddress.getByAddress(ip), prefixLength);
108+
} catch (IOException e) {
109+
this.err = e;
110+
return null;
111+
}
112+
}
113+
114+
public boolean isInIpv4Subtree(byte[] ip) {
115+
if (ip.length != 16) {
116+
return false;
117+
}
118+
for (int i = 0; i < 12; i++) {
119+
if (ip[i] != 0) {
120+
return false;
121+
}
122+
}
123+
return true;
124+
}
125+
126+
/*
127+
* Next prepares the next network for reading with the Network method. It
128+
* returns true if there is another network to be processed and false if there
129+
* are no more networks or if there is an error.
130+
*/
131+
@Override
132+
public boolean hasNext() {
133+
if (this.err != null) {
134+
return false;
135+
}
136+
while (!this.nodes.isEmpty()) {
137+
// Pop the last one.
138+
NetworkNode node = this.nodes.remove(this.nodes.size() - 1);
139+
140+
// Next until we don't have data.
141+
while (node.pointer != this.reader.getMetadata().getNodeCount()) {
142+
// This skips IPv4 aliases without hardcoding the networks that the writer
143+
// currently aliases.
144+
if (this.skipAliasedNetworks && this.reader.getIpv4Start() != 0
145+
&& node.pointer == this.reader.getIpv4Start()
146+
&& !isInIpv4Subtree(node.ip)) {
147+
break;
148+
}
149+
150+
if (node.pointer > this.reader.getMetadata().getNodeCount()) {
151+
this.lastNode = node;
152+
return true;
153+
}
154+
155+
byte[] ipRight = Arrays.copyOf(node.ip, node.ip.length);
156+
if (ipRight.length <= (node.prefix >> 3)) {
157+
this.err = new InvalidDatabaseException("Invalid search tree");
158+
return false;
159+
}
160+
161+
ipRight[node.prefix >> 3] |= 1 << (7 - (node.prefix % 8));
162+
163+
try {
164+
int rightPointer = this.reader.readNode(this.buffer, node.pointer, 1);
165+
node.prefix++;
166+
167+
this.nodes.add(new NetworkNode(ipRight, node.prefix, rightPointer));
168+
node.pointer = this.reader.readNode(this.buffer, node.pointer, 0);
169+
} catch (InvalidDatabaseException e) {
170+
this.err = e;
171+
return false;
172+
}
173+
}
174+
}
175+
return false;
176+
}
177+
178+
protected static class NetworkNode {
179+
public byte[] ip;
180+
public int prefix;
181+
public int pointer;
182+
183+
/**
184+
* Constructs a network node for internal use.
185+
* @param ip The ip address of the node.
186+
* @param prefix The prefix of the node.
187+
* @param pointer The node number
188+
*/
189+
public NetworkNode(byte[] ip, int prefix, int pointer) {
190+
this.ip = ip;
191+
this.prefix = prefix;
192+
this.pointer = pointer;
193+
}
194+
}
195+
196+
}

src/main/java/com/maxmind/db/Reader.java

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import java.io.File;
55
import java.io.IOException;
66
import java.io.InputStream;
7+
import java.net.Inet6Address;
78
import java.net.InetAddress;
9+
import java.net.UnknownHostException;
810
import java.nio.ByteBuffer;
911
import java.util.concurrent.ConcurrentHashMap;
1012
import java.util.concurrent.atomic.AtomicReference;
@@ -14,6 +16,8 @@
1416
* addresses can be looked up using the <code>get</code> method.
1517
*/
1618
public final class Reader implements Closeable {
19+
private static final int IPV4_LEN = 4;
20+
private static final int IPV6_LEN = 6;
1721
private static final int DATA_SECTION_SEPARATOR_SIZE = 16;
1822
private static final byte[] METADATA_START_MARKER = {(byte) 0xAB,
1923
(byte) 0xCD, (byte) 0xEF, 'M', 'a', 'x', 'M', 'i', 'n', 'd', '.',
@@ -149,6 +153,10 @@ public <T> T get(InetAddress ipAddress, Class<T> cls) throws IOException {
149153
return getRecord(ipAddress, cls).getData();
150154
}
151155

156+
protected int getIpv4Start() {
157+
return this.ipV4Start;
158+
}
159+
152160
/**
153161
* Looks up <code>ipAddress</code> in the MaxMind DB.
154162
*
@@ -190,7 +198,40 @@ record = this.readNode(buffer, record, bit);
190198
return new DatabaseRecord<>(dataRecord, ipAddress, pl);
191199
}
192200

193-
private BufferHolder getBufferHolder() throws ClosedDatabaseException {
201+
/**
202+
* Creates a Networks iterator.
203+
* Please note that a MaxMind DB may map IPv4 networks into several locations
204+
* in an IPv6 database. This iterator will iterate over all of these locations
205+
* separately. To only iterate over the IPv4 networks once, use the
206+
* SkipAliasedNetworks option.
207+
*
208+
* @param <T> the generics class for the type of the data in DatabaseRecord.
209+
* @param skipAliasedNetworks Enable skipping aliased networks.
210+
* @return Networks The Networks iterator.
211+
* @throws BadVersionException Exception for using an IPv6 network in ipv4-only database.
212+
* @throws ClosedDatabaseException Exception for a closed databased.
213+
* @throws InvalidDatabaseException Exception for an invalid database.
214+
*/
215+
public <T> Networks<T> networks(boolean skipAliasedNetworks) throws
216+
BadVersionException, ClosedDatabaseException, InvalidDatabaseException {
217+
try {
218+
InetAddress ipv4 = InetAddress.getByAddress(new byte[4]);
219+
InetAddress ipv6 = InetAddress.getByAddress(new byte[16]);
220+
Network ipAllV4 = new Network(ipv4, 0); // Mask 32.
221+
Network ipAllV6 = new Network(ipv6, 0); // Mask 128.
222+
223+
if (this.getMetadata().getIpVersion() == 6) {
224+
return this.networksWithIn(ipAllV6, skipAliasedNetworks);
225+
}
226+
return this.networksWithIn(ipAllV4, skipAliasedNetworks);
227+
} catch (UnknownHostException e) {
228+
/* This is returned by getByAddress. This should never happen
229+
as the ipv4 and ipv6 are constants set by us. */
230+
return null;
231+
}
232+
}
233+
234+
protected BufferHolder getBufferHolder() throws ClosedDatabaseException {
194235
BufferHolder bufferHolder = this.bufferHolderReference.get();
195236
if (bufferHolder == null) {
196237
throw new ClosedDatabaseException();
@@ -222,20 +263,98 @@ private int findIpV4StartNode(ByteBuffer buffer)
222263
return node;
223264
}
224265

225-
private int readNode(ByteBuffer buffer, int nodeNumber, int index)
226-
throws InvalidDatabaseException {
266+
/**
267+
* Returns an iterator within the specified network.
268+
* Please note that a MaxMind DB may map IPv4 networks into several locations
269+
* in an IPv6 database. This iterator will iterate over all of these locations
270+
* separately. To only iterate over the IPv4 networks once, use the
271+
* SkipAliasedNetworks option.
272+
* @param <T> Represents the data type(e.g., Map, HastMap, etc.).
273+
* @param network Specifies the network to be iterated.
274+
* @param skipAliasedNetworks Boolean for skipping aliased networks.
275+
* @return Networks
276+
* @throws BadVersionException Exception for using an IPv6 network in ipv4-only database.
277+
* @throws ClosedDatabaseException Exception for a closed databased.
278+
* @throws InvalidDatabaseException Exception for an invalid database.
279+
*/
280+
public <T> Networks<T> networksWithIn(Network network, boolean skipAliasedNetworks)
281+
throws BadVersionException, ClosedDatabaseException, InvalidDatabaseException {
282+
InetAddress networkAddress = network.getNetworkAddress();
283+
if (this.metadata.getIpVersion() == 4 && networkAddress instanceof Inet6Address) {
284+
throw new BadVersionException(networkAddress);
285+
}
286+
287+
byte[] ipBytes = networkAddress.getAddress();
288+
int prefixLength = network.getPrefixLength();
289+
290+
if (this.metadata.getIpVersion() == 6 && ipBytes.length == IPV4_LEN) {
291+
if (skipAliasedNetworks) {
292+
ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
293+
ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3] };
294+
} else {
295+
// Convert it to the IP address (in 16-byte from) of the IPv4 address.
296+
ipBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
297+
-1, -1, // -1 is for 0xff.
298+
ipBytes[0], ipBytes[1], ipBytes[2], ipBytes[3]};
299+
}
300+
prefixLength += 96;
301+
}
302+
303+
int[] traverseResult = this.traverseTree(ipBytes, 0, prefixLength);
304+
int node = traverseResult[0];
305+
int prefix = traverseResult[1];
306+
307+
Networks<T> networks = new Networks<T>(this, skipAliasedNetworks,
308+
new Networks.NetworkNode[]{ new Networks.NetworkNode(ipBytes, prefix, node) });
309+
310+
return networks;
311+
}
312+
313+
/**
314+
* Returns the node number and the prefix for the network.
315+
* @param ip The ip address to travese.
316+
* @param node The number of the node.
317+
* @param bitCount The prefix.
318+
* @return int[]
319+
*/
320+
public int[] traverseTree(byte[] ip, int node, int bitCount)
321+
throws ClosedDatabaseException, InvalidDatabaseException {
322+
int nodeCount = this.metadata.getNodeCount();
323+
int i = 0;
324+
325+
ByteBuffer buffer = this.getBufferHolder().get();
326+
327+
for (; i < bitCount && node < nodeCount; i++) {
328+
int bit = 1 & (ip[i >> 3] >> (7 - (i % 8)));
329+
330+
// bit:0 -> left record.
331+
// bit:1 -> right record.
332+
node = this.readNode(buffer, node, bit);
333+
}
334+
335+
return new int[]{node, i};
336+
}
337+
338+
protected int readNode(ByteBuffer buffer, int nodeNumber, int index)
339+
throws InvalidDatabaseException {
340+
// index is the index of the record within the node, which
341+
// can either be 0 or 1.
227342
int baseOffset = nodeNumber * this.metadata.getNodeByteSize();
228343

229344
switch (this.metadata.getRecordSize()) {
230345
case 24:
346+
// For a 24 bit record, each record is 3 bytes.
231347
buffer.position(baseOffset + index * 3);
232348
return Decoder.decodeInteger(buffer, 0, 3);
233349
case 28:
234350
int middle = buffer.get(baseOffset + 3);
235351

236352
if (index == 0) {
353+
// We get the most significant from the first half
354+
// of the byte. It belongs to the first record.
237355
middle = (0xF0 & middle) >>> 4;
238356
} else {
357+
// We get the most significant byte of the second record.
239358
middle = 0x0F & middle;
240359
}
241360
buffer.position(baseOffset + index * 4);
@@ -249,7 +368,7 @@ private int readNode(ByteBuffer buffer, int nodeNumber, int index)
249368
}
250369
}
251370

252-
private <T> T resolveDataPointer(
371+
protected <T> T resolveDataPointer(
253372
ByteBuffer buffer,
254373
int pointer,
255374
Class<T> cls

0 commit comments

Comments
 (0)