This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 3911c03a27 refactor: use bit operations for the DNS over TCP length
prefix (#3466)
3911c03a27 is described below
commit 3911c03a27776ac5818e40af8c79dcd730065692
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Aug 29 10:10:10 2026 +0100
refactor: use bit operations for the DNS over TCP length prefix (#3466)
RFC 1035 section 4.2.2 prefixes each DNS over TCP message with its
length as two big endian bytes. Both directions expressed this with
arithmetic rather than bit operations:
encodeLength: ByteString((length / 256).toByte, length.toByte)
decodeLength: ((data(0).toInt + 256) % 256) * 256 + ((data(1) + 256) %
256)
The `+ 256) % 256` pairs are working around Byte being signed, which
`& 0xFF` says directly, and the division and multiplication are a shift.
This is behaviour preserving, not a fix. `decodeLength` was verified
identical over all 65536 byte pairs, and `encodeLength` over the whole
valid length range 0 to 65535. The two forms diverge only for negative
lengths, which cannot occur: the only caller passes a ByteString length.
Adds tests for the prefix itself: a round trip over every representable
length, the expected big endian encoding, decoding of bytes with the
high bit set, and that trailing bytes after the prefix are ignored.
These pass against both the old and the new implementation, as a
behaviour preserving change requires.
---
.../pekko/io/dns/internal/TcpDnsClientSpec.scala | 32 ++++++++++++++++++++++
.../pekko/io/dns/internal/TcpDnsClient.scala | 9 ++++--
2 files changed, 39 insertions(+), 2 deletions(-)
diff --git
a/actor-tests/src/test/scala/org/apache/pekko/io/dns/internal/TcpDnsClientSpec.scala
b/actor-tests/src/test/scala/org/apache/pekko/io/dns/internal/TcpDnsClientSpec.scala
index e756d2b24a..c891f12cd8 100644
---
a/actor-tests/src/test/scala/org/apache/pekko/io/dns/internal/TcpDnsClientSpec.scala
+++
b/actor-tests/src/test/scala/org/apache/pekko/io/dns/internal/TcpDnsClientSpec.scala
@@ -28,6 +28,38 @@ import pekko.testkit.{ EventFilter, ImplicitSender,
PekkoSpec, TestProbe }
class TcpDnsClientSpec extends PekkoSpec with ImplicitSender {
import TcpDnsClient._
+ "The TCP DNS length prefix" should {
+ // RFC 1035 section 4.2.2: each message is prefixed by its length as two
big endian bytes.
+
+ "round trip every representable length" in {
+ for (length <- 0 to 65535) withClue(s"length $length: ") {
+ val encoded = encodeLength(length)
+ encoded.length should ===(2)
+ decodeLength(encoded) should ===(length)
+ }
+ }
+
+ "encode the length as two big endian bytes" in {
+ encodeLength(0) should ===(pekko.util.ByteString(0, 0))
+ encodeLength(1) should ===(pekko.util.ByteString(0, 1))
+ encodeLength(255) should ===(pekko.util.ByteString(0, 255.toByte))
+ encodeLength(256) should ===(pekko.util.ByteString(1, 0))
+ encodeLength(65535) should ===(pekko.util.ByteString(255.toByte,
255.toByte))
+ }
+
+ "decode lengths whose bytes have the high bit set" in {
+ // the bytes are unsigned on the wire; a naive signed conversion gets
these wrong
+ decodeLength(pekko.util.ByteString(0, 255.toByte)) should ===(255)
+ decodeLength(pekko.util.ByteString(255.toByte, 0)) should ===(65280)
+ decodeLength(pekko.util.ByteString(255.toByte, 255.toByte)) should
===(65535)
+ decodeLength(pekko.util.ByteString(128.toByte, 128.toByte)) should
===(32896)
+ }
+
+ "ignore any bytes after the two byte prefix" in {
+ decodeLength(pekko.util.ByteString(1, 2, 3, 4, 5)) should ===(258)
+ }
+ }
+
"The async TCP DNS client" should {
val exampleRequestMessage =
Message(42, MessageFlags(), questions = Seq(Question("pekko.io",
RecordType.A, RecordClass.IN)))
diff --git
a/actor/src/main/scala/org/apache/pekko/io/dns/internal/TcpDnsClient.scala
b/actor/src/main/scala/org/apache/pekko/io/dns/internal/TcpDnsClient.scala
index a85f36d073..4db1d1503d 100644
--- a/actor/src/main/scala/org/apache/pekko/io/dns/internal/TcpDnsClient.scala
+++ b/actor/src/main/scala/org/apache/pekko/io/dns/internal/TcpDnsClient.scala
@@ -99,10 +99,15 @@ import pekko.util.ByteString
}
}
private[internal] object TcpDnsClient {
+
+ /**
+ * DNS over TCP prefixes each message with its length as a two byte big
endian value
+ * (RFC 1035 section 4.2.2).
+ */
def encodeLength(length: Int): ByteString =
- ByteString((length / 256).toByte, length.toByte)
+ ByteString(((length >> 8) & 0xFF).toByte, (length & 0xFF).toByte)
- def decodeLength(data: ByteString): Int = ((data(0).toInt + 256) % 256) *
256 + ((data(1) + 256) % 256)
+ def decodeLength(data: ByteString): Int = ((data(0) & 0xFF) << 8) | (data(1)
& 0xFF)
def throwFailure(message: String, cause: Option[Throwable]): Unit =
cause match {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]