Skip to content

Commit 0d12dcb

Browse files
extracted x509Octets parsing logic into IpAddressAndPrefix
1 parent 66b61dc commit 0d12dcb

File tree

4 files changed

+76
-59
lines changed

4 files changed

+76
-59
lines changed

cidre/src/commonMain/kotlin/at/asitplus/cidre/IpAddressAndPrefix.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package at.asitplus.cidre
22

33
import at.asitplus.cidre.byteops.CidrNumber
44
import at.asitplus.cidre.byteops.invInPlace
5+
import at.asitplus.cidre.byteops.toPrefix
56
import kotlin.contracts.ExperimentalContracts
67
import kotlin.contracts.contract
78

@@ -44,6 +45,39 @@ sealed interface IpAddressAndPrefix<N : Number, S : CidrNumber<S>> {
4445
/**`true` if this is (part of) the [IpNetwork.SpecialRanges.multicast] network */
4546
val isMulticast: Boolean
4647

48+
/**
49+
* Encodes this network into X.509 iPAddressName ByteArray (RFC 5280).
50+
* IPv4: [4 bytes base address][4 bytes subnet mask] (8 bytes)
51+
* IPv6: [16 bytes base address][16 bytes subnet mask] (32 bytes)
52+
*/
53+
fun toX509Octets(): ByteArray = address.octets + netmask
54+
55+
companion object {
56+
57+
/**
58+
* Low-level helper used by [IpNetwork.fromX509Octets] and [IpInterface.fromX509Octets]
59+
* to extract base address and CIDR prefix
60+
* IPv4: [4 bytes base address][4 bytes subnet mask] (8 bytes)
61+
* IPv6: [16 bytes base address][16 bytes subnet mask] (32 bytes)
62+
*/
63+
internal fun parseX509Octets(bytes: ByteArray): Pair<IpAddress<*, *>, Prefix> =
64+
when (bytes.size) {
65+
2 * IpFamily.V4.numberOfOctets -> {
66+
val address = bytes.copyOfRange(0, 4)
67+
val mask = bytes.copyOfRange(4, 8)
68+
IpAddress.V4(address) to mask.toPrefix()
69+
}
70+
71+
2 * IpFamily.V6.numberOfOctets -> {
72+
val address = bytes.copyOfRange(0, 16)
73+
val mask = bytes.copyOfRange(16, 32)
74+
IpAddress.V6(address) to mask.toPrefix()
75+
}
76+
77+
else -> throw IllegalArgumentException("Invalid iPAddress length: ${bytes.size}")
78+
}
79+
}
80+
4781
/**
4882
* Sealed base interface of [IpAddress.V4] and [IpInterface.V4] with IPv4-specific add-ons.
4983
*

cidre/src/commonMain/kotlin/at/asitplus/cidre/IpInterface.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package at.asitplus.cidre
22

3+
import at.asitplus.cidre.IpAddressAndPrefix.Companion.parseX509Octets
34
import at.asitplus.cidre.byteops.CidrNumber
45

56

@@ -13,6 +14,8 @@ constructor(override val prefix: Prefix, val network: IpNetwork<N, S>) :
1314

1415
override fun toString(): String = "$address/$prefix"
1516

17+
override fun toX509Octets(): ByteArray = super.toX509Octets()
18+
1619
companion object {
1720
@Suppress("UNCHECKED_CAST")
1821
internal fun <N : Number, S: CidrNumber<S>> unsafe(
@@ -39,6 +42,20 @@ constructor(override val prefix: Prefix, val network: IpNetwork<N, S>) :
3942
val (addr, prefix) = parseIpAndPrefix(stringRepresentation)
4043
return IpInterface(addr, prefix)
4144
}
45+
46+
/**
47+
* Decodes an IpInterface from X.509 iPAddressName ByteArray (RFC 5280).
48+
* IPv4: [4 bytes base address][4 bytes subnet mask] (8 bytes)
49+
* IPv6: [16 bytes base address][16 bytes subnet mask] (32 bytes)
50+
*/
51+
@Throws(IllegalArgumentException::class)
52+
fun fromX509Octets(bytes: ByteArray): IpInterface<*, *> {
53+
val (address, prefix) = parseX509Octets(bytes)
54+
return when (address) {
55+
is IpAddress.V4 -> V4(address, prefix)
56+
is IpAddress.V6 -> V6(address, prefix)
57+
}
58+
}
4259
}
4360

4461
class V4 internal constructor(override val address: IpAddress.V4, prefix: Prefix, network: IpNetwork.V4) :

cidre/src/commonMain/kotlin/at/asitplus/cidre/IpNetwork.kt

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package at.asitplus.cidre
22

3+
import at.asitplus.cidre.IpAddressAndPrefix.Companion.parseX509Octets
34
import at.asitplus.cidre.byteops.CidrNumber
45
import at.asitplus.cidre.byteops.and
56
import at.asitplus.cidre.byteops.or
67
import at.asitplus.cidre.byteops.toNetmask
7-
import at.asitplus.cidre.byteops.toPrefix
88

99

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

214-
/**
215-
* Encodes this network into X.509 iPAddressName ByteArray (RFC 5280).
216-
*/
217-
fun toX509Octets(): ByteArray = address.octets + netmask
218-
219214
override fun equals(other: Any?): Boolean {
220215
if (this === other) return true
221216
if (other !is IpNetwork<*, *>) return false
@@ -536,27 +531,17 @@ constructor(address: IpAddress<N, S>, override val prefix: Prefix, strict: Boole
536531

537532
/**
538533
* Decodes an IpNetwork from X.509 iPAddressName ByteArray (RFC 5280).
539-
* 8 bytes (IPv4 base+mask)
540-
* 32 bytes (IPv6 base+mask)
534+
* IPv4: [4 bytes base address][4 bytes subnet mask] (8 bytes)
535+
* IPv6: [16 bytes base address][16 bytes subnet mask] (32 bytes)
541536
*/
542-
@Throws(IllegalArgumentException::class)
543-
fun fromX509Octets(bytes: ByteArray, strict: Boolean = false): IpNetwork<*, *> {
544-
return when (bytes.size) {
545-
8 -> {
546-
val address = bytes.copyOfRange(0, 4)
547-
val mask = bytes.copyOfRange(4, 8)
548-
val prefix = mask.toPrefix()
549-
V4(IpAddress.V4(address), prefix, strict = strict)
550-
}
551-
32 -> {
552-
val address = bytes.copyOfRange(0, 16)
553-
val mask = bytes.copyOfRange(16, 32)
554-
val prefix = mask.toPrefix()
555-
V6(IpAddress.V6(address), prefix, strict = strict)
556-
}
557-
else -> throw IllegalArgumentException("Invalid iPAddress length: ${bytes.size}")
537+
fun fromX509Octets(bytes: ByteArray): IpNetwork<*, *> {
538+
val (addr, prefix) = parseX509Octets(bytes)
539+
return when (addr) {
540+
is IpAddress.V4 -> V4(addr, prefix)
541+
is IpAddress.V6 -> V6(addr, prefix)
558542
}
559543
}
544+
560545
}
561546

562547

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import at.asitplus.cidre.IpInterface
12
import at.asitplus.cidre.IpNetwork
23
import kotlin.reflect.KClass
34
import kotlin.test.Test
@@ -24,13 +25,6 @@ class IpAddressNameParsingTest {
2425
0xff.toByte(), 0xff.toByte(), 0x00, 0x00,
2526
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
2627
)
27-
private val ipv6aMasked = byteArrayOf(
28-
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
29-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30-
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
31-
0xff.toByte(), 0xff.toByte(), 0x00, 0x00,
32-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
33-
)
3428

3529
private val ipv6b = byteArrayOf(
3630
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
@@ -54,48 +48,35 @@ class IpAddressNameParsingTest {
5448
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xfe.toByte(),
5549
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
5650
)
57-
private val ipv6dMasked = byteArrayOf(
58-
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa2.toByte(), 0x00, 0x00,
59-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
60-
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xfe.toByte(),
61-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
62-
)
6351

6452
private val ipv6e = byteArrayOf(
65-
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
66-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
67-
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
68-
0xff.toByte(), 0xff.toByte(), 0x80.toByte(), 0x00, 0x00, 0x00,
69-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
70-
)
71-
private val ipv6eMasked = byteArrayOf(
7253
0x20, 0x01, 0x0d, 0xb8.toByte(), 0x85.toByte(), 0xa3.toByte(), 0x00, 0x00,
7354
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
7455
0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte(),
7556
0xff.toByte(), 0xff.toByte(), 0x80.toByte(), 0x00, 0x00, 0x00,
7657
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
7758
)
7859

79-
private val ipv6aStr = "2001:db8:85a3::/48"
60+
private val ipv6aStr = "2001:db8:85a3::8a2e:a09:800/48"
8061
private val ipv6bStr = "2001:db8:85a3::8a2e:a09:800/128"
8162
private val ipv6cStr = "2001:db8:85a3::/48"
82-
private val ipv6dStr = "2001:db8:85a2::/47"
63+
private val ipv6dStr = "2001:db8:85a3::/47"
8364
private val ipv6eStr = "2001:db8:85a3::/49"
8465

85-
@Test fun parseIPv6a() = parseCheck(ipv6a, IpNetwork.V6::class, ipv6aStr, ipv6aMasked)
86-
@Test fun parseIPv6b() = parseCheck(ipv6b, IpNetwork.V6::class, ipv6bStr)
87-
@Test fun parseIPv6c() = parseCheck(ipv6c, IpNetwork.V6::class, ipv6cStr)
88-
@Test fun parseIPv6d() = parseCheck(ipv6d, IpNetwork.V6::class, ipv6dStr, ipv6dMasked)
89-
@Test fun parseIPv6e() = parseCheck(ipv6e, IpNetwork.V6::class, ipv6eStr, ipv6eMasked)
66+
@Test fun parseIPv6a() = parseCheck(ipv6a, IpInterface.V6::class, ipv6aStr)
67+
@Test fun parseIPv6b() = parseCheck(ipv6b, IpInterface.V6::class, ipv6bStr)
68+
@Test fun parseIPv6c() = parseCheck(ipv6c, IpInterface.V6::class, ipv6cStr)
69+
@Test fun parseIPv6d() = parseCheck(ipv6d, IpInterface.V6::class, ipv6dStr)
70+
@Test fun parseIPv6e() = parseCheck(ipv6e, IpInterface.V6::class, ipv6eStr)
9071

91-
@Test fun parseIPv4mask24() = parseCheck(ipv4WithMask1, IpNetwork.V4::class, ipv4WithMask1Str)
92-
@Test fun parseIPv4mask17() = parseCheck(ipv4WithMask2, IpNetwork.V4::class, ipv4WithMask2Str)
93-
@Test fun parseIPv4mask18() = parseCheck(ipv4WithMask3, IpNetwork.V4::class, ipv4WithMask3Str)
72+
@Test fun parseIPv4mask24() = parseCheck(ipv4WithMask1, IpInterface.V4::class, ipv4WithMask1Str)
73+
@Test fun parseIPv4mask17() = parseCheck(ipv4WithMask2, IpInterface.V4::class, ipv4WithMask2Str)
74+
@Test fun parseIPv4mask18() = parseCheck(ipv4WithMask3, IpInterface.V4::class, ipv4WithMask3Str)
9475

95-
private fun parseCheck(bytes: ByteArray, expectedType: KClass<out IpNetwork<*, *>>, expectedString: String, expectedBytes: ByteArray? = null) {
96-
val network = IpNetwork.fromX509Octets(bytes, false)
97-
assertTrue(expectedType.isInstance(network), "Expected type: $expectedType, but got ${network::class}")
98-
assertContentEquals(expectedBytes ?: bytes, network.toX509Octets())
99-
assertEquals(expectedString, network.toString())
76+
private fun parseCheck(bytes: ByteArray, expectedType: KClass<out IpInterface<*, *>>, expectedString: String) {
77+
val addressAndPrefix = IpInterface.fromX509Octets(bytes)
78+
assertTrue(expectedType.isInstance(addressAndPrefix))
79+
assertContentEquals(bytes, addressAndPrefix.toX509Octets())
80+
assertEquals(expectedString, addressAndPrefix.toString())
10081
}
10182
}

0 commit comments

Comments
 (0)