summaryrefslogtreecommitdiff
path: root/pytest/protocol.py
diff options
context:
space:
mode:
Diffstat (limited to 'pytest/protocol.py')
-rw-r--r--pytest/protocol.py1998
1 files changed, 1998 insertions, 0 deletions
diff --git a/pytest/protocol.py b/pytest/protocol.py
new file mode 100644
index 000000000000..6b0cd9ad0327
--- /dev/null
+++ b/pytest/protocol.py
@@ -0,0 +1,1998 @@
+#! /usr/bin/env python
+
+"""
+Protocol definitions for python based lib9p server/client.
+
+The sub-namespace td has type definitions (qid, stat) and values
+that are "#define" constants in C code (e.g., DMDIR, QTFILE, etc).
+This also contains the byte values for protocol codes like Tversion,
+Rversion, Rerror, and so on.
+
+ >>> td.Tversion
+ 100
+ >>> td.Rlerror
+ 7
+
+The qid and stat types are PFOD classes and generate instances that
+are a cross between namedtuple and OrderedDictionary (see pfod.py
+for details):
+
+ >>> td.qid(type=td.QTFILE, path=2, version=1)
+ qid(type=0, version=1, path=2)
+
+The td.stat() type output is pretty long, since it has all the
+dotu-specific members (used only when packing for dotu/dotl and
+set only when unpacking those), so here's just one field:
+
+ >>> td.stat(*(15 * [0])).mode
+ 0
+ >>> import pprint; pprint.pprint(td.stat()._fields)
+ ('type',
+ 'dev',
+ 'qid',
+ 'mode',
+ 'atime',
+ 'mtime',
+ 'length',
+ 'name',
+ 'uid',
+ 'gid',
+ 'muid',
+ 'extension',
+ 'n_uid',
+ 'n_gid',
+ 'n_muid')
+
+Stat objects sent across the protocol must first be encoded into
+wirestat objects, which are basically size-counted pre-sequenced
+stat objects. The pre-sequencing uses:
+
+ >>> td.stat_seq
+ Sequencer('stat')
+
+For parsing bytes returned in a Tread on a directory, td.wirestat_seq
+is the sequencer. However, most users should rely on the packers and
+unpackers in each protocol (see {pack,unpack}_wirestat below).
+
+ >>> td.wirestat_seq
+ Sequencer('wirestat')
+
+There is a dictionary fcall_to_name that maps from byte value
+to protocol code. Names map to themselves as well:
+
+ >>> fcall_names[101]
+ 'Rversion'
+ >>> fcall_names['Tversion']
+ 'Tversion'
+
+The sub-namespace rrd has request (Tversion, Topen, etc) and
+response (Rversion, Ropen, etc) data definitions. Each of these
+is a PFOD class:
+
+ >>> rrd.Tversion(1000, 'hello', tag=0)
+ Tversion(tag=0, msize=1000, version='hello')
+
+The function p9_version() looks up the instance of each supported
+protocol, or raises a KeyError when given an invalid protocol.
+The names may be spelled in any mixture of cases.
+
+The names plain, dotu, and dotl are predefined as the three
+supported protocols:
+
+ >>> p9_version('invalid')
+ Traceback (most recent call last):
+ ...
+ KeyError: 'invalid'
+ >>> p9_version('9p2000') == plain
+ True
+ >>> p9_version('9P2000') == plain
+ True
+ >>> p9_version('9P2000.u') == dotu
+ True
+ >>> p9_version('9p2000.L') == dotl
+ True
+
+Protocol instances have a pack() method that encodes a set of
+arguments into a packet. To know what to encode, pack() must
+receive an fcall value and a dictionary containing argument
+values, or something equivalent. The required argument values
+depend on the fcall. For instance, a Tversion fcall needs three
+arguments: the version name, the tag, and the msize (these of
+course are the pre-filled fields in a Tversion PFOD instance).
+
+ >>> args = {'version': '!', 'tag': 1, 'msize': 1000}
+ >>> pkt = dotu.pack(fcall='Tversion', args=args)
+ >>> len(pkt)
+ 14
+
+The length of string '!' is 1, and the packet (or wire) format of
+a Tversion request is:
+
+ size[4] fcall[1] tag[2] msize[4] version[s]
+
+which corresponds to a struct's IBHIH (for the fixed size parts)
+followed by 1 B (for the string). The overall packet is 14 bytes
+long, so we have size=9, fcall=100, tag=1, msize=1000, and the
+version string is length=1, value=33 (ord('!')).
+
+ >>> import struct
+ >>> struct.unpack('<IBHIHB', pkt)
+ (14, 100, 1, 1000, 1, 33)
+
+Of course, this packed a completely bogus "version" string, but
+that's what we told it to do. Protocol instances remember their
+version, so we can get it right by omitting the version from the
+arguments:
+
+ >>> dotu.version
+ '9P2000.u'
+ >>> args = {'tag': 99, 'msize': 1000}
+ >>> pkt = dotu.pack(fcall='Tversion', args=args)
+ >>> len(pkt)
+ 21
+
+The fcall can be supplied numerically:
+
+ >>> pkt2 = dotu.pack(fcall=td.Tversion, args=args)
+ >>> pkt == pkt2
+ True
+
+Instead of providing an fcall you can provide an instance of
+the appropriate PFOD. In this case pack() finds the type from
+the PFOD instance. As usual, the version parameter is filled in
+for you:
+
+ >>> pkt2 = dotu.pack(rrd.Tversion(tag=99, msize=1000))
+ >>> pkt == pkt2
+ True
+
+Note that it's up to you to check the other end's version and
+switch to a "lower" protocol as needed. Each instance does provide
+a downgrade_to() method that gets you a possibly-downgraded instance.
+This will fail if you are actually trying to upgrade, and also if
+you provide a bogus version:
+
+ >>> dotu.downgrade_to('9P2000.L')
+ Traceback (most recent call last):
+ ...
+ KeyError: '9P2000.L'
+ >>> dotu.downgrade_to('we never heard of this protocol')
+ Traceback (most recent call last):
+ ...
+ KeyError: 'we never heard of this protocol'
+
+Hence you might use:
+
+ try:
+ proto = protocol.dotl.downgrade(vstr)
+ except KeyError:
+ pkt = protocol.plain.pack(fcall='Rerror',
+ args={'tag': tag, 'errstr': 'unknown protocol version '
+ '{0!r}'.format(vstr)})
+ else:
+ pkt = proto.pack(fcall='Rversion', args={'tag': tag, 'msize': msize})
+
+When using a PFOD instance, it is slightly more efficient to use
+pack_from():
+
+ try:
+ proto = protocol.dotl.downgrade(vstr)
+ reply = protocol.rrd.Rversion(tag=tag, msize=msize)
+ except KeyError:
+ proto = protocol.plain
+ reply = protocol.rrd.Rerror(tag=tag,
+ errstr='unknown protocol version {0!r}'.format(vstr))
+ pkt = proto.pack_from(reply)
+
+does the equivalent of the try/except/else variant. Note that
+the protocol.rrd.Rversion() instance has version=None. Like
+proto.pack, the pack_from will detect this "missing" value and
+fill it in.
+
+Because errors vary (one should use Rlerror for dotl and Rerror
+for dotu and plain), and it's convenient to use an Exception
+instance for an error, all protocols provide .error(). This
+builds the appropriate kind of error response, extracting and
+converting errno's and error messages as appropriate.
+
+If <err> is an instance of Exception, err.errno provides the errnum
+or ecode value (if used, for dotu and dotl) and err.strerror as the
+errstr value (if used, for plain 9p2000). Otherwise err should be
+an integer, and we'll use os.strerror() to get a message.
+
+When using plain 9P2000 this sends error *messages*:
+
+ >>> import errno, os
+ >>> utf8 = os.strerror(errno.ENOENT).encode('utf-8')
+ >>> pkt = None
+ >>> try:
+ ... os.open('presumably this file does not exist here', 0)
+ ... except OSError as err:
+ ... pkt = plain.error(1, err)
+ ...
+ >>> pkt[-len(utf8):] == utf8
+ True
+ >>> pkt2 = plain.error(1, errno.ENOENT)
+ >>> pkt == pkt2
+ True
+
+When using 9P2000.u it sends the error code as well, and when
+using 9P2000.L it sends only the error code (and more error
+codes can pass through):
+
+ >>> len(pkt)
+ 34
+ >>> len(dotu.error(1, errno.ENOENT))
+ 38
+ >>> len(dotl.error(1, errno.ENOENT))
+ 11
+
+For even more convenience (and another slight speed hack), the
+protocol has member functions for each valid pfod, which
+effectively do a pack_from of a pfod built from the arguments. In
+the above example this is not very useful (because we want two
+different replies), but for Rlink, for instance, which has only
+a tag, a server might implement Tlink() as:
+
+ def do_Tlink(proto, data): # data will be a protocol.rrd.Tlink(...)
+ tag = data.tag
+ dfid = data.dfid
+ fid = data.fid
+ name = data.name
+ ... some code to set up for doing the link link ...
+ try:
+ os.link(path1, path2)
+ except OSError as err:
+ return proto.error(tag, err)
+ else:
+ return proto.Rlink(tag)
+
+ >>> pkt = dotl.Rlink(12345)
+ >>> struct.unpack('<IBH', pkt)
+ (7, 71, 12345)
+
+Similarly, a client can build a Tversion packet quite trivially:
+
+ >>> vpkt = dotl.Tversion(tag=0, msize=12345)
+
+To see that this is a valid version packet, let's unpack its bytes.
+The overall length is 21 bytes: 4 bytes of size, 1 byte of code 100
+for Tversion, 2 bytes of tag, 4 bytes of msize, 2 bytes of string
+length, and 8 bytes of string '9P2000.L'.
+
+ >>> tup = struct.unpack('<IBHIH8B', vpkt)
+ >>> tup[0:5]
+ (21, 100, 0, 12345, 8)
+ >>> ''.join(chr(i) for i in tup[5:])
+ '9P2000.L'
+
+Of course, since you can *pack*, you can also *unpack*. It's
+possible that the incoming packet is malformed. If so, this
+raises various errors (see below).
+
+Unpack is actually a two step process: first we unpack a header
+(where the size is already removed and is implied by len(data)),
+then we unpack the data within the packet. You can invoke the
+first step separately. Furthermore, there's a noerror argument
+that leaves some fields set to None or empty strings, if the
+packet is too short. (Note that we need a hack for py2k vs py3k
+strings here, for doctests. Also, encoding 12345 into a byte
+string produces '90', by ASCII luck!)
+
+ >>> pkt = pkt[4:] # strip generated size
+ >>> import sys
+ >>> py3k = sys.version_info[0] >= 3
+ >>> b2s = lambda x: x.decode('utf-8') if py3k else x
+ >>> d = plain.unpack_header(pkt[0:1], noerror=True)
+ >>> d.data = b2s(d.data)
+ >>> d
+ Header(size=5, dsize=0, fcall=71, data='')
+ >>> d = plain.unpack_header(pkt[0:2], noerror=True)
+ >>> d.data = b2s(d.data)
+ >>> d
+ Header(size=6, dsize=1, fcall=71, data='9')
+
+Without noerror=True a short packet raises a SequenceError:
+
+ >>> plain.unpack_header(pkt[0:0]) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: out of data while unpacking 'fcall'
+
+Of course, a normal packet decodes fine:
+
+ >>> d = plain.unpack_header(pkt)
+ >>> d.data = b2s(d.data)
+ >>> d
+ Header(size=7, dsize=2, fcall=71, data='90')
+
+but one that is too *long* potentially raises a SequencError.
+(This is impossible for a header, though, since the size and
+data size are both implied: either there is an fcall code, and
+the rest of the bytes are "data", or there isn't and the packet
+is too short. So we can only demonstrate this for regular
+unpack; see below.)
+
+Note that all along, this has been decoding Rlink (fcall=71),
+which is not valid for plain 9P2000 protocol. It's up to the
+caller to check:
+
+ >>> plain.supports(71)
+ False
+
+ >>> plain.unpack(pkt) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: invalid fcall 'Rlink' for 9P2000
+ >>> dotl.unpack(pkt)
+ Rlink(tag=12345)
+
+However, the unpack() method DOES check that the fcall type is
+valid, even if you supply noerror=True. This is because we can
+only really decode the header, not the data, if the fcall is
+invalid:
+
+ >>> plain.unpack(pkt, noerror=True) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: invalid fcall 'Rlink' for 9P2000
+
+The same applies to much-too-short packets even if noerror is set.
+Specifically, if the (post-"size") header shortens down to the empty
+string, the fcall will be None:
+
+ >>> dotl.unpack(b'', noerror=True) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: invalid fcall None for 9P2000.L
+
+If there is at least a full header, though, noerror will do the obvious:
+
+ >>> dotl.unpack(pkt[0:1], noerror=True)
+ Rlink(tag=None)
+ >>> dotl.unpack(pkt[0:2], noerror=True)
+ Rlink(tag=None)
+
+If the packet is too long, noerror suppresses the SequenceError:
+
+ >>> dotl.unpack(pkt + b'x') # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: 1 byte(s) unconsumed
+ >>> dotl.unpack(pkt + b'x', noerror=True)
+ Rlink(tag=12345)
+
+To pack a stat object when producing data for reading a directory,
+use pack_wirestat. This puts a size in front of the packed stat
+data (they're represented this way in read()-of-directory data,
+but not elsewhere).
+
+To unpack the result of a Tstat or a read() on a directory, use
+unpack_wirestat. The stat values are variable length so this
+works with offsets. If the packet is truncated, you'll get a
+SequenceError, but just as for header unpacking, you can use
+noerror to suppress this.
+
+(First, we'll need to build some valid packet data.)
+
+ >>> statobj = td.stat(type=0,dev=0,qid=td.qid(0,0,0),mode=0,
+ ... atime=0,mtime=0,length=0,name=b'foo',uid=b'0',gid=b'0',muid=b'0')
+ >>> data = plain.pack_wirestat(statobj)
+ >>> len(data)
+ 55
+
+Now we can unpack it:
+
+ >>> newobj, offset = plain.unpack_wirestat(data, 0)
+ >>> newobj == statobj
+ True
+ >>> offset
+ 55
+
+Since the packed data do not include the dotu extensions, we get
+a SequenceError if we try to unpack with dotu or dotl:
+
+ >>> dotu.unpack_wirestat(data, 0) # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ SequenceError: out of data while unpacking 'extension'
+
+When using noerror, the returned new offset will be greater
+than the length of the packet, after a failed unpack, and some
+elements may be None:
+
+ >>> newobj, offset = plain.unpack_wirestat(data[0:10], 0, noerror=True)
+ >>> offset
+ 55
+ >>> newobj.length is None
+ True
+
+Similarly, use unpack_dirent to unpack the result of a dot-L
+readdir(), using offsets. (Build them with pack_dirent.)
+
+ >>> dirent = td.dirent(qid=td.qid(1,2,3),offset=0,
+ ... type=td.DT_REG,name=b'foo')
+ >>> pkt = dotl.pack_dirent(dirent)
+ >>> len(pkt)
+ 27
+
+and then:
+
+ >>> newde, offset = dotl.unpack_dirent(pkt, 0)
+ >>> newde == dirent
+ True
+ >>> offset
+ 27
+
+"""
+
+from __future__ import print_function
+
+import collections
+import os
+import re
+import sys
+
+import p9err
+import pfod
+import sequencer
+
+SequenceError = sequencer.SequenceError
+
+fcall_names = {}
+
+# begin ???
+# to interfere with (eg) the size part of the packet:
+# pkt = proto.pack(fcall=protocol.td.Tversion,
+# size=123, # wrong
+# args={ 'tag': 1, msize: 1000, version: '9p2000.u' })
+# a standard Twrite:
+# pkt = proto.pack(fcall=protocol.td.Twrite,
+# args={ 'tag': 1, 'fid': 2, 'offset': 0, 'data': b'rawdata' })
+# or:
+# pkt = proto.pack(fcall=protocol.td.Twrite,
+# data=proto.Twrite(tag=1, fid=2, offset=0, data=b'rawdata' })
+# a broken Twrite:
+# pkt = proto.pack(fcall=protocol.td.Twrite,
+# args={ 'tag': 1, 'fid': 2, 'offset': 0, 'count': 99,
+# 'data': b'rawdata' }) -- XXX won't work (yet?)
+#
+# build a QID: (td => typedefs and defines)
+# qid = protocol.td.qid(type=protocol.td.QTFILE, version=1, path=2)
+# build the Twrite data as a data structure:
+# wrdata = protocol.td.Twrite(tag=1, fid=2, offset=0, data=b'rawdata')
+#
+# turn incoming byte stream data into a Header and remaining data:
+# foo = proto.pack(data)
+
+class _PackInfo(object):
+ """
+ Essentially just a Sequencer, except that we remember
+ if there are any :auto annotations on any of the coders,
+ and we check for coders that are string coders ('data[size]').
+
+ This could in theory be a recursive check, but in practice
+ all the automatics are at the top level, and we have no mechanism
+ to pass down inner automatics.
+ """
+ def __init__(self, seq):
+ self.seq = seq
+ self.autos = None
+ for pair in seq: # (cond, code) pair
+ sub = pair[1]
+ if sub.aux is None:
+ continue
+ assert sub.aux == 'auto' or sub.aux == 'len'
+ if self.autos is None:
+ self.autos = []
+ self.autos.append(pair)
+
+ def __repr__(self):
+ return '{0}({1!r})'.format(self.__class__.__name__, self.seq)
+
+ def pack(self, auto_vars, conditions, data, rodata):
+ """
+ Pack data. Insert automatic and/or counted variables
+ automatically, if they are not already set in the data.
+
+ If rodata ("read-only data") is True we make sure not
+ to modify the caller's data. Since data is a PFOD rather
+ than a normal ordered dictionary, we use _copy().
+ """
+ if self.autos:
+ for cond, sub in self.autos:
+ # False conditionals don't need to be filled-in.
+ if cond is not None and not conditions[cond]:
+ continue
+ if sub.aux == 'auto':
+ # Automatic variable, e.g., version. The
+ # sub-coder's name ('version') is the test item.
+ if data.get(sub.name) is None:
+ if rodata:
+ data = data._copy()
+ rodata = False
+ data[sub.name] = auto_vars[sub.name]
+ else:
+ # Automatic length, e.g., data[count]. The
+ # sub-coders's repeat item ('count') is the
+ # test item. Of course, it's possible that
+ # the counted item is missing as well. If so
+ # we just leave both None and take the
+ # encoding error.
+ assert sub.aux == 'len'
+ if data.get(sub.repeat) is not None:
+ continue
+ item = data.get(sub.name)
+ if item is not None:
+ if rodata:
+ data = data._copy()
+ rodata = False
+ data[sub.repeat] = len(item)
+ return self.seq.pack(data, conditions)
+
+class _P9Proto(object):
+ def __init__(self, auto_vars, conditions, p9_data, pfods, index):
+ self.auto_vars = auto_vars # currently, just version
+ self.conditions = conditions # '.u'
+ self.pfods = pfods # dictionary, maps pfod to packinfo
+ self.index = index # for comparison: plain < dotu < dotl
+
+ self.use_rlerror = rrd.Rlerror in pfods
+
+ for dtype in pfods:
+ name = dtype.__name__
+ # For each Txxx/Rxxx, define a self.<name>() to
+ # call self.pack_from().
+ #
+ # The packinfo is from _Packinfo(seq); the fcall and
+ # seq come from p9_data.protocol[<name>].
+ proto_tuple = p9_data.protocol[name]
+ assert dtype == proto_tuple[0]
+ packinfo = pfods[dtype]
+ # in theory we can do this with no names using nested
+ # lambdas, but that's just too confusing, so let's
+ # do it with nested functions instead.
+ def builder(constructor=dtype, packinfo=packinfo):
+ "return function that calls _pack_from with built PFOD"
+ def invoker(self, *args, **kwargs):
+ "build PFOD and call _pack_from"
+ return self._pack_from(constructor(*args, **kwargs),
+ rodata=False, caller=None,
+ packinfo=packinfo)
+ return invoker
+ func = builder()
+ func.__name__ = name
+ func.__doc__ = 'pack from {0}'.format(name)
+ setattr(self.__class__, name, func)
+
+ def __repr__(self):
+ return '{0}({1!r})'.format(self.__class__.__name__, self.version)
+
+ def __str__(self):
+ return self.version
+
+ # define rich-comparison operators, so we can, e.g., test vers > plain
+ def __lt__(self, other):
+ return self.index < other.index
+ def __le__(self, other):
+ return self.index <= other.index
+ def __eq__(self, other):
+ return self.index == other.index
+ def __ne__(self, other):
+ return self.index != other.index
+ def __gt__(self, other):
+ return self.index > other.index
+ def __ge__(self, other):
+ return self.index >= other.index
+
+ def downgrade_to(self, other_name):
+ """
+ Downgrade from this protocol to a not-greater one.
+
+ Raises KeyError if other_name is not a valid protocol,
+ or this is not a downgrade (with setting back to self
+ considered a valid "downgrade", i.e., we're doing subseteq
+ rather than subset).
+ """
+ if not isinstance(other_name, str) and isinstance(other_name, bytes):
+ other_name = other_name.decode('utf-8', 'surrogateescape')
+ other = p9_version(other_name)
+ if other > self:
+ raise KeyError(other_name)
+ return other
+
+ def error(self, tag, err):
+ "produce Rerror or Rlerror, whichever is appropriate"
+ if isinstance(err, Exception):
+ errnum = err.errno
+ errmsg = err.strerror
+ else:
+ errnum = err
+ errmsg = os.strerror(errnum)
+ if self.use_rlerror:
+ return self.Rlerror(tag=tag, ecode=p9err.to_dotl(errnum))
+ return self.Rerror(tag=tag, errstr=errmsg,
+ errnum=p9err.to_dotu(errnum))
+
+ def pack(self, *args, **kwargs):
+ "pack up a pfod or fcall-and-arguments"
+ fcall = kwargs.pop('fcall', None)
+ if fcall is None:
+ # Called without fcall=...
+ # This requires that args have one argument that
+ # is the PFOD; kwargs should be empty (but we'll take
+ # data=pfod as well). The size is implied, and
+ # fcall comes from the pfod.
+ data = kwargs.pop('data', None)
+ if data is None:
+ if len(args) != 1:
+ raise TypeError('pack() with no fcall requires 1 argument')
+ data = args[0]
+ if len(kwargs):
+ raise TypeError('pack() got an unexpected keyword argument '
+ '{0}'.format(kwargs.popitem()[0]))
+ return self._pack_from(data, True, 'pack', None)
+
+ # Called as pack(fcall=whatever, data={...}).
+ # The data argument must be a dictionary since we're going to
+ # apply ** to it in the call to build the PFOD. Note that
+ # it could already be a PFOD, which is OK, but we're going to
+ # copy it to a new one regardless (callers that have a PFOD
+ # should use pack_from instead).
+ if len(args):
+ raise TypeError('pack() got unexpected arguments '
+ '{0!r}'.format(args))
+ data = kwargs.pop('args', None)
+ if len(kwargs):
+ raise TypeError('pack() got an unexpected keyword argument '
+ '{0}'.format(kwargs.popitem()[0]))
+ if not isinstance(data, dict):
+ raise TypeError('pack() with fcall and data '
+ 'requires data to be a dictionary')
+ try:
+ name = fcall_names[fcall]
+ except KeyError:
+ raise TypeError('pack(): {0} is not a valid '
+ 'fcall value'.format(fcall))
+ cls = getattr(rrd, name)
+ data = cls(**data)
+ return self._pack_from(data, False, 'pack', None)
+
+ def pack_from(self, data):
+ "pack from pfod data, using its type to determine fcall"
+ return self._pack_from(data, True, 'pack_from', None)
+
+ def _pack_from(self, data, rodata, caller, packinfo):
+ """
+ Internal pack(): called from both invokers (self.Tversion,
+ self.Rwalk, etc.) and from pack and pack_from methods.
+ "caller" says which. If rodata is True we're not supposed to
+ modify the incoming data, as it may belong to someone
+ else. Some calls to pack() build a PFOD and hence pass in
+ False.
+
+ The predefined invokers pass in a preconstructed PFOD,
+ *and* set rodata=False, *and* provide a packinfo, so that
+ we never have to copy, nor look up the packinfo.
+ """
+ if caller is not None:
+ assert caller in ('pack', 'pack_from') and packinfo is None
+ # Indirect call from pack_from(), or from pack() after
+ # pack() built a PFOD. We make sure this kind of PFOD
+ # is allowed for this protocol.
+ packinfo = self.pfods.get(data.__class__, None)
+ if packinfo is None:
+ raise TypeError('{0}({1!r}): invalid '
+ 'input'.format(caller, data))
+
+ # Pack the data
+ pkt = packinfo.pack(self.auto_vars, self.conditions, data, rodata)
+
+ fcall = data.__class__.__name__
+ fcall_code = getattr(td, fcall)
+
+ # That's the inner data; now we must add the header,
+ # with fcall (translated back to byte code value) and
+ # outer data. The size is implied by len(pkt). There
+ # are no other auto variables, and no conditions.
+ #
+ # NB: the size includes the size of the header itself
+ # and the fcall code byte, plus the size of the data.
+ data = _9p_data.header_pfod(size=4 + 1 + len(pkt), dsize=len(pkt),
+ fcall=fcall_code, data=pkt)
+ empty = None # logically should be {}, but not actually used below
+ pkt = _9p_data.header_pack_seq.pack(data, empty)
+ return pkt
+
+ @staticmethod
+ def unpack_header(bstring, noerror=False):
+ """
+ Unpack header.
+
+ We know that our caller has already stripped off the
+ overall size field (4 bytes), leaving us with the fcall
+ (1 byte) and data (len(bstring)-1 bytes). If len(bstring)
+ is 0, this is an invalid header: set dsize to 0 and let
+ fcall become None, if noerror is set.
+ """
+ vdict = _9p_data.header_pfod()
+ vdict['size'] = len(bstring) + 4
+ vdict['dsize'] = max(0, len(bstring) - 1)
+ _9p_data.header_unpack_seq.unpack(vdict, None, bstring, noerror)
+ return vdict
+
+ def unpack(self, bstring, noerror=False):
+ "produce filled PFOD from fcall in packet"
+ vdict = self.unpack_header(bstring, noerror)
+ # NB: vdict['dsize'] is used internally during unpack, to
+ # find out how many bytes to copy to vdict['data'], but by
+ # the time unpack is done, we no longer need it.
+ #
+ # size = vdict['size']
+ # dsize = vdict['dsize']
+ fcall = vdict['fcall']
+ data = vdict['data']
+ # Note: it's possible for size and/or fcall to be None,
+ # when noerror is true. However, if we support fcall, then
+ # clearly fcall is not None; and since fcall follows size,
+ # we can always proceed if we support fcall.
+ if self.supports(fcall):
+ fcall = fcall_names[fcall]
+ cls = getattr(rrd, fcall)
+ seq = self.pfods[cls].seq
+ elif fcall == td.Rlerror:
+ # As a special case for diod, we accept Rlerror even
+ # if it's not formally part of the protocol.
+ cls = rrd.Rlerror
+ seq = dotl.pfods[rrd.Rlerror].seq
+ else:
+ fcall = fcall_names.get(fcall, fcall)
+ raise SequenceError('invalid fcall {0!r} for '
+ '{1}'.format(fcall, self))
+ vdict = cls()
+ seq.unpack(vdict, self.conditions, data, noerror)
+ return vdict
+
+ def pack_wirestat(self, statobj):
+ """
+ Pack a stat object to appear as data returned by read()
+ on a directory. Essentially, we prefix the data with a size.
+ """
+ data = td.stat_seq.pack(statobj, self.conditions)
+ return td.wirestat_seq.pack({'size': len(data), 'data': data}, {})
+
+ def unpack_wirestat(self, bstring, offset, noerror=False):
+ """
+ Produce the next td.stat object from byte-string,
+ returning it and new offset.
+ """
+ statobj = td.stat()
+ d = { 'size': None }
+ newoff = td.wirestat_seq.unpack_from(d, self.conditions, bstring,
+ offset, noerror)
+ size = d['size']
+ if size is None: # implies noerror; newoff==offset+2
+ return statobj, newoff
+ # We now have size and data. If noerror, data might be
+ # too short, in which case we'll unpack a partial statobj.
+ # Or (with or without noeror), data might be too long, so
+ # that while len(data) == size, not all the data get used.
+ # That may be allowed by the protocol: it's not clear.
+ data = d['data']
+ used = td.stat_seq.unpack_from(statobj, self.conditions, data,
+ 0, noerror)
+ # if size != used ... then what?
+ return statobj, newoff
+
+ def pack_dirent(self, dirent):
+ """
+ Dirents (dot-L only) are easy to pack, but we provide
+ this function for symmetry. (Should we raise an error
+ if called on plain or dotu?)
+ """
+ return td.dirent_seq.pack(dirent, self.conditions)
+
+ def unpack_dirent(self, bstring, offset, noerror=False):
+ """
+ Produces the next td.dirent object from byte-string,
+ returning it and new offset.
+ """
+ deobj = td.dirent()
+ offset = td.dirent_seq.unpack_from(deobj, self.conditions, bstring,
+ offset, noerror)
+ return deobj, offset
+
+ def supports(self, fcall):
+ """
+ Return True if and only if this protocol supports the
+ given fcall.
+
+ >>> plain.supports(100)
+ True
+ >>> plain.supports('Tversion')
+ True
+ >>> plain.supports('Rlink')
+ False
+ """
+ fcall = fcall_names.get(fcall, None)
+ if fcall is None:
+ return False
+ cls = getattr(rrd, fcall)
+ return cls in self.pfods
+
+ def get_version(self, as_bytes=True):
+ "get Plan 9 protocol version, as string or (default) as bytes"
+ ret = self.auto_vars['version']
+ if as_bytes and not isinstance(ret, bytes):
+ ret = ret.encode('utf-8')
+ return ret
+
+ @property
+ def version(self):
+ "Plan 9 protocol version"
+ return self.get_version(as_bytes=False)
+
+DEBUG = False
+
+# This defines a special en/decoder named "s" using a magic
+# builtin. This and stat are the only variable-length
+# decoders, and this is the only recursively-variable-length
+# one (i.e., stat decoding is effectively fixed size once we
+# handle strings). So this magic avoids the need for recursion.
+#
+# Note that _string_ is, in effect, size[2] orig_var[size].
+_STRING_MAGIC = '_string_'
+SDesc = "typedef s: " + _STRING_MAGIC
+
+# This defines an en/decoder for type "qid",
+# which en/decodes 1 byte called type, 4 called version, and
+# 8 called path (for a total of 13 bytes).
+#
+# It also defines QTDIR, QTAPPEND, etc. (These are not used
+# for en/decode, or at least not yet.)
+QIDDesc = """\
+typedef qid: type[1] version[4] path[8]
+
+ #define QTDIR 0x80
+ #define QTAPPEND 0x40
+ #define QTEXCL 0x20
+ #define QTMOUNT 0x10
+ #define QTAUTH 0x08
+ #define QTTMP 0x04
+ #define QTSYMLINK 0x02
+ #define QTFILE 0x00
+"""
+
+# This defines a stat decoder, which has a 9p2000 standard front,
+# followed by an optional additional portion.
+#
+# The constants are named DMDIR etc.
+STATDesc = """
+typedef stat: type[2] dev[4] qid[qid] mode[4] atime[4] mtime[4] \
+length[8] name[s] uid[s] gid[s] muid[s] \
+{.u: extension[s] n_uid[4] n_gid[4] n_muid[4] }
+
+ #define DMDIR 0x80000000
+ #define DMAPPEND 0x40000000
+ #define DMMOUNT 0x10000000
+ #define DMAUTH 0x08000000
+ #define DMTMP 0x04000000
+ #define DMSYMLINK 0x02000000
+ /* 9P2000.u extensions */
+ #define DMDEVICE 0x00800000
+ #define DMNAMEDPIPE 0x00200000
+ #define DMSOCKET 0x00100000
+ #define DMSETUID 0x00080000
+ #define DMSETGID 0x00040000
+"""
+
+# This defines a wirestat decoder. A wirestat is a size and then
+# a (previously encoded, or future-decoded) stat.
+WirestatDesc = """
+typedef wirestat: size[2] data[size]
+"""
+
+# This defines a dirent decoder, which has a dot-L specific format.
+#
+# The dirent type fields are defined as DT_* (same as BSD and Linux).
+DirentDesc = """
+typedef dirent: qid[qid] offset[8] type[1] name[s]
+
+ #define DT_UNKNOWN 0
+ #define DT_FIFO 1
+ #define DT_CHR 2
+ #define DT_DIR 4
+ #define DT_BLK 6
+ #define DT_REG 8
+ #define DT_LNK 10
+ #define DT_SOCK 12
+ #define DT_WHT 14
+"""
+
+# N.B.: this is largely a slightly more rigidly formatted variant of
+# the contents of:
+# https://github.com/chaos/diod/blob/master/protocol.md
+#
+# Note that <name> = <value>: ... assigns names for the fcall
+# fcall (function call) table. Names without "= value" are
+# assumed to be the previous value +1 (and the two names are
+# also checked to make sure they are Tfoo,Rfoo).
+ProtocolDesc = """\
+Rlerror.L = 7: tag[2] ecode[4]
+ ecode is a numerical Linux errno
+
+Tstatfs.L = 8: tag[2] fid[4]
+Rstatfs.L: tag[2] type[4] bsize[4] blocks[8] bfree[8] bavail[8] \
+ files[8] ffree[8] fsid[8] namelen[4]
+ Rstatfs corresponds to Linux statfs structure:
+ struct statfs {
+ long f_type; /* type of file system */
+ long f_bsize; /* optimal transfer block size */
+ long f_blocks; /* total data blocks in file system */
+ long f_bfree; /* free blocks in fs */
+ long f_bavail; /* free blocks avail to non-superuser */
+ long f_files; /* total file nodes in file system */
+ long f_ffree; /* free file nodes in fs */
+ fsid_t f_fsid; /* file system id */
+ long f_namelen; /* maximum length of filenames */
+ };
+
+ This comes from nowhere obvious...
+ #define FSTYPE 0x01021997
+
+Tlopen.L = 12: tag[2] fid[4] flags[4]
+Rlopen.L: tag[2] qid[qid] iounit[4]
+ lopen prepares fid for file (or directory) I/O.
+
+ flags contains Linux open(2) flag bits, e.g., O_RDONLY, O_RDWR, O_WRONLY.
+
+ #define L_O_CREAT 000000100
+ #define L_O_EXCL 000000200
+ #define L_O_NOCTTY 000000400
+ #define L_O_TRUNC 000001000
+ #define L_O_APPEND 000002000
+ #define L_O_NONBLOCK 000004000
+ #define L_O_DSYNC 000010000
+ #define L_O_FASYNC 000020000
+ #define L_O_DIRECT 000040000
+ #define L_O_LARGEFILE 000100000
+ #define L_O_DIRECTORY 000200000
+ #define L_O_NOFOLLOW 000400000
+ #define L_O_NOATIME 001000000
+ #define L_O_CLOEXEC 002000000
+ #define L_O_SYNC 004000000
+ #define L_O_PATH 010000000
+ #define L_O_TMPFILE 020000000
+
+Tlcreate.L = 14: tag[2] fid[4] name[s] flags[4] mode[4] gid[4]
+Rlcreate.L: tag[2] qid[qid] iounit[4]
+ lcreate creates a regular file name in directory fid and prepares
+ it for I/O.
+
+ fid initially represents the parent directory of the new file.
+ After the call it represents the new file.
+
+ flags contains Linux open(2) flag bits (including O_CREAT).
+
+ mode contains Linux creat(2) mode (permissions) bits.
+
+ gid is the effective gid of the caller.
+
+Tsymlink.L = 16: tag[2] dfid[4] name[s] symtgt[s] gid[4]
+Rsymlink.L: tag[2] qid[qid]
+ symlink creates a symbolic link name in directory dfid. The
+ link will point to symtgt.
+
+ gid is the effective group id of the caller.
+
+ The qid for the new symbolic link is returned in the reply.
+
+Tmknod.L = 18: tag[2] dfid[4] name[s] mode[4] major[4] minor[4] gid[4]
+Rmknod.L: tag[2] qid[qid]
+ mknod creates a device node name in directory dfid with major
+ and minor numbers.
+
+ mode contains Linux mknod(2) mode bits. (Note that these
+ include the S_IFMT bits which may be S_IFBLK, S_IFCHR, or
+ S_IFSOCK.)
+
+ gid is the effective group id of the caller.
+
+ The qid for the new device node is returned in the reply.
+
+Trename.L = 20: tag[2] fid[4] dfid[4] name[s]
+Rrename.L: tag[2]
+ rename renames a file system object referenced by fid, to name
+ in the directory referenced by dfid.
+
+ This operation will eventually be replaced by renameat.
+
+Treadlink.L = 22: tag[2] fid[4]
+Rreadlink.L: tag[2] target[s]
+ readlink returns the contents of teh symbolic link referenced by fid.
+
+Tgetattr.L = 24: tag[2] fid[4] request_mask[8]
+Rgetattr.L: tag[2] valid[8] qid[qid] mode[4] uid[4] gid[4] nlink[8] \
+ rdev[8] size[8] blksize[8] blocks[8] \
+ atime_sec[8] atime_nsec[8] mtime_sec[8] mtime_nsec[8] \
+ ctime_sec[8] ctime_nsec[8] btime_sec[8] btime_nsec[8] \
+ gen[8] data_version[8]
+
+ getattr gets attributes of a file system object referenced by fid.
+ The response is intended to follow pretty closely the fields
+ returned by the stat(2) system call:
+
+ struct stat {
+ dev_t st_dev; /* ID of device containing file */
+ ino_t st_ino; /* inode number */
+ mode_t st_mode; /* protection */
+ nlink_t st_nlink; /* number of hard links */
+ uid_t st_uid; /* user ID of owner */
+ gid_t st_gid; /* group ID of owner */
+ dev_t st_rdev; /* device ID (if special file) */
+ off_t st_size; /* total size, in bytes */
+ blksize_t st_blksize; /* blocksize for file system I/O */
+ blkcnt_t st_blocks; /* number of 512B blocks allocated */
+ time_t st_atime; /* time of last access */
+ time_t st_mtime; /* time of last modification */
+ time_t st_ctime; /* time of last status change */
+ };
+
+ The differences are:
+
+ * st_dev is omitted
+ * st_ino is contained in the path component of qid
+ * times are nanosecond resolution
+ * btime, gen and data_version fields are reserved for future use
+
+ Not all fields are valid in every call. request_mask is a bitmask
+ indicating which fields are requested. valid is a bitmask
+ indicating which fields are valid in the response. The mask
+ values are as follows:
+
+ #define GETATTR_MODE 0x00000001
+ #define GETATTR_NLINK 0x00000002
+ #define GETATTR_UID 0x00000004
+ #define GETATTR_GID 0x00000008
+ #define GETATTR_RDEV 0x00000010
+ #define GETATTR_ATIME 0x00000020
+ #define GETATTR_MTIME 0x00000040
+ #define GETATTR_CTIME 0x00000080
+ #define GETATTR_INO 0x00000100
+ #define GETATTR_SIZE 0x00000200
+ #define GETATTR_BLOCKS 0x00000400
+
+ #define GETATTR_BTIME 0x00000800
+ #define GETATTR_GEN 0x00001000
+ #define GETATTR_DATA_VERSION 0x00002000
+
+ #define GETATTR_BASIC 0x000007ff /* Mask for fields up to BLOCKS */
+ #define GETATTR_ALL 0x00003fff /* Mask for All fields above */
+
+Tsetattr.L = 26: tag[2] fid[4] valid[4] mode[4] uid[4] gid[4] size[8] \
+ atime_sec[8] atime_nsec[8] mtime_sec[8] mtime_nsec[8]
+Rsetattr.L: tag[2]
+ setattr sets attributes of a file system object referenced by
+ fid. As with getattr, valid is a bitmask selecting which
+ fields to set, which can be any combination of:
+
+ mode - Linux chmod(2) mode bits.
+
+ uid, gid - New owner, group of the file as described in Linux chown(2).
+
+ size - New file size as handled by Linux truncate(2).
+
+ atime_sec, atime_nsec - Time of last file access.
+
+ mtime_sec, mtime_nsec - Time of last file modification.
+
+ The valid bits are defined as follows:
+
+ #define SETATTR_MODE 0x00000001
+ #define SETATTR_UID 0x00000002
+ #define SETATTR_GID 0x00000004
+ #define SETATTR_SIZE 0x00000008
+ #define SETATTR_ATIME 0x00000010
+ #define SETATTR_MTIME 0x00000020
+ #define SETATTR_CTIME 0x00000040
+ #define SETATTR_ATIME_SET 0x00000080
+ #define SETATTR_MTIME_SET 0x00000100
+
+ If a time bit is set without the corresponding SET bit, the
+ current system time on the server is used instead of the value
+ sent in the request.
+
+Txattrwalk.L = 30: tag[2] fid[4] newfid[4] name[s]
+Rxattrwalk.L: tag[2] size[8]
+ xattrwalk gets a newfid pointing to xattr name. This fid can
+ later be used to read the xattr value. If name is NULL newfid
+ can be used to get the list of extended attributes associated
+ with the file system object.
+
+Txattrcreate.L = 32: tag[2] fid[4] name[s] attr_size[8] flags[4]
+Rxattrcreate.L: tag[2]
+ xattrcreate gets a fid pointing to the xattr name. This fid
+ can later be used to set the xattr value.
+
+ flag is derived from set Linux setxattr. The manpage says
+
+ The flags parameter can be used to refine the semantics of
+ the operation. XATTR_CREATE specifies a pure create,
+ which fails if the named attribute exists already.
+ XATTR_REPLACE specifies a pure replace operation, which
+ fails if the named attribute does not already exist. By
+ default (no flags), the extended attribute will be created
+ if need be, or will simply replace the value if the
+ attribute exists.
+
+ The actual setxattr operation happens when the fid is clunked.
+ At that point the written byte count and the attr_size
+ specified in TXATTRCREATE should be same otherwise an error
+ will be returned.
+
+Treaddir.L = 40: tag[2] fid[4] offset[8] count[4]
+Rreaddir.L: tag[2] count[4] data[count]
+ readdir requests that the server return directory entries from
+ the directory represented by fid, previously opened with
+ lopen. offset is zero on the first call.
+
+ Directory entries are represented as variable-length records:
+ qid[qid] offset[8] type[1] name[s]
+ At most count bytes will be returned in data. If count is not
+ zero in the response, more data is available. On subsequent
+ calls, offset is the offset returned in the last directory
+ entry of the previous call.
+
+Tfsync.L = 50: tag[2] fid[4]
+Rfsync.L: tag[2]
+ fsync tells the server to flush any cached data associated
+ with fid, previously opened with lopen.
+
+Tlock.L = 52: tag[2] fid[4] type[1] flags[4] start[8] length[8] \
+ proc_id[4] client_id[s]
+Rlock.L: tag[2] status[1]
+ lock is used to acquire or release a POSIX record lock on fid
+ and has semantics similar to Linux fcntl(F_SETLK).
+
+ type has one of the values:
+
+ #define LOCK_TYPE_RDLCK 0
+ #define LOCK_TYPE_WRLCK 1
+ #define LOCK_TYPE_UNLCK 2
+
+ start, length, and proc_id correspond to the analagous fields
+ passed to Linux fcntl(F_SETLK):
+
+ struct flock {
+ short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
+ short l_whence;/* How to intrprt l_start: SEEK_SET,SEEK_CUR,SEEK_END */
+ off_t l_start; /* Starting offset for lock */
+ off_t l_len; /* Number of bytes to lock */
+ pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
+ };
+
+ flags bits are:
+
+ #define LOCK_SUCCESS 0
+ #define LOCK_BLOCKED 1
+ #define LOCK_ERROR 2
+ #define LOCK_GRACE 3
+
+ The Linux v9fs client implements the fcntl(F_SETLKW)
+ (blocking) lock request by calling lock with
+ LOCK_FLAGS_BLOCK set. If the response is LOCK_BLOCKED,
+ it retries the lock request in an interruptible loop until
+ status is no longer LOCK_BLOCKED.
+
+ The Linux v9fs client translates BSD advisory locks (flock) to
+ whole-file POSIX record locks. v9fs does not implement
+ mandatory locks and will return ENOLCK if use is attempted.
+
+ Because of POSIX record lock inheritance and upgrade
+ properties, pass-through servers must be implemented
+ carefully.
+
+Tgetlock.L = 54: tag[2] fid[4] type[1] start[8] length[8] proc_id[4] \
+ client_id[s]
+Rgetlock.L: tag[2] type[1] start[8] length[8] proc_id[4] client_id[s]
+ getlock tests for the existence of a POSIX record lock and has
+ semantics similar to Linux fcntl(F_GETLK).
+
+ As with lock, type has one of the values defined above, and
+ start, length, and proc_id correspond to the analagous fields
+ in struct flock passed to Linux fcntl(F_GETLK), and client_Id
+ is an additional mechanism for uniquely identifying the lock
+ requester and is set to the nodename by the Linux v9fs client.
+
+Tlink.L = 70: tag[2] dfid[4] fid[4] name[s]
+Rlink.L: tag[2]
+ link creates a hard link name in directory dfid. The link
+ target is referenced by fid.
+
+Tmkdir.L = 72: tag[2] dfid[4] name[s] mode[4] gid[4]
+Rmkdir.L: tag[2] qid[qid]
+ mkdir creates a new directory name in parent directory dfid.
+
+ mode contains Linux mkdir(2) mode bits.
+
+ gid is the effective group ID of the caller.
+
+ The qid of the new directory is returned in the response.
+
+Trenameat.L = 74: tag[2] olddirfid[4] oldname[s] newdirfid[4] newname[s]
+Rrenameat.L: tag[2]
+ Change the name of a file from oldname to newname, possible
+ moving it from old directory represented by olddirfid to new
+ directory represented by newdirfid.
+
+ If the server returns ENOTSUPP, the client should fall back to
+ the rename operation.
+
+Tunlinkat.L = 76: tag[2] dirfd[4] name[s] flags[4]
+Runlinkat.L: tag[2]
+ Unlink name from directory represented by dirfd. If the file
+ is represented by a fid, that fid is not clunked. If the
+ server returns ENOTSUPP, the client should fall back to the
+ remove operation.
+
+ There seems to be only one defined flag:
+
+ #define AT_REMOVEDIR 0x200
+
+Tversion = 100: tag[2] msize[4] version[s]:auto
+Rversion: tag[2] msize[4] version[s]
+
+ negotiate protocol version
+
+ version establishes the msize, which is the maximum message
+ size inclusive of the size value that can be handled by both
+ client and server.
+
+ It also establishes the protocol version. For 9P2000.L
+ version must be the string 9P2000.L.
+
+Tauth = 102: tag[2] afid[4] uname[s] aname[s] n_uname[4]
+Rauth: tag[2] aqid[qid]
+ auth initiates an authentication handshake for n_uname.
+ Rlerror is returned if authentication is not required. If
+ successful, afid is used to read/write the authentication
+ handshake (protocol does not specify what is read/written),
+ and afid is presented in the attach.
+
+Tattach = 104: tag[2] fid[4] afid[4] uname[s] aname[s] {.u: n_uname[4] }
+Rattach: tag[2] qid[qid]
+ attach introduces a new user to the server, and establishes
+ fid as the root for that user on the file tree selected by
+ aname.
+
+ afid can be NOFID (~0) or the fid from a previous auth
+ handshake. The afid can be clunked immediately after the
+ attach.
+
+ #define NOFID 0xffffffff
+
+ n_uname, if not set to NONUNAME (~0), is the uid of the
+ user and is used in preference to uname. Note that it appears
+ in both .u and .L (unlike most .u-specific features).
+
+ #define NONUNAME 0xffffffff
+
+ v9fs has several modes of access which determine how it uses
+ attach. In the default access=user, an initial attach is sent
+ for the user provided in the uname=name mount option, and for
+ each user that accesses the file system thereafter. For
+ access=, only the initial attach is sent for and all other
+ users are denied access by the client.
+
+Rerror = 107: tag[2] errstr[s] {.u: errnum[4] }
+
+Tflush = 108: tag[2] oldtag[2]
+Rflush: tag[2]
+ flush aborts an in-flight request referenced by oldtag, if any.
+
+Twalk = 110: tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s])
+Rwalk: tag[2] nwqid[2] nwqid*(wqid[qid])
+ walk is used to descend a directory represented by fid using
+ successive path elements provided in the wname array. If
+ succesful, newfid represents the new path.
+
+ fid can be cloned to newfid by calling walk with nwname set to
+ zero.
+
+ if nwname==0, fid need not represent a directory.
+
+Topen = 112: tag[2] fid[4] mode[1]
+Ropen: tag[2] qid[qid] iounit[4]
+ open prepares fid for file (or directory) I/O.
+
+ mode is:
+ #define OREAD 0 /* open for read */
+ #define OWRITE 1 /* open for write */
+ #define ORDWR 2 /* open for read and write */
+ #define OEXEC 3 /* open for execute */
+
+ #define OTRUNC 16 /* truncate (illegal if OEXEC) */
+ #define OCEXEC 32 /* close on exec (nonsensical) */
+ #define ORCLOSE 64 /* remove on close */
+ #define ODIRECT 128 /* direct access (.u extension?) */
+
+Tcreate = 114: tag[2] fid[4] name[s] perm[4] mode[1] {.u: extension[s] }
+Rcreate: tag[2] qid[qid] iounit[4]
+ create is similar to open; however, the incoming fid is the
+ diretory in which the file is to be created, and on success,
+ return, the fid refers to the then-created file.
+
+Tread = 116: tag[2] fid[4] offset[8] count[4]
+Rread: tag[2] count[4] data[count]
+ perform a read on the file represented by fid. Note that in
+ v9fs, a read(2) or write(2) system call for a chunk of the
+ file that won't fit in a single request is broken up into
+ multiple requests.
+
+ Under 9P2000.L, read cannot be used on directories. See readdir.
+
+Twrite = 118: tag[2] fid[4] offset[8] count[4] data[count]
+Rwrite: tag[2] count[4]
+ perform a write on the file represented by fid. Note that in
+ v9fs, a read(2) or write(2) system call for a chunk of the
+ file that won't fit in a single request is broken up into
+ multiple requests.
+
+ write cannot be used on directories.
+
+Tclunk = 120: tag[2] fid[4]
+Rclunk: tag[2]
+ clunk signifies that fid is no longer needed by the client.
+
+Tremove = 122: tag[2] fid[4]
+Rremove: tag[2]
+ remove removes the file system object represented by fid.
+
+ The fid is always clunked (even on error).
+
+Tstat = 124: tag[2] fid[4]
+Rstat: tag[2] size[2] data[size]
+
+Twstat = 126: tag[2] fid[4] size[2] data[size]
+Rwstat: tag[2]
+"""
+
+class _Token(object):
+ r"""
+ A scanned token.
+
+ Tokens have a type (tok.ttype) and value (tok.value). The value
+ is generally the token itself, although sometimes a prefix and/or
+ suffix has been removed (for 'label', 'word*', ':aux', and
+ '[type]' tokens). If prefix and/or suffix are removed, the full
+ original token is
+ in its .orig.
+
+ Tokens are:
+ - 'word', 'word*', or 'label':
+ '[.\w]+' followed by optional '*' or ':':
+
+ - 'aux': ':' followed by '\w+' (used for :auto annotation)
+
+ - 'type':
+ open bracket '[', followed by '\w+' or '\d+' (only one of these),
+ followed by close bracket ']'
+
+ - '(', ')', '{', '}': themeselves
+
+ Each token can have arbitrary leading white space (which is
+ discarded).
+
+ (Probably should return ':' as a char and handle it in parser,
+ but oh well.)
+ """
+ def __init__(self, ttype, value, orig=None):
+ self.ttype = ttype
+ self.value = value
+ self.orig = value if orig is None else orig
+ if self.ttype == 'type' and self.value.isdigit():
+ self.ival = int(self.value)
+ else:
+ self.ival = None
+ def __str__(self):
+ return self.orig
+
+_Token.tok_expr = re.compile(r'\s*([.\w]+(?:\*|:)?'
+ r'|:\w+'
+ r'|\[(?:\w+|\d+)\]'
+ r'|[(){}])')
+
+def _scan(string):
+ """
+ Tokenize a string.
+
+ Note: This raises a ValueError with the position of any unmatched
+ character in the string.
+ """
+ tlist = []
+
+ # make sure entire string is tokenized properly
+ pos = 0
+ for item in _Token.tok_expr.finditer(string):
+ span = item.span()
+ if span[0] != pos:
+ print('error: unmatched character(s) in input\n{0}\n{1}^'.format(
+ string, ' ' * pos))
+ raise ValueError('unmatched lexeme', pos)
+ pos = span[1]
+ tlist.append(item.group(1))
+ if pos != len(string):
+ print('error: unmatched character(s) in input\n{0}\n{1}^'.format(
+ string, ' ' * pos))
+ raise ValueError('unmatched lexeme', pos)
+
+ # classify each token, stripping decorations
+ result = []
+ for item in tlist:
+ if item in ('(', ')', '{', '}'):
+ tok = _Token(item, item)
+ elif item[0] == ':':
+ tok = _Token('aux', item[1:], item)
+ elif item.endswith(':'):
+ tok = _Token('label', item[0:-1], item)
+ elif item.endswith('*'):
+ tok = _Token('word*', item[0:-1], item)
+ elif item[0] == '[':
+ # integer or named type
+ if item[-1] != ']':
+ raise ValueError('internal error: "{0}" is not [...]'.format(
+ item))
+ tok = _Token('type', item[1:-1], item)
+ else:
+ tok = _Token('word', item)
+ result.append(tok)
+ return result
+
+def _debug_print_sequencer(seq):
+ """for debugging"""
+ print('sequencer is {0!r}'.format(seq), file=sys.stderr)
+ for i, enc in enumerate(seq):
+ print(' [{0:d}] = {1}'.format(i, enc), file=sys.stderr)
+
+def _parse_expr(seq, string, typedefs):
+ """
+ Parse "expression-ish" items, which is a list of:
+ name[type]
+ name*(subexpr) (a literal asterisk)
+ { label ... }
+
+ The "type" may be an integer or a second name. In the case
+ of a second name it must be something from <typedefs>.
+
+ The meaning of name[integer] is that we are going to encode
+ or decode a fixed-size field of <integer> bytes, using the
+ given name.
+
+ For name[name2], we can look up name2 in our typedefs table.
+ The only real typedefs's used here are "stat" and "s"; each
+ of these expands to a variable-size encode/decode. See the
+ special case below, though.
+
+ The meaning of name*(...) is: the earlier name will have been
+ defined by an earlier _parse_expr for this same line. That
+ earlier name provides a repeat-count.
+
+ Inside the parens we get a name[type] sub-expressino. This may
+ not recurse further, so we can use a pretty cheesy parser.
+
+ As a special case, given name[name2], we first check whether
+ name2 is an earlier name a la name*(...). Here the meaning
+ is much like name2*(name[1]), except that the result is a
+ simple byte string, rather than an array.
+
+ The meaning of "{ label ... " is that everything following up
+ to "}" is optional and used only with 9P2000.u and/or 9P2000.L.
+ Inside the {...} pair is the usual set of tokens, but again
+ {...} cannot recurse.
+
+ The parse fills in a Sequencer instance, and returns a list
+ of the parsed names.
+ """
+ names = []
+ cond = None
+
+ tokens = collections.deque(_scan(string))
+
+ def get_subscripted(tokens):
+ """
+ Allows name[integer] and name1[name2] only; returns
+ tuple after stripping off both tokens, or returns None
+ and does not strip tokens.
+ """
+ if len(tokens) == 0 or tokens[0].ttype != 'word':
+ return None
+ if len(tokens) > 1 and tokens[1].ttype == 'type':
+ word = tokens.popleft()
+ return word, tokens.popleft()
+ return None
+
+ def lookup(name, typeinfo, aux=None):
+ """
+ Convert cond (if not None) to its .value, so that instead
+ of (x, '.u') we get '.u'.
+
+ Convert typeinfo to an encdec. Typeinfo may be 1/2/4/8, or
+ one of our typedef names. If it's a typedef name it will
+ normally correspond to an EncDecTyped, but we have one special
+ case for string types, and another for using an earlier-defined
+ variable.
+ """
+ condval = None if cond is None else cond.value
+ if typeinfo.ival is None:
+ try:
+ cls, sub = typedefs[typeinfo.value]
+ except KeyError:
+ raise ValueError('unknown type name {0}'.format(typeinfo))
+ # the type name is typeinfo.value; the corresponding
+ # pfod class is cls; the *variable* name is name;
+ # and the sub-sequence is sub. But if cls is None
+ # then it's our string type.
+ if cls is None:
+ encdec = sequencer.EncDecSimple(name, _STRING_MAGIC, aux)
+ else:
+ encdec = sequencer.EncDecTyped(cls, name, sub, aux)
+ else:
+ if typeinfo.ival not in (1, 2, 4, 8):
+ raise ValueError('bad integer code in {0}'.format(typeinfo))
+ encdec = sequencer.EncDecSimple(name, typeinfo.ival, aux)
+ return condval, encdec
+
+ def emit_simple(name, typeinfo, aux=None):
+ """
+ Emit name[type]. We may be inside a conditional; if so
+ cond is not None.
+ """
+ condval, encdec = lookup(name, typeinfo, aux)
+ seq.append_encdec(condval, encdec)
+ names.append(name)
+
+ def emit_repeat(name1, name2, typeinfo):
+ """
+ Emit name1*(name2[type]).
+
+ Note that the conditional is buried in the sub-coder for
+ name2. It must be passed through anyway in case the sub-
+ coder is only partly conditional. If the sub-coder is
+ fully conditional, each sub-coding uses or produces no
+ bytes and hence the array itself is effectively conditional
+ as well (it becomes name1 * [None]).
+
+ We don't (currently) have any auxiliary data for arrays.
+ """
+ if name1 not in names:
+ raise ValueError('{0}*({1}[{2}]): '
+ '{0} undefined'.format(name1, name2,
+ typeinfo.value))
+ condval, encdec = lookup(name2, typeinfo)
+ encdec = sequencer.EncDecA(name1, name2, encdec)
+ seq.append_encdec(condval, encdec)
+ names.append(name2)
+
+ def emit_bytes_repeat(name1, name2):
+ """
+ Emit name1[name2], e.g., data[count].
+ """
+ condval = None if cond is None else cond.value
+ # Note that the two names are reversed when compared to
+ # count*(data[type]). The "sub-coder" is handled directly
+ # by EncDecA, hence is None.
+ #
+ # As a peculiar side effect, all bytes-repeats cause the
+ # count itself to become automatic (to have an aux of 'len').
+ encdec = sequencer.EncDecA(name2, name1, None, 'len')
+ seq.append_encdec(condval, encdec)
+ names.append(name1)
+
+ supported_conditions = ('.u')
+ while tokens:
+ token = tokens.popleft()
+ if token.ttype == 'label':
+ raise ValueError('misplaced label')
+ if token.ttype == 'aux':
+ raise ValueError('misplaced auxiliary')
+ if token.ttype == '{':
+ if cond is not None:
+ raise ValueError('nested "{"')
+ if len(tokens) == 0:
+ raise ValueError('unclosed "{"')
+ cond = tokens.popleft()
+ if cond.ttype != 'label':
+ raise ValueError('"{" not followed by cond label')
+ if cond.value not in supported_conditions:
+ raise ValueError('unsupported condition "{0}"'.format(
+ cond.value))
+ continue
+ if token.ttype == '}':
+ if cond is None:
+ raise ValueError('closing "}" w/o opening "{"')
+ cond = None
+ continue
+ if token.ttype == 'word*':
+ if len(tokens) == 0 or tokens[0].ttype != '(':
+ raise ValueError('{0} not followed by (...)'.format(token))
+ tokens.popleft()
+ repeat = get_subscripted(tokens)
+ if repeat is None:
+ raise ValueError('parse error after {0}('.format(token))
+ if len(tokens) == 0 or tokens[0].ttype != ')':
+ raise ValueError('missing ")" after {0}({1}{2}'.format(
+ token, repeat[0], repeat[1]))
+ tokens.popleft()
+ # N.B.: a repeat cannot have an auxiliary info (yet?).
+ emit_repeat(token.value, repeat[0].value, repeat[1])
+ continue
+ if token.ttype == 'word':
+ # Special case: _STRING_MAGIC turns into a string
+ # sequencer. This should be used with just one
+ # typedef (typedef s: _string_).
+ if token.value == _STRING_MAGIC:
+ names.append(_STRING_MAGIC) # XXX temporary
+ continue
+ if len(tokens) == 0 or tokens[0].ttype != 'type':
+ raise ValueError('parse error after {0}'.format(token))
+ type_or_size = tokens.popleft()
+ # Check for name[name2] where name2 is a word (not a
+ # number) that is in the names[] array.
+ if type_or_size.value in names:
+ # NB: this cannot have auxiliary info.
+ emit_bytes_repeat(token.value, type_or_size.value)
+ continue
+ if len(tokens) > 0 and tokens[0].ttype == 'aux':
+ aux = tokens.popleft()
+ if aux.value != 'auto':
+ raise ValueError('{0}{1}: only know "auto", not '
+ '{2}'.format(token, type_or_size,
+ aux.value))
+ emit_simple(token.value, type_or_size, aux.value)
+ else:
+ emit_simple(token.value, type_or_size)
+ continue
+ raise ValueError('"{0}" not valid here"'.format(token))
+
+ if cond is not None:
+ raise ValueError('unclosed "}"')
+
+ return names
+
+class _ProtoDefs(object):
+ def __init__(self):
+ # Scan our typedefs. This may execute '#define's as well.
+ self.typedefs = {}
+ self.defines = {}
+ typedef_re = re.compile(r'\s*typedef\s+(\w+)\s*:\s*(.*)')
+ self.parse_lines('SDesc', SDesc, typedef_re, self.handle_typedef)
+ self.parse_lines('QIDDesc', QIDDesc, typedef_re, self.handle_typedef)
+ self.parse_lines('STATDesc', STATDesc, typedef_re, self.handle_typedef)
+ self.parse_lines('WirestatDesc', WirestatDesc, typedef_re,
+ self.handle_typedef)
+ self.parse_lines('DirentDesc', DirentDesc, typedef_re,
+ self.handle_typedef)
+
+ # Scan protocol (the bulk of the work). This, too, may
+ # execute '#define's.
+ self.protocol = {}
+ proto_re = re.compile(r'(\*?\w+)(\.\w+)?\s*(?:=\s*(\d+))?\s*:\s*(.*)')
+ self.prev_proto_value = None
+ self.parse_lines('ProtocolDesc', ProtocolDesc,
+ proto_re, self.handle_proto_def)
+
+ self.setup_header()
+
+ # set these up for export()
+ self.plain = {}
+ self.dotu = {}
+ self.dotl = {}
+
+ def parse_lines(self, name, text, regexp, match_handler):
+ """
+ Parse a sequence of lines. Match each line using the
+ given regexp, or (first) as a #define line. Note that
+ indented lines are either #defines or are commentary!
+
+ If hnadling raises a ValueError, we complain and include
+ the appropriate line offset. Then we sys.exit(1) (!).
+ """
+ define = re.compile(r'\s*#define\s+(\w+)\s+([^/]*)'
+ r'(\s*/\*.*\*/)?\s*$')
+ for lineoff, line in enumerate(text.splitlines()):
+ try:
+ match = define.match(line)
+ if match:
+ self.handle_define(*match.groups())
+ continue
+ match = regexp.match(line)
+ if match:
+ match_handler(*match.groups())
+ continue
+ if len(line) and not line[0].isspace():
+ raise ValueError('unhandled line: {0}'.format(line))
+ except ValueError as err:
+ print('Internal error while parsing {0}:\n'
+ ' {1}\n'
+ '(at line offset +{2}, discounting \\-newline)\n'
+ 'The original line in question reads:\n'
+ '{3}'.format(name, err.args[0], lineoff, line),
+ file=sys.stderr)
+ sys.exit(1)
+
+ def handle_define(self, name, value, comment):
+ """
+ Handle #define match.
+
+ The regexp has three fields, matching the name, value,
+ and possibly-empty comment; these are our arguments.
+ """
+ # Obnoxious: int(,0) requires new 0o syntax in py3k;
+ # work around by trying twice, once with base 0, then again
+ # with explicit base 8 if the first attempt fails.
+ try:
+ value = int(value, 0)
+ except ValueError:
+ value = int(value, 8)
+ if DEBUG:
+ print('define: defining {0} as {1:x}'.format(name, value),
+ file=sys.stderr)
+ if name in self.defines:
+ raise ValueError('redefining {0}'.format(name))
+ self.defines[name] = (value, comment)
+
+ def handle_typedef(self, name, expr):
+ """
+ Handle typedef match.
+
+ The regexp has just two fields, the name and the expression
+ to parse (note that the expression must fit all on one line,
+ using backslach-newline if needed).
+
+ Typedefs may refer back to existing typedefs, so we pass
+ self.typedefs to _parse_expr().
+ """
+ seq = sequencer.Sequencer(name)
+ fields = _parse_expr(seq, expr, self.typedefs)
+ # Check for special string magic typedef. (The name
+ # probably should be just 's' but we won't check that
+ # here.)
+ if len(fields) == 1 and fields[0] == _STRING_MAGIC:
+ cls = None
+ else:
+ cls = pfod.pfod(name, fields)
+ if DEBUG:
+ print('typedef: {0} = {1!r}; '.format(name, fields),
+ end='', file=sys.stderr)
+ _debug_print_sequencer(seq)
+ if name in self.typedefs:
+ raise ValueError('redefining {0}'.format(name))
+ self.typedefs[name] = cls, seq
+
+ def handle_proto_def(self, name, proto_version, value, expr):
+ """
+ Handle protocol definition.
+
+ The regexp matched:
+ - The name of the protocol option such as Tversion,
+ Rversion, Rlerror, etc.
+ - The protocol version, if any (.u or .L).
+ - The value, if specified. If no value is specified
+ we use "the next value".
+ - The expression to parse.
+
+ As with typedefs, the expression must fit all on one
+ line.
+ """
+ if value:
+ value = int(value)
+ elif self.prev_proto_value is not None:
+ value = self.prev_proto_value + 1
+ else:
+ raise ValueError('{0}: missing protocol value'.format(name))
+ if value < 0 or value > 255:
+ raise ValueError('{0}: protocol value {1} out of '
+ 'range'.format(name, value))
+ self.prev_proto_value = value
+
+ seq = sequencer.Sequencer(name)
+ fields = _parse_expr(seq, expr, self.typedefs)
+ cls = pfod.pfod(name, fields)
+ if DEBUG:
+ print('proto: {0} = {1}; '.format(name, value),
+ end='', file=sys.stderr)
+ _debug_print_sequencer(seq)
+ if name in self.protocol:
+ raise ValueError('redefining {0}'.format(name))
+ self.protocol[name] = cls, value, proto_version, seq
+
+ def setup_header(self):
+ """
+ Handle header definition.
+
+ This is a bit gimmicky and uses some special cases,
+ because data is sized to dsize which is effectively
+ just size - 5. We can't express this in our mini language,
+ so we just hard-code the sequencer and pfod.
+
+ In addition, the unpacker never gets the original packet's
+ size field, only the fcall and the data.
+ """
+ self.header_pfod = pfod.pfod('Header', 'size dsize fcall data')
+
+ seq = sequencer.Sequencer('Header-pack')
+ # size: 4 bytes
+ seq.append_encdec(None, sequencer.EncDecSimple('size', 4, None))
+ # fcall: 1 byte
+ seq.append_encdec(None, sequencer.EncDecSimple('fcall', 1, None))
+ # data: string of length dsize
+ seq.append_encdec(None, sequencer.EncDecA('dsize', 'data', None))
+ if DEBUG:
+ print('Header-pack:', file=sys.stderr)
+ _debug_print_sequencer(seq)
+ self.header_pack_seq = seq
+
+ seq = sequencer.Sequencer('Header-unpack')
+ seq.append_encdec(None, sequencer.EncDecSimple('fcall', 1, None))
+ seq.append_encdec(None, sequencer.EncDecA('dsize', 'data', None))
+ if DEBUG:
+ print('Header-unpack:', file=sys.stderr)
+ _debug_print_sequencer(seq)
+ self.header_unpack_seq = seq
+
+ def export(self, mod):
+ """
+ Dump results of internal parsing process
+ into our module namespace.
+
+ Note that we do not export the 's' typedef, which
+ did not define a data structure.
+
+ Check for name collisions while we're at it.
+ """
+ namespace = type('td', (object,), {})
+
+ # Export the typedefs (qid, stat).
+ setattr(mod, 'td', namespace)
+ for key in self.typedefs:
+ cls = self.typedefs[key][0]
+ if cls is None:
+ continue
+ setattr(namespace, key, cls)
+
+ # Export two sequencers for en/decoding stat fields
+ # (needed for reading directories and doing Twstat).
+ setattr(namespace, 'stat_seq', self.typedefs['stat'][1])
+ setattr(namespace, 'wirestat_seq', self.typedefs['wirestat'][1])
+
+ # Export the similar dirent decoder.
+ setattr(namespace, 'dirent_seq', self.typedefs['dirent'][1])
+
+ # Export the #define values
+ for key, val in self.defines.items():
+ if hasattr(namespace, key):
+ print('{0!r} is both a #define and a typedef'.format(key))
+ raise AssertionError('bad internal names')
+ setattr(namespace, key, val[0])
+
+ # Export Tattach, Rattach, Twrite, Rversion, etc values.
+ # Set up fcall_names[] table to map from value back to name.
+ # We also map fcall names to themselves, so given either a
+ # name or a byte code we can find out whether it's a valid
+ # fcall.
+ for key, val in self.protocol.items():
+ if hasattr(namespace, key):
+ prev_def = '#define' if key in self.defines else 'typedef'
+ print('{0!r} is both a {1} and a protocol '
+ 'value'.format(key, prev_def))
+ raise AssertionError('bad internal names')
+ setattr(namespace, key, val[1])
+ fcall_names[key] = key
+ fcall_names[val[1]] = key
+
+ # Hook up PFOD's for each protocol object -- for
+ # Tversion/Rversion, Twrite/Rwrite, Tlopen/Rlopen, etc.
+ # They go in the rrd name-space, and also in dictionaries
+ # per-protocol here, with the lookup pointing to a _PackInfo
+ # for the corresponding sequencer.
+ #
+ # Note that each protocol PFOD is optionally annotated with
+ # its specific version. We know that .L > .u > plain; but
+ # all the "lesser" PFODs are available to all "greater"
+ # protocols at all times.
+ #
+ # (This is sort-of-wrong for Rerror vs Rlerror, but we
+ # don't bother to exclude Rerror from .L.)
+ #
+ # The PFODs themselves were already created, at parse time.
+ namespace = type('rrd', (object,), {})
+ setattr(mod, 'rrd', namespace)
+ for key, val in self.protocol.items():
+ cls = val[0]
+ proto_version = val[2]
+ seq = val[3]
+ packinfo = _PackInfo(seq)
+ if proto_version is None:
+ # all three protocols have it
+ self.plain[cls] = packinfo
+ self.dotu[cls] = packinfo
+ self.dotl[cls] = packinfo
+ elif proto_version == '.u':
+ # only .u and .L have it
+ self.dotu[cls] = packinfo
+ self.dotl[cls] = packinfo
+ elif proto_version == '.L':
+ # only .L has it
+ self.dotl[cls] = packinfo
+ else:
+ raise AssertionError('unknown protocol {1} for '
+ '{0}'.format(key, proto_version))
+ setattr(namespace, key, cls)
+
+_9p_data = _ProtoDefs()
+_9p_data.export(sys.modules[__name__])
+
+# Currently we look up by text-string, in lowercase.
+_9p_versions = {
+ '9p2000': _P9Proto({'version': '9P2000'},
+ {'.u': False},
+ _9p_data,
+ _9p_data.plain,
+ 0),
+ '9p2000.u': _P9Proto({'version': '9P2000.u'},
+ {'.u': True},
+ _9p_data,
+ _9p_data.dotu,
+ 1),
+ '9p2000.l': _P9Proto({'version': '9P2000.L'},
+ {'.u': True},
+ _9p_data,
+ _9p_data.dotl,
+ 2),
+}
+def p9_version(vers_string):
+ """
+ Return protocol implementation of given version. Raises
+ KeyError if the version is invalid. Note that the KeyError
+ will be on a string-ified, lower-cased version of the vers_string
+ argument, even if it comes in as a bytes instance in py3k.
+ """
+ if not isinstance(vers_string, str) and isinstance(vers_string, bytes):
+ vers_string = vers_string.decode('utf-8', 'surrogateescape')
+ return _9p_versions[vers_string.lower()]
+
+plain = p9_version('9p2000')
+dotu = p9_version('9p2000.u')
+dotl = p9_version('9p2000.L')
+
+def qid_type2name(qidtype):
+ """
+ Convert qid type field to printable string.
+
+ >>> qid_type2name(td.QTDIR)
+ 'dir'
+ >>> qid_type2name(td.QTAPPEND)
+ 'append-only'
+ >>> qid_type2name(0xff)
+ 'invalid(0xff)'
+ """
+ try:
+ # Is it ever OK to have multiple bits set,
+ # e.g., both QTAPPEND and QTEXCL?
+ return {
+ td.QTDIR: 'dir',
+ td.QTAPPEND: 'append-only',
+ td.QTEXCL: 'exclusive',
+ td.QTMOUNT: 'mount',
+ td.QTAUTH: 'auth',
+ td.QTTMP: 'tmp',
+ td.QTSYMLINK: 'symlink',
+ td.QTFILE: 'file',
+ }[qidtype]
+ except KeyError:
+ pass
+ return 'invalid({0:#x})'.format(qidtype)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()