mirror of
https://git.FreeBSD.org/src.git
synced 2024-11-24 07:40:52 +00:00
134e17798c
Approved by: trasz MFC after: 1 month Sponsored by: Conclusive Engineering (development), vStack.com (funding)
1999 lines
73 KiB
Python
1999 lines
73 KiB
Python
#! /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()
|