Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## NEXT

## 0.2.0
* Add methods for parsing `ByteArray` representing `IpNetwork` in X509 `IpAddressName`
* `fromX509Octets`
* `toX509Octets`
* Revised generic type arguments
* Introduce `CidrNumber` optimized for CIDR operations
* `CidrNumber.V4` for IPv4
Expand Down
3 changes: 3 additions & 0 deletions cidre/api/android/cidre.api
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,13 @@ public abstract class at/asitplus/cidre/IpNetwork : at/asitplus/cidre/IpAddressA
public final fun overlaps (Lat/asitplus/cidre/IpNetwork;)Z
public final fun plus (Lat/asitplus/cidre/IpNetwork;)Lat/asitplus/cidre/IpNetwork;
public fun toString ()Ljava/lang/String;
public final fun toX509Octets ()[B
}

public final class at/asitplus/cidre/IpNetwork$Companion {
public final fun forAddress-Qn1smSk (Lat/asitplus/cidre/IpAddress;I)Lat/asitplus/cidre/IpNetwork;
public final fun fromX509Octets ([BZ)Lat/asitplus/cidre/IpNetwork;
public static synthetic fun fromX509Octets$default (Lat/asitplus/cidre/IpNetwork$Companion;[BZILjava/lang/Object;)Lat/asitplus/cidre/IpNetwork;
public final fun invoke (Ljava/lang/String;Z)Lat/asitplus/cidre/IpNetwork;
public static synthetic fun invoke$default (Lat/asitplus/cidre/IpNetwork$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lat/asitplus/cidre/IpNetwork;
public final fun invoke-OsBMiQA (Lat/asitplus/cidre/IpAddress;IZ)Lat/asitplus/cidre/IpNetwork;
Expand Down
2 changes: 2 additions & 0 deletions cidre/api/cidre.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ sealed class <#A: kotlin/Number, #B: at.asitplus.cidre.byteops/CidrNumber<#B>> a
final fun isSupernetOf(at.asitplus.cidre/IpNetwork<#A, #B>): kotlin/Boolean // at.asitplus.cidre/IpNetwork.isSupernetOf|isSupernetOf(at.asitplus.cidre.IpNetwork<1:0,1:1>){}[0]
final fun overlaps(at.asitplus.cidre/IpNetwork<#A, #B>): kotlin/Boolean // at.asitplus.cidre/IpNetwork.overlaps|overlaps(at.asitplus.cidre.IpNetwork<1:0,1:1>){}[0]
final fun plus(at.asitplus.cidre/IpNetwork<#A, #B>): at.asitplus.cidre/IpNetwork<#A, #B>? // at.asitplus.cidre/IpNetwork.plus|plus(at.asitplus.cidre.IpNetwork<1:0,1:1>){}[0]
final fun toX509Octets(): kotlin/ByteArray // at.asitplus.cidre/IpNetwork.toX509Octets|toX509Octets(){}[0]
open fun compareTo(at.asitplus.cidre/IpNetwork<#A, #B>): kotlin/Int // at.asitplus.cidre/IpNetwork.compareTo|compareTo(at.asitplus.cidre.IpNetwork<1:0,1:1>){}[0]
open fun equals(kotlin/Any?): kotlin/Boolean // at.asitplus.cidre/IpNetwork.equals|equals(kotlin.Any?){}[0]
open fun hashCode(): kotlin/Int // at.asitplus.cidre/IpNetwork.hashCode|hashCode(){}[0]
Expand Down Expand Up @@ -517,6 +518,7 @@ sealed class <#A: kotlin/Number, #B: at.asitplus.cidre.byteops/CidrNumber<#B>> a
final object Companion { // at.asitplus.cidre/IpNetwork.Companion|null[0]
final fun <#A2: kotlin/Number, #B2: at.asitplus.cidre.byteops/CidrNumber<#B2>> forAddress(at.asitplus.cidre/IpAddress<#A2, #B2>, kotlin/UInt): at.asitplus.cidre/IpNetwork<#A2, #B2> // at.asitplus.cidre/IpNetwork.Companion.forAddress|forAddress(at.asitplus.cidre.IpAddress<0:0,0:1>;kotlin.UInt){0§<kotlin.Number>;1§<at.asitplus.cidre.byteops.CidrNumber<0:1>>}[0]
final fun <#A2: kotlin/Number, #B2: at.asitplus.cidre.byteops/CidrNumber<#B2>> invoke(at.asitplus.cidre/IpAddress<#A2, #B2>, kotlin/UInt, kotlin/Boolean = ...): at.asitplus.cidre/IpNetwork<#A2, #B2> // at.asitplus.cidre/IpNetwork.Companion.invoke|invoke(at.asitplus.cidre.IpAddress<0:0,0:1>;kotlin.UInt;kotlin.Boolean){0§<kotlin.Number>;1§<at.asitplus.cidre.byteops.CidrNumber<0:1>>}[0]
final fun fromX509Octets(kotlin/ByteArray, kotlin/Boolean = ...): at.asitplus.cidre/IpNetwork<*, *> // at.asitplus.cidre/IpNetwork.Companion.fromX509Octets|fromX509Octets(kotlin.ByteArray;kotlin.Boolean){}[0]
final fun invoke(kotlin/String, kotlin/Boolean = ...): at.asitplus.cidre/IpNetwork<*, *> // at.asitplus.cidre/IpNetwork.Companion.invoke|invoke(kotlin.String;kotlin.Boolean){}[0]
}
}
Expand Down
3 changes: 3 additions & 0 deletions cidre/api/jvm/cidre.api
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,13 @@ public abstract class at/asitplus/cidre/IpNetwork : at/asitplus/cidre/IpAddressA
public final fun overlaps (Lat/asitplus/cidre/IpNetwork;)Z
public final fun plus (Lat/asitplus/cidre/IpNetwork;)Lat/asitplus/cidre/IpNetwork;
public fun toString ()Ljava/lang/String;
public final fun toX509Octets ()[B
}

public final class at/asitplus/cidre/IpNetwork$Companion {
public final fun forAddress-Qn1smSk (Lat/asitplus/cidre/IpAddress;I)Lat/asitplus/cidre/IpNetwork;
public final fun fromX509Octets ([BZ)Lat/asitplus/cidre/IpNetwork;
public static synthetic fun fromX509Octets$default (Lat/asitplus/cidre/IpNetwork$Companion;[BZILjava/lang/Object;)Lat/asitplus/cidre/IpNetwork;
public final fun invoke (Ljava/lang/String;Z)Lat/asitplus/cidre/IpNetwork;
public static synthetic fun invoke$default (Lat/asitplus/cidre/IpNetwork$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lat/asitplus/cidre/IpNetwork;
public final fun invoke-OsBMiQA (Lat/asitplus/cidre/IpAddress;IZ)Lat/asitplus/cidre/IpNetwork;
Expand Down
29 changes: 29 additions & 0 deletions cidre/src/commonMain/kotlin/at/asitplus/cidre/IpNetwork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import at.asitplus.cidre.byteops.CidrNumber
import at.asitplus.cidre.byteops.and
import at.asitplus.cidre.byteops.or
import at.asitplus.cidre.byteops.toNetmask
import at.asitplus.cidre.byteops.toPrefix


sealed class IpNetwork<N : Number, S : CidrNumber<S>>
Expand Down Expand Up @@ -210,6 +211,11 @@ constructor(address: IpAddress<N, S>, override val prefix: Prefix, strict: Boole
return address.octets contentEquals (network.address.octets and netmask)
}

/**
* Encodes this network into X.509 iPAddressName ByteArray (RFC 5280).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

illustrate the byte layout in the API Doc

*/
fun toX509Octets(): ByteArray = address.octets + netmask

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is IpNetwork<*, *>) return false
Expand Down Expand Up @@ -528,6 +534,29 @@ constructor(address: IpAddress<N, S>, override val prefix: Prefix, strict: Boole
) as IpNetwork<N, S>
}

/**
* Decodes an IpNetwork from X.509 iPAddressName ByteArray (RFC 5280).
* 8 bytes (IPv4 base+mask)
* 32 bytes (IPv6 base+mask)
*/
@Throws(IllegalArgumentException::class)
fun fromX509Octets(bytes: ByteArray, strict: Boolean = false): IpNetwork<*, *> {
return when (bytes.size) {
8 -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's constants for all octet sizes in (I think) IpFamily

val address = bytes.copyOfRange(0, 4)
val mask = bytes.copyOfRange(4, 8)
val prefix = mask.toPrefix()
V4(IpAddress.V4(address), prefix, strict = strict)
}
32 -> {
val address = bytes.copyOfRange(0, 16)
val mask = bytes.copyOfRange(16, 32)
val prefix = mask.toPrefix()
V6(IpAddress.V6(address), prefix, strict = strict)
}
else -> throw IllegalArgumentException("Invalid iPAddress length: ${bytes.size}")
}
}
}


Expand Down
101 changes: 101 additions & 0 deletions cidre/src/jvmTest/kotlin/IpAddressNameParsingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import at.asitplus.cidre.IpNetwork
import kotlin.reflect.KClass
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class IpAddressNameParsingTest {

// IPv4 raw bytes
private val ipv4WithMask1 = byteArrayOf(0x0a, 0x09, 0x08, 0x00, 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0x00)
private val ipv4WithMask2 = byteArrayOf(0x0a, 0x09, 0x00, 0x00, 0xff.toByte(), 0xff.toByte(), 0x80.toByte(), 0x00)
private val ipv4WithMask3 = byteArrayOf(0x0a, 0x09, 0x00, 0x00, 0xff.toByte(), 0xff.toByte(), 0xc0.toByte(), 0x00)

private val ipv4WithMask1Str = "10.9.8.0/24"
private val ipv4WithMask2Str = "10.9.0.0/17"
private val ipv4WithMask3Str = "10.9.0.0/18"

// IPv6 raw bytes
private val ipv6a = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x8a.toByte(), 0x2e, 0x0a, 0x09, 0x08, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
private val ipv6aMasked = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)

private val ipv6b = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x8a.toByte(), 0x2e, 0x0a, 0x09, 0x08, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte()
)

private val ipv6c = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)

private val ipv6d = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xfe.toByte(),
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
private val ipv6dMasked = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa2.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xfe.toByte(),
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)

private val ipv6e = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0x80.toByte(), 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
private val ipv6eMasked = byteArrayOf(
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
0xff.toByte(), 0xff.toByte(), 0x80.toByte(), 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)

private val ipv6aStr = "2001:db8:85a3::/48"
private val ipv6bStr = "2001:db8:85a3::8a2e:a09:800/128"
private val ipv6cStr = "2001:db8:85a3::/48"
private val ipv6dStr = "2001:db8:85a2::/47"
private val ipv6eStr = "2001:db8:85a3::/49"

@Test fun parseIPv6a() = parseCheck(ipv6a, IpNetwork.V6::class, ipv6aStr, ipv6aMasked)
@Test fun parseIPv6b() = parseCheck(ipv6b, IpNetwork.V6::class, ipv6bStr)
@Test fun parseIPv6c() = parseCheck(ipv6c, IpNetwork.V6::class, ipv6cStr)
@Test fun parseIPv6d() = parseCheck(ipv6d, IpNetwork.V6::class, ipv6dStr, ipv6dMasked)
@Test fun parseIPv6e() = parseCheck(ipv6e, IpNetwork.V6::class, ipv6eStr, ipv6eMasked)

@Test fun parseIPv4mask24() = parseCheck(ipv4WithMask1, IpNetwork.V4::class, ipv4WithMask1Str)
@Test fun parseIPv4mask17() = parseCheck(ipv4WithMask2, IpNetwork.V4::class, ipv4WithMask2Str)
@Test fun parseIPv4mask18() = parseCheck(ipv4WithMask3, IpNetwork.V4::class, ipv4WithMask3Str)

private fun parseCheck(bytes: ByteArray, expectedType: KClass<out IpNetwork<*, *>>, expectedString: String, expectedBytes: ByteArray? = null) {
val network = IpNetwork.fromX509Octets(bytes, false)
assertTrue(expectedType.isInstance(network), "Expected type: $expectedType, but got ${network::class}")
assertContentEquals(expectedBytes ?: bytes, network.toX509Octets())
assertEquals(expectedString, network.toString())
}
}