What I'm thinking about is something like this:
struct TTL {
let value: Int32
init? (value: Int32) { /* return nil if negative*/ }
}
/// For something like DoH, not in swift-dns currently
/// Essentially the type can take care of itself for all operations, instead of we having to handle an `Int32` everywhere we use it.
extension TTL: Codable {
init(from decoder: any Decoder) throws { /*throw if negative*/ }
}
struct OPTTTLFieldOrWhatever {
let underlyingValue: TTL
var fieldOne: Int_ { /* get/set field, likely via some bitwise operations*/ }
/// .....
}
Rough Idea.
Currently, in some cases, these "combined" fields (e.g. one Int32, but multiple different inner values) are eagerly decoded, and in some cases not. I'd prefer to make it always not eagerly decode, and provide get/set accessors to do the work, like in the example above.
Prime example of this in swift-dns is the DNS header impl:
Click for Header.Bytes3And4
These are simple bitwise operations but they do still have tests.
Don't mind the custom operators such as &<<<, we can likely get rid of them.
/// Represents Bytes 16 to 31 in the DNS header.
/// That is, QR, OPCODE, AA, TC, RD, RA, ZZ, AD, CD and RCODE.
public struct Bytes3And4: Sendable {
/// private
public var rawValue: UInt16
/// TODO: check whether `truncatingIfNeeded` has a positive impact on performance
/// Compared to just using init().
public var messageType: MessageType {
get {
MessageType(rawValue: UInt8(truncatingIfNeeded: self.rawValue >> 15))!
}
set {
/// clear the 15th bit then set it to the new value
self.rawValue =
(self.rawValue & 0b01111111_11111111)
| UInt16(truncatingIfNeeded: newValue.rawValue) &<<< 15
}
}
public var opCode: OPCode {
get {
OPCode(rawValue: UInt8((rawValue >> 11) & 0xF))!
}
set {
self.rawValue =
(self.rawValue & 0b10000111_11111111)
| UInt16(truncatingIfNeeded: newValue.rawValue) &<<< 11
}
}
public var authoritative: Bool {
get {
(rawValue & 0b00000100_00000000) == 0b00000100_00000000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000100_00000000)
case false: rawValue = (rawValue & 0b11111011_11111111)
}
}
}
public var truncation: Bool {
get {
(rawValue & 0b00000010_00000000) == 0b00000010_00000000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000010_00000000)
case false: rawValue = (rawValue & 0b11111101_11111111)
}
}
}
public var recursionDesired: Bool {
get {
(rawValue & 0b00000001_00000000) == 0b00000001_00000000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000001_00000000)
case false: rawValue = (rawValue & 0b11111110_11111111)
}
}
}
public var recursionAvailable: Bool {
get {
(rawValue & 0b00000000_10000000) == 0b00000000_10000000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000000_10000000)
case false: rawValue = (rawValue & 0b11111111_01111111)
}
}
}
public var authenticData: Bool {
get {
(rawValue & 0b00000000_00100000) == 0b00000000_00100000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000000_00100000)
case false: rawValue = (rawValue & 0b11111111_11011111)
}
}
}
public var checkingDisabled: Bool {
get {
(rawValue & 0b00000000_00010000) == 0b00000000_00010000
}
set {
switch newValue {
case true: rawValue = (rawValue | 0b00000000_00010000)
case false: rawValue = (rawValue & 0b11111111_11101111)
}
}
}
public var responseCode: ResponseCode {
get {
ResponseCode(rawValue & 0b00000000_00001111)
}
set {
self.rawValue = (self.rawValue & 0b11111111_11110000) | newValue.rawValue
}
}
public init(rawValue: UInt16) {
self.rawValue = rawValue
}
}
For reference:
Click for current EDNS impl
EDNS.swift
/// Edns implements the higher level concepts for working with extended dns as it is used to create or be
/// created from OPT record data.
@available(SwiftStdlib 5.1, *)
public struct EDNS: Sendable {
/// EDNS flags
///
/// <https://www.rfc-editor.org/rfc/rfc6891#section-6.1.4>
public struct Flags: Sendable {
/// DNSSEC OK bit as defined by RFC 3225
public var dnssecOk: Bool
/// Remaining bits in the flags field
///
/// Note that the most significant bit in this value is represented by the `dnssec_ok` field.
/// As such, it will be zero when decoding and will not be encoded.
///
/// Unless you have a specific need to set this value, we recommend leaving this as zero.
public var z: UInt16
var rawValue: UInt16 {
switch self.dnssecOk {
case true:
return 0x8000 | self.z
case false:
return 0x7FFF & self.z
}
}
public init(dnssecOk: Bool, z: UInt16) {
self.dnssecOk = dnssecOk
self.z = z
}
}
// high 8 bits that make up the 12 bit total field when included with the 4bit rcode from the
// header (from TTL)
public var rcodeHigh: UInt8
// Indicates the implementation level of the setter. (from TTL)
public var version: UInt8
public var flags: Flags
// max payload size, minimum of 512, (from RR CLASS)
public var maxPayload: UInt16
public var options: OPT
var ttl: UInt32 {
(UInt32(self.rcodeHigh) &<<< 24)
| (UInt32(self.version) &<<< 16)
| UInt32(self.flags.rawValue)
}
public init(rcodeHigh: UInt8, version: UInt8, flags: Flags, maxPayload: UInt16, options: OPT) {
self.rcodeHigh = rcodeHigh
self.version = version
self.flags = flags
self.maxPayload = maxPayload
self.options = options
}
}
@available(SwiftStdlib 5.1, *)
extension EDNS {
package init(fromOPTRecord record: consuming Record) {
assert(record.rdata.recordType == .OPT)
self.rcodeHigh = UInt8(truncatingIfNeeded: (record.ttl & 0xFF00_0000) >> 24)
self.version = UInt8(truncatingIfNeeded: (record.ttl & 0x00FF_0000) >> 16)
self.flags = Flags(from: record.ttl)
self.maxPayload = record.dnsClass.rawValue
self.options = OPT(fromOPTRData: record.rdata)
}
}
@available(SwiftStdlib 5.1, *)
extension EDNS {
package func toRecord() -> Record {
Record(
nameLabels: DomainName.root,
dnsClass: DNSClass(forOPT: self.maxPayload),
ttl: self.ttl,
rdata: RData.OPT(self.options)
)
}
}
@available(SwiftStdlib 5.1, *)
extension EDNS.Flags {
package init(from ttl: UInt32) {
let first16bits = UInt16(truncatingIfNeeded: ttl & 0x0000_FFFF)
self.dnssecOk = (first16bits & 0x8000) == 0x8000
self.z = first16bits & 0x7FFF
}
}
About using protocols, it'd be fine to use protocols if we really need to, but generally I'm not in favor of using protocols mostly to make sure there is no performance hit by users using any MyProtocol.
For a specification like DNS which we already know all the cases, we can just use enums, specially since we now have @nonexhaustive enums since Swift 6.3.
Then If we want to allow custom values or "unknown" values to be passed, we can have one last enum case which simply just allows the base type to be passed.
The ability to pass custom values can be helpful for users that want to experiment ahead with incoming RFCs, which won't be uncommon, but at the same time one could simply just fork the repo and add stuff as well.
I guess I have already implemented all these so my preference is clear in the code (with the caveat that my preference might have had some changes over the past ~year which I haven't worked on swift-dns too much.).
One way I've found nice to provide wrapper types is using dynamicMemberLookup:
Click for SpecializedMessage impl
Specialized Types.swift
/// A ``Message`` that provides convenient access to its ``answers`` by taking care of unwrapping them to ``SpecializedRecords``.
///
/// This type implements ``@dynamicMemberLookup`` over the ``message``, then shadows ``message.answers`` which
/// is of type ``[Record]``, by providing a ``answers`` property which is of the specialized type ``SpecializedRecords<RDataType>``.
@available(SwiftStdlib 5.1, *)
@dynamicMemberLookup
public struct SpecializedMessage<RDataType: RDataConvertible>: Sendable {
public var message: Message
/// TODO: can do more than just `answers`?
/// Use `message.answers` if you want to access the raw records, or if you want to modify them.
public var answers: SpecializedRecords<RDataType> {
SpecializedRecords(records: self.message.answers)
}
public subscript<T>(dynamicMember member: KeyPath<Message, T>) -> T {
/// FIXME: use `read`/`modify` accessors?
get {
self.message[keyPath: member]
}
}
public subscript<T>(dynamicMember member: WritableKeyPath<Message, T>) -> T {
/// FIXME: use `read`/`modify` accessors?
get {
self.message[keyPath: member]
}
set {
self.message[keyPath: member] = newValue
}
}
public init(message: Message) {
self.message = message
}
}
/// A lazy sequence of ``Record``s that correspond to the given ``RDataType``.
///
/// For example you might use the dns resolver to resolve A records for `www.example.com.`.
/// In this case, `www.example.com.` only has CNAME records and no A records.
/// The upstream dns resolvers usually respond with 1-2 few CNAME records followed by 2+ A records.
///
/// What this type does is it filters out the CNAME records and only returns the A records to
/// make the end user's life easier as they never requested CNAME records anyway.
@available(SwiftStdlib 5.1, *)
public struct SpecializedRecords<RDataType: RDataConvertible>: Sendable {
public let records: TinyFastSequence<Record>
public init(records: TinyFastSequence<Record>) {
self.records = records
}
}
@available(SwiftStdlib 5.1, *)
extension SpecializedRecords: Sequence {
/// Complexity: O(n)
@inlinable
public var undeterminedCount: Int {
self.count
}
/// A ``Record`` that provides convenient access to its ``rdata`` by taking care of unwrapping it to ``RDataType``.
///
/// This type implements ``@dynamicMemberLookup`` over the ``record``, then shadows ``record.rdata`` which
/// is of type ``RData``, by providing a ``rdata`` property which is of the specialized type ``RDataType``.
///
/// This type guarantees that `rdata` and `record.rdata` are the same.
/// That's the reason why both properties are marked as `private(set)`:
/// So we don't have to manage syncing `rdata` and `record.rdata` manually.
@dynamicMemberLookup
public struct Element: Sendable {
public let record: Record
public var rdata: RDataType {
try! RDataType(rdata: record.rdata)
}
public subscript<T>(dynamicMember member: KeyPath<Record, T>) -> T {
_read {
yield self.record[keyPath: member]
}
}
public init(record: Record) throws(FromRDataTypeMismatchError<RDataType>) {
self.record = record
/// Test once to ensure the RData is expected
_ = try RDataType(rdata: record.rdata)
}
}
@inlinable
public func makeIterator() -> Iterator {
Iterator(base: self)
}
/// An iterator over a ``SpecializedRecords`` that filters out records that
/// don't correspond to ``RDataType``.
public struct Iterator: Sendable, IteratorProtocol {
@usableFromInline
var baseIterator: TinyFastSequence<Record>.Iterator
@inlinable
init(base: SpecializedRecords<RDataType>) {
self.baseIterator = base.records.makeIterator()
}
@inlinable
public mutating func next() -> Element? {
while true {
guard let record = self.baseIterator.next() else {
return nil
}
/// If the record is of the wrong type, we skip it and continue to the next record.
/// For example we could be getting both `CNAME` and `A` record for a domain name that
/// is `CNAME`ed to another domain name with `A` records.
///
/// So here if the query is a A query, we simply ignore the `CNAME`s.
if let element = try? Element(record: record) {
return element
} else {
/// Got a bad record. Skip and continue to the next record.
continue
}
}
}
}
}
...
/// A ``Record`` that provides convenient access to its ``rdata`` by taking care of unwrapping it to ``RDataType``.
///
/// This type implements ``@dynamicMemberLookup`` over the ``record``, then shadows ``record.rdata`` which
/// is of type ``RData``, by providing a ``rdata`` property which is of the specialized type ``RDataType``.
///
/// This type guarantees that `rdata` and `record.rdata` are the same.
/// That's the reason why both properties are marked as `private(set)`:
/// So we don't have to manage syncing `rdata` and `record.rdata` manually.
@dynamicMemberLookup
public struct Element: Sendable {
public let record: Record
public var rdata: RDataType {
try! RDataType(rdata: record.rdata)
}
public subscript<T>(dynamicMember member: KeyPath<Record, T>) -> T {
_read {
yield self.record[keyPath: member]
}
}
public init(record: Record) throws(FromRDataTypeMismatchError<RDataType>) {
self.record = record
/// Test once to ensure the RData is expected
_ = try RDataType(rdata: record.rdata)
}
}
Notice that record itself has a rdata member. This type is simply shadowing rdata of record with a explicitly-typed rdata, compared to record.rdata which is an enum with all the cases.