# Primitives for the 9P2000 protocol.
#
# See http://cm.bell-labs.com/sys/man/5/INDEX.html
# See http://swtch.com/plan9port/man/man9/
module Rumai
module IXP
# define constants for easier bit manipulation of 9P2000 field values
# uchar (1 byte), ushort (2 bytes), uint32 (4 bytes), uint64 (8 bytes)
4.times do |n|
bytes = 2 ** n
bits = 8 * bytes
limit = 2 ** bits
mask = limit - 1
const_set "BYTE#{bytes}_BITS", bits
const_set "BYTE#{bytes}_LIMIT", limit
const_set "BYTE#{bytes}_MASK", mask
end
##
# A 9P2000 byte stream.
#
module Stream
# uchar, ushort, uint32 (all of them little-endian)
PACKING_FLAGS = { 1 => 'C', 2 => 'v', 4 => 'V' }.freeze
##
# Unpacks the given number of bytes from this 9P2000 byte stream.
#
def read_9p num_bytes
read(num_bytes).unpack(PACKING_FLAGS[num_bytes])[0]
end
end
##
# A common container for exceptions concerning IXP.
#
class Error < StandardError
end
##
# A serializable 9P2000 data structure.
#
module Struct
attr_reader :fields
##
# Allows field values to be initialized via the constructor.
#
# @param field_values
# a mapping from field name to field value
#
def initialize field_values = {}
@fields = self.class.fields
@values = field_values
end
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p
@fields.map {|f| f.to_9p(@values) }.join
end
##
# Populates this object with information
# from the given 9P2000 byte stream.
#
def load_9p stream
@fields.each do |f|
f.load_9p stream, @values
end
end
##
# Provides a convenient DSL (for defining fields)
# to all objects which *include* this module.
#
def self.included target
class << target
##
# Returns a list of fields which compose this Struct.
#
def fields
@fields ||=
if superclass.respond_to? :fields
superclass.fields.dup
else
[]
end
end
##
# Defines a new field in this Struct.
#
# @param args
# arguments for {Field.new}
#
def field name, format = nil, *args
klass = Field.factory(format)
field = klass.new(name.to_sym, format, *args)
# register field as being part of this structure
fields << field
# provide accessor methods to field values
self.class_eval <<-EOS, __FILE__, __LINE__
def #{field.name}
@values[#{field.name.inspect}]
end
def #{field.name}= value
@values[#{field.name.inspect}] = value
end
EOS
field
end
##
# Creates a new instance of this class from the
# given 9P2000 byte stream and returns the instance.
#
def from_9p stream, msg_class = self
msg = msg_class.new
msg.load_9p(stream)
msg
end
end
end
##
# A field inside a Struct.
#
# A field's value is considered to be either:
# * array of format when counter && format.is_a? Class
# * raw byte string when counter && format.nil?
#
class Field
attr_reader :name, :format, :counter, :countee
##
# @param name
# unique (among all fields in a struct) name for the field
#
# @param format
# number of bytes, a class, or nil
#
# @param [Field] counter
# field which counts the length of this field's value
#
def initialize name, format = nil, counter = nil
@name = name
@format = format
@countee = nil
self.counter = counter
end
##
# Sets the counter for this field (implying that the
# length of this field is counted by the given field).
#
def counter= field
if @counter = field
extend CounteeField
@counter.countee = self
end
end
##
# Sets the countee for this field (implying that
# this field counts the length of the given field).
#
def countee= field
if @countee = field
extend CounterField
end
end
##
# Returns a Field class that best represents the given format.
#
def self.factory format
case format
when String then StringField
when Class then ClassField
when 8 then Integer8Field
else Field
end
end
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p field_values
value_to_9p field_values[@name]
end
##
# Populates this object with information
# taken from the given 9P2000 byte stream.
#
def load_9p stream, field_values
field_values[@name] = value_from_9p stream
end
private
##
# Converts the given value, according to the format
# of this field, into a string of 9P2000 bytes.
#
def value_to_9p value
value.to_i.to_9p @format.to_i
end
##
# Parses a value, according to the format of
# this field, from the given 9P2000 byte stream.
#
def value_from_9p stream
stream.read_9p @format.to_i
end
##
# Methods for a field that counts the length of another field.
#
module CounterField
def to_9p field_values
value = field_values[@countee.name]
count =
case value
when String then value.bytesize
else value.length
end
value_to_9p count
end
end
##
# Methods for a field whose length is counted by another field.
#
module CounteeField
def to_9p field_values
value = field_values[@name]
if @format
value.map {|v| value_to_9p v}.join
else
value.to_s # raw byte sequence
end
end
def load_9p stream, field_values
count = field_values[@counter.name].to_i
field_values[@name] =
if @format
Array.new(count) { value_from_9p stream }
else
stream.read(count) # raw byte sequence
end
end
end
end
##
# A field whose value knows how to convert itself to and from 9p.
#
class ClassField < Field
def value_to_9p value
value.to_9p
end
def value_from_9p stream
@format.from_9p stream
end
end
##
# A field whose value is a string.
#
class StringField < ClassField
def value_to_9p value
value.to_s.to_9p
end
end
##
# A field whose value is a 8-byte integer.
#
class Integer8Field < Field
def value_to_9p value
v = value.to_i
(BYTE4_MASK & v).to_9p(4) << # lower bytes
(BYTE4_MASK & (v >> BYTE4_BITS)).to_9p(4) # higher bytes
end
def value_from_9p stream
stream.read_9p(4) | (stream.read_9p(4) << BYTE4_BITS)
end
end
end
##
# Holds information about a file being accessed on a 9P2000 server.
#
# See http://cm.bell-labs.com/magic/man2html/5/intro
#
class Qid
include Struct
# type[1] version[4] path[8]
field :type , 1
field :version , 4
field :path , 8
##
# The following constant definitions originate from:
# http://swtch.com/usr/local/plan9/include/libc.h
#
QTDIR = 0x80 # type bit for directories
QTAPPEND = 0x40 # type bit for append only files
QTEXCL = 0x20 # type bit for exclusive use files
QTMOUNT = 0x10 # type bit for mounted channel
QTAUTH = 0x08 # type bit for authentication file
QTTMP = 0x04 # type bit for non-backed-up file
QTSYMLINK = 0x02 # type bit for symbolic link
QTFILE = 0x00 # type bits for plain file
end
##
# Holds information about a file on a 9P2000 server.
#
# See http://cm.bell-labs.com/magic/man2html/5/stat
#
class Stat
include Struct
field :size , 2
field :type , 2
field :dev , 4
field :qid , Qid
field :mode , 4
field :atime , Time
field :mtime , Time
field :length , 8
field :name , String
field :uid , String
field :gid , String
field :muid , String
##
# The following constant definitions originate from:
# http://swtch.com/usr/local/plan9/include/libc.h
#
DMDIR = 0x80000000 # mode bit for directories
DMAPPEND = 0x40000000 # mode bit for append only files
DMEXCL = 0x20000000 # mode bit for exclusive use files
DMMOUNT = 0x10000000 # mode bit for mounted channel
DMAUTH = 0x08000000 # mode bit for authentication file
DMTMP = 0x04000000 # mode bit for non-backed-up file
DMSYMLINK = 0x02000000 # mode bit for symbolic link (Unix, 9P2000.u)
DMDEVICE = 0x00800000 # mode bit for device file (Unix, 9P2000.u)
DMNAMEDPIPE = 0x00200000 # mode bit for named pipe (Unix, 9P2000.u)
DMSOCKET = 0x00100000 # mode bit for socket (Unix, 9P2000.u)
DMSETUID = 0x00080000 # mode bit for setuid (Unix, 9P2000.u)
DMSETGID = 0x00040000 # mode bit for setgid (Unix, 9P2000.u)
DMREAD = 0x4 # mode bit for read permission
DMWRITE = 0x2 # mode bit for write permission
DMEXEC = 0x1 # mode bit for execute permission
##
# Tests if this file is a directory.
#
def directory?
mode & DMDIR > 0
end
end
##
# Fcall is the basic unit of communication in the 9P2000 protocol.
# It is analogous to a "packet" in the Internetwork Protocol (IP).
#
# See http://cm.bell-labs.com/magic/man2html/2/fcall
#
class Fcall
include Struct
# The first two fields are disabled because they are automatically
# calculated by the Fcall#to_9p and Fcall::from_9p methods below:
#
# field :size , 4 # disabled
# field :type , 1 # disabled
#
field :tag , 2
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p
data = type.to_9p(1) << super
size = (data.bytesize + 4).to_9p(4)
size << data
end
class << self
alias __from_9p__ from_9p
undef from_9p
##
# Creates a new instance of this class from the
# given 9P2000 byte stream and returns the instance.
#
def from_9p stream
size = stream.read_9p(4)
type = stream.read_9p(1)
unless fcall = TYPE_TO_CLASS[type]
raise Error, "illegal fcall type: #{type}"
end
__from_9p__ stream, fcall
end
end
NOTAG = BYTE2_MASK # (ushort)
NOFID = BYTE4_MASK # (uint32)
end
# size[4] Tversion tag[2] msize[4] version[s]
class Tversion < Fcall
field :msize , 4
field :version , String
VERSION = '9P2000'.freeze
MSIZE = 8192 # magic number defined in Plan9
# for [TR]version and [TR]read
end
# size[4] Rversion tag[2] msize[4] version[s]
class Rversion < Fcall
field :msize , 4
field :version , String
end
# size[4] Tauth tag[2] afid[4] uname[s] aname[s]
class Tauth < Fcall
field :afid , 4
field :uname , String
field :aname , String
end
# size[4] Rauth tag[2] aqid[13]
class Rauth < Fcall
field :aqid , Qid
end
# size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s]
class Tattach < Fcall
field :fid , 4
field :afid , 4
field :uname , String
field :aname , String
end
# size[4] Rattach tag[2] qid[13]
class Rattach < Fcall
field :qid , Qid
end
# illegal
class Terror < Fcall
def to_9p
raise Error, 'the Terror fcall cannot be transmitted'
end
end
# size[4] Rerror tag[2] ename[s]
class Rerror < Fcall
field :ename , String
end
# size[4] Tflush tag[2] oldtag[2]
class Tflush < Fcall
field :oldtag , 2
end
# size[4] Rflush tag[2]
class Rflush < Fcall
end
# size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s])
class Twalk < Fcall
field :fid , 4
field :newfid , 4
c = field :nwname , 2
field :wname , String , c
end
# size[4] Rwalk tag[2] nwqid[2] nwqid*(wqid[13])
class Rwalk < Fcall
c = field :nwqid , 2
field :wqid , Qid , c
end
# size[4] Topen tag[2] fid[4] mode[1]
class Topen < Fcall
field :fid , 4
field :mode , 1
##
# The following constant definitions originate from:
# http://swtch.com/usr/local/plan9/include/libc.h
#
OREAD = 0 # open for read
OWRITE = 1 # write
ORDWR = 2 # read and write
OEXEC = 3 # execute, == read but check execute permission
OTRUNC = 16 # or'ed in (except for exec), truncate file first
OCEXEC = 32 # or'ed in, close on exec
ORCLOSE = 64 # or'ed in, remove on close
ODIRECT = 128 # or'ed in, direct access
ONONBLOCK = 256 # or'ed in, non-blocking call
OEXCL = 0x1000 # or'ed in, exclusive use (create only)
OLOCK = 0x2000 # or'ed in, lock after opening
OAPPEND = 0x4000 # or'ed in, append only
end
# size[4] Ropen tag[2] qid[13] iounit[4]
class Ropen < Fcall
field :qid , Qid
field :iounit , 4
end
# size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1]
class Tcreate < Fcall
field :fid , 4
field :name , String
field :perm , 4
field :mode , 1
end
# size[4] Rcreate tag[2] qid[13] iounit[4]
class Rcreate < Fcall
field :qid , Qid
field :iounit , 4
end
# size[4] Tread tag[2] fid[4] offset[8] count[4]
class Tread < Fcall
field :fid , 4
field :offset , 8
field :count , 4
end
# size[4] Rread tag[2] count[4] data[count]
class Rread < Fcall
c = field :count , 4
field :data , nil , c
end
# size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count]
class Twrite < Fcall
field :fid , 4
field :offset , 8
c = field :count , 4
field :data , nil , c
end
# size[4] Rwrite tag[2] count[4]
class Rwrite < Fcall
field :count , 4
end
# size[4] Tclunk tag[2] fid[4]
class Tclunk < Fcall
field :fid , 4
end
# size[4] Rclunk tag[2]
class Rclunk < Fcall
end
# size[4] Tremove tag[2] fid[4]
class Tremove < Fcall
field :fid , 4
end
# size[4] Rremove tag[2]
class Rremove < Fcall
end
# size[4] Tstat tag[2] fid[4]
class Tstat < Fcall
field :fid , 4
end
# size[4] Rstat tag[2] stat[n]
class Rstat < Fcall
field :nstat , 2
field :stat , Stat
end
# size[4] Twstat tag[2] fid[4] stat[n]
class Twstat < Fcall
field :fid , 4
field :nstat , 2
field :stat , Stat
end
# size[4] Rwstat tag[2]
class Rwstat < Fcall
end
##
# A remote function call (fcall).
#
class Fcall
CLASS_TO_TYPE = {
Tversion => 100,
Rversion => 101,
Tauth => 102,
Rauth => 103,
Tattach => 104,
Rattach => 105,
Terror => 106,
Rerror => 107,
Tflush => 108,
Rflush => 109,
Twalk => 110,
Rwalk => 111,
Topen => 112,
Ropen => 113,
Tcreate => 114,
Rcreate => 115,
Tread => 116,
Rread => 117,
Twrite => 118,
Rwrite => 119,
Tclunk => 120,
Rclunk => 121,
Tremove => 122,
Rremove => 123,
Tstat => 124,
Rstat => 125,
Twstat => 126,
Rwstat => 127,
}.freeze
TYPE_TO_CLASS = CLASS_TO_TYPE.invert.freeze
##
# Returns the value of the 'type' field for this fcall.
#
def self.type
CLASS_TO_TYPE[self]
end
##
# Returns the value of the 'type' field for this fcall.
#
def type
self.class.type
end
end
end
end
class Integer
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p num_bytes
[self].pack Rumai::IXP::Stream::PACKING_FLAGS[num_bytes]
end
end
# count[2] s[count]
class String
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p
count = [bytesize, Rumai::IXP::BYTE2_MASK].min
count.to_9p(2) << byteslice(0, count)
end
##
# Creates a new instance of this class from the
# given 9P2000 byte stream and returns the instance.
#
def self.from_9p stream
stream.read stream.read_9p(2)
end
unless method_defined? :byteslice
##
# Does the same thing as String#slice but
# operates on bytes instead of characters.
#
def byteslice(*args)
unpack('C*').slice(*args).pack('C*')
end
end
end
class Time
##
# Transforms this object into a string of 9P2000 bytes.
#
def to_9p
to_i.to_9p(4)
end
##
# Creates a new instance of this class from the
# given 9P2000 byte stream and returns the instance.
#
def self.from_9p stream
at stream.read_9p(4)
end
end
class IO
include Rumai::IXP::Stream
end
require 'stringio'
class StringIO
include Rumai::IXP::Stream
end