If it’s worth doing once, it’s worth writing a system to do it.
So I’m exploring writing a DNS server in Python, and while there are a number of solutions for reading and writing DNS data in a variety of formats (such as BIND configuration files, over-the-wire encoding, etc.) I learn best by doing, not by using someone else’s code. Calling it “not invented here” is naïve at best, so let’s get started with the first over-engineered bit I’ve written.
DNS is a binary protocol, which is a seriously good thing compared to protocols such as SMTP, NNTP, POP, and IMAP. It makes rather extensive use of bit masks to represent flags, thus we’ll need a method to encode and decode these bit masks, and a convenient way to display them in a human-readable way. Also attached to this Gist is a copy of the flags.py
file from the dnspython
package, a quite complete and mature package by any standard. Unfortunately, it followed in twisted.names
’ footsteps. If you examine that file, you’ll notice that it defines everything several times; constants to represent the bits, a dictionary to map names to bits, then, separately, inverse mappings, then separate functions to encode and decode the bit fields.
This is horrible, horrible duplication. So to start, I needed an attribute-access dictionary; one is already part of my marrow
suite, so I’ll base my Flags class on that:
class Flags(marrow.util.bunch.Bunch):
Now the first thing we’ll need is the inverse mapping. Since the meaning of the bits can not change during runtime, it’s safe to do so in the __init__
method:
def __init__(self, *args, **kw):
super(Flags, self).__init__(*args, **kw)
self.__dict__['inverse'] = dict(zip(self.itervalues(), self.iterkeys()))
To prevent modification later, we disable __setitem__
and __setattr__
:
def __setitem__(self, name, value):
raise RuntimeError("Flags can not be defined at runtime.")
__setattr__ = __setitem__
We then need to be able to encode (turn a textual representation into binary) and decode (the reverse). First, encoding:
def encode(self, text):
return reduce(operator.__or__, (self[hunk.strip().upper()] for hunk in text.split()), 0)
The above may look slightly insane, but it works like a hot damn. All it does is look up each flag value (the bits) and OR them together. Now for decoding, which is a little harder:
def decode(self, value):
return ' '.join(v for k, v in reversed(sorted(zip(self.itervalues(), self.iterkeys()))) if value & k)
This returns an ordered string containing the flag names. It purposefully does not use the self.inverse
mapping because it needs the results sorted.
See the second attachment (02-marrow-dns-flags.py
) for the complete implementation and example flag sets.