diff --git a/src/main/java/com/maxmind/geoip2/NetworkDeserializer.java b/src/main/java/com/maxmind/geoip2/NetworkDeserializer.java index 2e640149..7932b6f2 100644 --- a/src/main/java/com/maxmind/geoip2/NetworkDeserializer.java +++ b/src/main/java/com/maxmind/geoip2/NetworkDeserializer.java @@ -11,10 +11,10 @@ /** * This class provides a deserializer for the Network class. */ -public class NetworkDeserializer extends StdDeserializer { +public final class NetworkDeserializer extends StdDeserializer { /** - * Constructs a @{code NetworkDeserializer} object. + * Constructs a {@code NetworkDeserializer} with no type specified. */ public NetworkDeserializer() { this(null); @@ -30,23 +30,46 @@ public NetworkDeserializer(Class vc) { } @Override - public Network deserialize( - JsonParser jsonparser, DeserializationContext context) - throws IOException { + public Network deserialize(JsonParser jsonparser, DeserializationContext context) + throws IOException { - String cidr = jsonparser.getText(); - if (cidr == null) { + final String cidr = jsonparser.getValueAsString(); + if (cidr == null || cidr.isBlank()) { return null; } - String[] parts = cidr.split("/", 2); + return parseCidr(cidr); + } + + private static Network parseCidr(String cidr) throws IOException { + final String[] parts = cidr.split("/", 2); if (parts.length != 2) { - throw new RuntimeException("Invalid cidr format: " + cidr); + throw new IllegalArgumentException("Invalid CIDR format: " + cidr); } - int prefixLength = Integer.parseInt(parts[1]); + + final String addrPart = parts[0]; + final String prefixPart = parts[1]; + + final InetAddress address; try { - return new Network(InetAddress.getByName(parts[0]), prefixLength); + address = InetAddress.getByName(addrPart); } catch (UnknownHostException e) { - throw new RuntimeException(e); + throw new IOException("Unknown host in CIDR: " + cidr, e); } + + final int prefixLength; + try { + prefixLength = Integer.parseInt(prefixPart); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid prefix length in CIDR: " + cidr, e); + } + + final int maxPrefix = (address.getAddress().length == 4) ? 32 : 128; + if (prefixLength < 0 || prefixLength > maxPrefix) { + throw new IllegalArgumentException( + "Prefix length out of range (0-" + maxPrefix + ") for CIDR: " + cidr); + } + + return new Network(address, prefixLength); } -} \ No newline at end of file +} diff --git a/src/test/java/com/maxmind/geoip2/NetworkDeserializerTest.java b/src/test/java/com/maxmind/geoip2/NetworkDeserializerTest.java new file mode 100644 index 00000000..60acf26d --- /dev/null +++ b/src/test/java/com/maxmind/geoip2/NetworkDeserializerTest.java @@ -0,0 +1,89 @@ +package com.maxmind.geoip2; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.maxmind.db.Network; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetAddress; + +import static org.junit.jupiter.api.Assertions.*; + +final class NetworkDeserializerTest { + + + private static Network parse(String jsonString) throws IOException { + var deserializer = new NetworkDeserializer(); + JsonFactory jf = new JsonFactory(); + try (JsonParser p = jf.createParser(jsonString)) { + p.nextToken(); + return deserializer.deserialize(p, null); + } + } + private static void assertNetwork(Network n, String addr, int prefix) throws Exception { + assertNotNull(n); + assertEquals(InetAddress.getByName(addr), n.getNetworkAddress()); + assertEquals(prefix, n.getPrefixLength()); + } + + @Test + void parsesValidIPv4Cidr() throws Exception { + Network actual = parse("\"1.2.3.0/24\""); + assertNetwork(actual, "1.2.3.0", 24); + } + + @Test + void parsesValidIPv6Cidr() throws Exception { + Network actual = parse("\"2001:db8::/32\""); + assertNetwork(actual, "2001:db8::", 32); + } + + @Test + void rejectsWhitespaceInCidr() { + assertThrows(IOException.class, () -> parse("\" 10.0.0.0/8 \"")); + } + + + + + @Test + void returnsNullOnJsonNull() throws Exception { + Network actual = parse("null"); + assertNull(actual); + } + + @Test + void returnsNullOnBlankString() throws Exception { + Network actual = parse("\" \""); + assertNull(actual); + } + + @Test + void throwsOnMissingSlash() { + assertThrows(IllegalArgumentException.class, () -> parse("\"1.2.3.0\"")); + } + + @Test + void throwsOnNonNumericPrefix() { + assertThrows(IllegalArgumentException.class, () -> parse("\"1.2.3.0/xx\"")); + } + + @Test + void throwsOnOutOfRangePrefixIpv4() { + assertThrows(IllegalArgumentException.class, () -> parse("\"1.2.3.0/64\"")); + assertThrows(IllegalArgumentException.class, () -> parse("\"1.2.3.0/-1\"")); + } + + @Test + void throwsOnOutOfRangePrefixIpv6() { + assertThrows(IllegalArgumentException.class, () -> parse("\"::/129\"")); + assertThrows(IllegalArgumentException.class, () -> parse("\"::/-1\"")); + } + + @Test + void wrapsUnknownHostInIOException() { + IOException ex = assertThrows(IOException.class, () -> parse("\"999.999.999.999/24\"")); + assertNotNull(ex.getCause()); + } +}