commit fb37ce2aa1f717002944b5aec393b3318e2e7261 from: Stephen J Day date: Fri Oct 30 01:03:10 2015 UTC fs/p9p/new: refactor channel framing After a few connection bugs and framing issues, the transport has been refactored into a channel that manages raw message sending. More importantly, this frees up pre-session protocol version negotiation. To support this, the encoder and decoder have been folded into a Codec interface. The encoding support still needs work, but this has made the server and client code much simpler to understand. Signed-off-by: Stephen J Day commit - 97423e8b7909c86abde24bd416798b1172759758 commit + fb37ce2aa1f717002944b5aec393b3318e2e7261 blob - 49f36bf99fe75ae3bdfb40e35d93b4c68c440fac blob + 30c2205d3c5f55a717ead6a71e823d1445b868fa --- client.go +++ client.go @@ -10,6 +10,8 @@ import ( ) type client struct { + version string + msize uint32 ctx context.Context transport roundTripper } @@ -18,14 +20,41 @@ type client struct { // a context for out of bad messages, such as flushes, that may be sent by the // session. The session can effectively shutdown with this context. func NewSession(ctx context.Context, conn net.Conn) (Session, error) { + const msize = 64 << 10 + const vers = "9P2000" + + ch := newChannel(conn, codec9p{}, msize) // sets msize, effectively. + + // negotiate the protocol version + smsize, svers, err := version(ctx, ch, msize, vers) + if err != nil { + return nil, err + } + + if svers != vers { + // TODO(stevvooe): A stubborn client indeed! + return nil, fmt.Errorf("unsupported server version: %v", vers) + } + + if smsize > msize { + // upgrade msize if server differs. + ch.setmsize(int(smsize)) + } + return &client{ + version: vers, + msize: msize, ctx: ctx, - transport: newTransport(ctx, conn), + transport: newTransport(ctx, ch), }, nil } var _ Session = &client{} +func (c *client) Version() (uint32, string) { + return c.msize, c.version +} + func (c *client) Auth(ctx context.Context, afid Fid, uname, aname string) (Qid, error) { panic("not implemented") } @@ -169,10 +198,10 @@ func (c *client) Open(ctx context.Context, fid Fid, mo return Qid{}, 0, fmt.Errorf("invalid rpc response for open message: %v", resp) } - return respmsg.Qid, respmsg.Msize, nil + return respmsg.Qid, respmsg.IOUnit, nil } -func (c *client) Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, error) { +func (c *client) Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, uint32, error) { panic("not implemented") } @@ -184,16 +213,27 @@ func (c *client) WStat(context.Context, Fid, Dir) erro panic("not implemented") } -func (c *client) Version(ctx context.Context, msize uint32, version string) (uint32, string, error) { - msg := MessageTversion{ +func (c *client) flush(ctx context.Context, tag Tag) error { + // TODO(stevvooe): We need to fire and forget flush messages when a call + // context gets cancelled. + + panic("not implemented") +} + +// version negiotiates the protocol version using channel, blocking until a +// response is received. This should be called before starting the transport. +func version(ctx context.Context, ch *channel, msize uint32, version string) (uint32, string, error) { + req := newFcall(MessageTversion{ MSize: uint32(msize), Version: version, + }) + + if err := ch.writeFcall(ctx, req); err != nil { + return 0, "", err } - fcall := newFcall(msg) - - resp, err := c.transport.send(ctx, fcall) - if err != nil { + resp := new(Fcall) + if err := ch.readFcall(ctx, resp); err != nil { return 0, "", err } @@ -202,15 +242,5 @@ func (c *client) Version(ctx context.Context, msize ui return 0, "", fmt.Errorf("invalid rpc response for version message: %v", resp) } - // TODO(stevvooe): Use this response to set iounit and version on this - // client instance. - return mv.MSize, mv.Version, nil } - -func (c *client) flush(ctx context.Context, tag Tag) error { - // TODO(stevvooe): We need to fire and forget flush messages when a call - // context gets cancelled. - - panic("not implemented") -} blob - /dev/null blob + 9de03f0d26e4cad75685fe0929ef3cc725232ec9 (mode 644) --- /dev/null +++ channel.go @@ -0,0 +1,173 @@ +package p9pnew + +import ( + "bufio" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "log" + "net" + + "golang.org/x/net/context" +) + +// channel provides bidirectional protocol framing for 9p over net.Conn. +// Operations are not thread-safe but reads and writes may be carried out +// concurrently, supporting separate read and write loops. +type channel struct { + conn net.Conn + codec Codec + brd *bufio.Reader + bwr *bufio.Writer + closed chan struct{} + rdbuf []byte + wrbuf []byte +} + +func newChannel(conn net.Conn, codec Codec, msize int) *channel { + return &channel{ + conn: conn, + codec: codec, + brd: bufio.NewReaderSize(conn, msize), // msize may not be optimal buffer size + bwr: bufio.NewWriterSize(conn, msize), + closed: make(chan struct{}), + rdbuf: make([]byte, msize), + wrbuf: make([]byte, msize), + } +} + +// setmsize resizes the buffers for use with a separate msize. This call must +// be protected by a mutex or made before passing to other goroutines. +func (ch *channel) setmsize(msize int) { + // NOTE(stevvooe): We cannot safely resize the buffered reader and writer. + // Proceed assuming that original size is sufficient. + + if msize < len(ch.rdbuf) { + // just change the cap + ch.rdbuf = ch.rdbuf[:msize] + ch.wrbuf = ch.wrbuf[:msize] + return + } + + ch.rdbuf = make([]byte, msize) + ch.wrbuf = make([]byte, msize) +} + +// ReadFcall reads the next message from the channel into fcall. +func (ch *channel) readFcall(ctx context.Context, fcall *Fcall) error { + select { + case <-ch.closed: + return ErrClosed + default: + } + log.Println("channel: readFcall", fcall) + + if deadline, ok := ctx.Deadline(); ok { + if err := ch.conn.SetReadDeadline(deadline); err != nil { + log.Printf("transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err) + } + } + + n, err := readmsg(ch.brd, ch.rdbuf) + if err != nil { + // TODO(stevvooe): There may be more we can do here to detect partial + // reads. For now, we just propagate the error untouched. + return err + } + + if n > len(ch.rdbuf) { + // TODO(stevvooe): Make this error detectable and respond with error + // message. + return fmt.Errorf("message large than buffer:", n) + } + + return ch.codec.Unmarshal(ch.rdbuf[:n], fcall) +} + +func (ch *channel) writeFcall(ctx context.Context, fcall *Fcall) error { + select { + case <-ch.closed: + return ErrClosed + default: + } + log.Println("channel: writeFcall", fcall) + + if deadline, ok := ctx.Deadline(); ok { + if err := ch.conn.SetWriteDeadline(deadline); err != nil { + log.Printf("transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err) + } + } + + n, err := ch.codec.Marshal(ch.wrbuf, fcall) + if err != nil { + return err + } + + p := ch.wrbuf[:n] + + if err := sendmsg(ch.bwr, p); err != nil { + return err + } + + return ch.bwr.Flush() +} + +// readmsg reads a 9p message into p from rd, ensuring that all bytes are +// consumed from the size header. If the size header indicates the message is +// larger than p, the entire message will be discarded, leaving a truncated +// portion in p. Any error should be treated as a framing error unless n is +// zero. The caller must check that n is less than or equal to len(p) to +// ensure that a valid message has been read. +func readmsg(rd io.Reader, p []byte) (n int, err error) { + var msize uint32 + + if err := binary.Read(rd, binary.LittleEndian, &msize); err != nil { + return 0, err + } + + n += binary.Size(msize) + mbody := int(msize) - 4 + + if mbody < len(p) { + p = p[:mbody] + } + + np, err := io.ReadFull(rd, p) + if err != nil { + return np + n, err + } + n += np + + if mbody > len(p) { + // message has been read up to len(p) but we must consume the entire + // message. This is an error condition but is non-fatal if we can + // consume msize bytes. + nn, err := io.CopyN(ioutil.Discard, rd, int64(mbody-len(p))) + n += int(nn) + if err != nil { + return n, err + } + } + + return n, nil +} + +// sendmsg writes a message of len(p) to wr with a 9p size header. All errors +// should be considered terminal. +func sendmsg(wr io.Writer, p []byte) error { + size := uint32(len(p) + 4) // message size plus 4-bytes for size. + if err := binary.Write(wr, binary.LittleEndian, size); err != nil { + return nil + } + + // This assume partial writes to wr aren't possible. Not sure if this + // valid. Matters during timeout retries. + if n, err := wr.Write(p); err != nil { + return err + } else if n < len(p) { + return io.ErrShortWrite + } + + return nil +} blob - d003a08067db2beb05f4e2124a29c59f1d0a43b8 blob + 1db7807e7ff9d6b0336ace83a50ede2453e8270a --- cmd/9pr/main.go +++ cmd/9pr/main.go @@ -74,6 +74,12 @@ func main() { } commander.readline = rl + msize, version := commander.session.Version() + if err != nil { + log.Fatalln(err) + } + log.Println("9p version", version, msize) + log.Println("attach root") // attach root commander.nextfid = 1 @@ -123,7 +129,7 @@ func main() { } } - ctx, _ = context.WithTimeout(commander.ctx, time.Second) + ctx, _ = context.WithTimeout(commander.ctx, 5*time.Second) if err := cmd(ctx, args[1:]...); err != nil { log.Printf("👹 %s: %v", name, err) } @@ -170,11 +176,12 @@ func (c *fsCommander) cmdls(ctx context.Context, args } defer c.session.Clunk(ctx, targetfid) - if _, _, err := c.session.Open(ctx, targetfid, p9pnew.OREAD); err != nil { + _, msize, err := c.session.Open(ctx, targetfid, p9pnew.OREAD) + if err != nil { return err } - p := make([]byte, 4<<20) + p := make([]byte, msize) n, err := c.session.Read(ctx, targetfid, p, 0) if err != nil { @@ -469,7 +476,7 @@ func (s *simpleSession) Open(ctx context.Context, fid return fi.dir.Qid, 4 << 20, nil } -func (s *simpleSession) Create(ctx context.Context, parent p9pnew.Fid, name string, perm uint32, mode uint32) (p9pnew.Qid, error) { +func (s *simpleSession) Create(ctx context.Context, parent p9pnew.Fid, name string, perm uint32, mode uint32) (p9pnew.Qid, uint32, error) { panic("not implemented") } @@ -481,9 +488,6 @@ func (s *simpleSession) WStat(context.Context, p9pnew. panic("not implemented") } -// TODO(stevvooe): The version message affects a lot of protocol behavior. -// Consider hiding it behind the implementation, letting the version get -// negotiated. The API user should still be able to query it. -func (s *simpleSession) Version(ctx context.Context, msize uint32, version string) (uint32, string, error) { - return 4096, "9P2000", nil +func (s *simpleSession) Version() (uint32, string) { + return 64 << 10, "9P2000" } blob - 200e30422567e89a0b07142ec27544ad2d5364dc blob + 10d52740dedc0b060356f11de11880aee4f26b6b --- encoding.go +++ encoding.go @@ -1,6 +1,7 @@ package p9pnew import ( + "bytes" "encoding/binary" "fmt" "io" @@ -23,6 +24,35 @@ func EncodeDir(wr io.Writer, d *Dir) error { func DecodeDir(rd io.Reader, d *Dir) error { dec := &decoder{rd} return dec.decode(d) +} + +type Codec interface { + Unmarshal(data []byte, v interface{}) error + Marshal(data []byte, v interface{}) (n int, err error) +} + +type codec9p struct{} + +func (c codec9p) Unmarshal(data []byte, v interface{}) error { + dec := &decoder{bytes.NewReader(data)} + return dec.decode(v) +} + +func (c codec9p) Marshal(data []byte, v interface{}) (n int, err error) { + n = int(size9p(v)) + + buf := bytes.NewBuffer(data[:0]) + enc := &encoder{buf} + + if err := enc.encode(v); err != nil { + return buf.Len(), nil + } + + if len(data) < buf.Len() { + return len(data), io.ErrShortBuffer + } + + return buf.Len(), nil } // NOTE(stevvooe): This file covers 9p encoding and decoding (despite just @@ -118,7 +148,7 @@ func (e *encoder) encode(vs ...interface{}) error { return err } case *Fcall: - if err := e.encode(size9p(v), v.Type, v.Tag, v.Message); err != nil { + if err := e.encode(v.Type, v.Tag, v.Message); err != nil { return err } default: @@ -189,17 +219,20 @@ func (d *decoder) decode(vs ...interface{}) error { return err } case *Fcall: - var size uint32 - if err := d.decode(&size, &v.Type, &v.Tag); err != nil { + if err := d.decode(&v.Type, &v.Tag); err != nil { return err } var err error v.Message, err = newMessage(v.Type) if err != nil { + log.Printf("unknown message type %#v", v.Type) return err } + // take the address of v.Message from the struct and encode into + // that. + if err := d.decode(v.Message); err != nil { return err } @@ -300,7 +333,7 @@ func size9p(vs ...interface{}) uint32 { s += size9p(elements...) case Fcall: - s += size9p(uint32(0), v.Type, v.Tag, v.Message) + s += size9p(v.Type, v.Tag, v.Message) case *Fcall: s += size9p(*v) default: @@ -330,11 +363,6 @@ func fields9p(v interface{}) ([]interface{}, error) { return nil, fmt.Errorf("can't interface: %v", f) } - if !f.CanSet() { - panic("asdf") - return nil, fmt.Errorf("cannot set %v", f) - } - if f.CanAddr() { f = f.Addr() } blob - 6e11cca7e4518ea4c56ec3c5b7877dce8cd47fb6 blob + 31ff66b1312bc7cae6d6b1ec9f8cc17ece4b5744 --- encoding_test.go +++ encoding_test.go @@ -40,7 +40,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x13, 0x0, 0x0, 0x0, 0x64, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0, 0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54}, }, @@ -55,7 +54,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x13, 0x0, 0x0, 0x0, 0x65, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0, 0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54}, }, @@ -71,7 +69,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x1a, 0x0, 0x0, 0x0, 0x6e, 0x22, 0x16, 0xf2, 0x3, 0x0, 0x0, 0xf3, 0x3, 0x0, 0x0, 0x3, 0x0, // len(wnames) 0x1, 0x0, 0x61, // "a" @@ -97,7 +94,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x23, 0x0, 0x0, 0x0, 0x6f, 0xb4, 0x15, 0x2, 0x0, 0x80, 0x68, 0x2b, 0x0, 0x0, 0x57, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, @@ -113,7 +109,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x1b, 0x0, 0x0, 0x0, 0x75, 0xb4, 0x15, 0x12, 0x0, 0x61, 0x20, 0x6c, 0x6f, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x64, 0x61, 0x74, 0x61}, @@ -144,7 +139,6 @@ func TestEncodeDecode(t *testing.T) { }, }, marshaled: []byte{ - 0x47, 0x0, 0x0, 0x0, 0x7d, 0xb4, 0x15, // 0x40, 0x0, // TODO(stevvooe): Include Dir size. Not straightforward. 0xff, 0xff, // type blob - 9e03f7e1d58923ae84789c8cf90fab13bc7c9b6e blob + 5689477107327c6ad5e089a80ad44d7096a206a3 --- fcall.go +++ fcall.go @@ -109,6 +109,7 @@ type Fcall struct { func newFcall(msg Message) *Fcall { return &Fcall{ Type: msg.Type(), + Tag: NOTAG, Message: msg, } } @@ -246,8 +247,8 @@ type MessageTopen struct { } type MessageRopen struct { - Qid Qid - Msize uint32 + Qid Qid + IOUnit uint32 } type MessageTcreate struct { blob - db1076f7a47a1f6a1475b98a52947c464adaccc5 blob + 325347028d2ac5a31e07eb8ed43af174b5a3beb1 --- server.go +++ server.go @@ -51,6 +51,11 @@ func (s *server) run() { log.Println("server:", "wait") fcall := new(Fcall) + + // BUG(stevvooe): The decoder is not reliably consuming all of the + // bytes of the wire. Needs to be setup to consume all bytes indicated + // in the size portion. Should use msize from the version negotiation. + if err := dec.decode(fcall); err != nil { log.Println("server decoding fcall:", err) continue @@ -136,14 +141,14 @@ func (s *server) handle(ctx context.Context, req *Fcal return nil, fmt.Errorf("bad message: %v message=%v", req, req.Message) } - qid, msize, err := s.session.Open(ctx, reqmsg.Fid, reqmsg.Mode) + qid, iounit, err := s.session.Open(ctx, reqmsg.Fid, reqmsg.Mode) if err != nil { return nil, err } resp = newFcall(&MessageRopen{ - Qid: qid, - Msize: msize, + Qid: qid, + IOUnit: iounit, }) case Tread: reqmsg, ok := req.Message.(*MessageTread) blob - 37779b341b219f43c948e22ba0287ac579e16625 blob + 91e26ec436170573317f400b7da56a6a2971d62b --- session.go +++ session.go @@ -26,14 +26,14 @@ type Session interface { Read(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) Write(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) Open(ctx context.Context, fid Fid, mode uint8) (Qid, uint32, error) - Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, error) + Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, uint32, error) Stat(context.Context, Fid) (Dir, error) WStat(context.Context, Fid, Dir) error - // TODO(stevvooe): The version message affects a lot of protocol behavior. - // Consider hiding it behind the implementation, letting the version get - // negotiated. The API user should still be able to query it. - Version(ctx context.Context, msize uint32, version string) (uint32, string, error) + // Version returns the supported version and msize of the session. This + // can be affected by negotiating or the level of support provided by the + // session implementation. + Version() (msize uint32, version string) } func Dial(ctx context.Context, addr string) (Session, error) { blob - 7c956dd15cc2364349bdac627b8a8af380784de7 blob + 4b25bc02d50aef020b58e0cb0ca06ea43cedede0 --- transport.go +++ transport.go @@ -1,11 +1,9 @@ package p9pnew import ( - "bufio" "fmt" "log" "net" - "time" "golang.org/x/net/context" ) @@ -20,17 +18,17 @@ type roundTripper interface { type transport struct { ctx context.Context - conn net.Conn + ch *channel requests chan *fcallRequest closed chan struct{} tags uint16 } -func newTransport(ctx context.Context, conn net.Conn) roundTripper { +func newTransport(ctx context.Context, ch *channel) roundTripper { t := &transport{ ctx: ctx, - conn: conn, + ch: ch, requests: make(chan *fcallRequest), closed: make(chan struct{}), } @@ -77,6 +75,7 @@ func (t *transport) send(ctx context.Context, fcall *F case <-ctx.Done(): return nil, ctx.Err() case resp := <-req.response: + log.Println("resp", resp) if resp.Type == Rerror { // pack the error into something useful respmesg, ok := resp.Message.(*MessageRerror) @@ -100,56 +99,42 @@ func (t *transport) handle() { tags Tag // outstanding provides a map of tags to outstanding requests. outstanding = map[Tag]*fcallRequest{} - brd = bufio.NewReader(t.conn) - bwr = bufio.NewWriter(t.conn) - enc = &encoder{bwr} - dec = &decoder{brd} ) // loop to read messages off of the connection go func() { - + defer func() { + log.Println("exited read loop") + close(t.closed) + }() loop: for { - const pump = time.Second - - // Continuously set the read dead line pump the loop below. We can - // probably set a connection dead threshold that can count these. - // Usually, this would only matter when there are actually - // outstanding requests. - deadline, ok := t.ctx.Deadline() - if !ok { - deadline = time.Now().Add(pump) - } else { - // if the deadline is before - nd := time.Now().Add(pump) - if nd.Before(deadline) { - deadline = nd - } - } - - if err := t.conn.SetReadDeadline(deadline); err != nil { - log.Printf("error setting read deadline: %v", err) - } - - fc := new(Fcall) - if err := dec.decode(fc); err != nil { + fcall := new(Fcall) + if err := t.ch.readFcall(t.ctx, fcall); err != nil { switch err := err.(type) { case net.Error: if err.Timeout() || err.Temporary() { - break loop + // BUG(stevvooe): There may be partial reads under + // timeout errors where this is actually fatal. + + // can only retry if we haven't offset the frame. + continue loop } } - panic(fmt.Sprintf("connection read error: %v", err)) + log.Println("fatal error reading msg:", err) + t.Close() + return } select { case <-t.ctx.Done(): + log.Println("ctx done") return case <-t.closed: + log.Println("transport closed") return - case responses <- fc: + case responses <- fcall: } } }() @@ -157,37 +142,33 @@ func (t *transport) handle() { for { select { case req := <-t.requests: - tags++ - req.fcall.Tag = tags - outstanding[req.fcall.Tag] = req - // use deadline to set write deadline for this request. - deadline, ok := req.ctx.Deadline() - if !ok { - deadline = time.Now().Add(time.Second) + log.Println("send", req.fcall) + if req.fcall.Type != Tversion { + tags++ + req.fcall.Tag = tags + outstanding[req.fcall.Tag] = req + } else { + // TODO(stevvooe): Man this protocol is bad. Version messages + // have no response tag. Effectively, the client can only have + // one version call outstanding at a time. We have to create + // an entire special code path to handle it. The client + // shouldn't proceed until the version reply is completed. + req.fcall.Tag = NOTAG } - if err := t.conn.SetWriteDeadline(deadline); err != nil { - log.Printf("error setting write deadline: %v", err) - } - // TODO(stevvooe): Consider the case of requests that never // receive a response. We need to remove the fcall context from // the tag map and dealloc the tag. We may also want to send a // flush for the tag. - - log.Println("send", req.fcall) - if err := enc.encode(req.fcall); err != nil { + if err := t.ch.writeFcall(req.ctx, req.fcall); err != nil { delete(outstanding, req.fcall.Tag) req.err <- err } - if err := bwr.Flush(); err != nil { - delete(outstanding, req.fcall.Tag) - req.err <- err - } log.Println("sent", req.fcall) case b := <-responses: + log.Println("recv", b) req, ok := outstanding[b.Tag] if !ok { panic("unknown tag received") blob - a721ab636b2e82ae5b06a92c70f3c5b17e96a47e blob + 604d967a1ddcf792eb4bba17b3110e4ac5814534 --- types.go +++ types.go @@ -1,6 +1,9 @@ package p9pnew -import "time" +import ( + "fmt" + "time" +) const ( NOFID = ^Fid(0) @@ -14,20 +17,35 @@ const ( DMMOUNT = 0x10000000 // mode bit for mounted channel DMAUTH = 0x08000000 // mode bit for authentication file DMTMP = 0x04000000 // mode bit for non-backed-up files - DMREAD = 0x4 // mode bit for read permission - DMWRITE = 0x2 // mode bit for write permission - DMEXEC = 0x1 // mode bit for execute permission + + // 9p2000.u extensions + DMSYMLINK = 0x02000000 + DMDEVICE = 0x00800000 + DMNAMEDPIPE = 0x00200000 + DMSOCKET = 0x00100000 + DMSETUID = 0x00080000 + DMSETGID = 0x00040000 + + DMREAD = 0x4 // mode bit for read permission + DMWRITE = 0x2 // mode bit for write permission + DMEXEC = 0x1 // mode bit for execute permission ) const ( - 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 - OEXCL = 0x1000 // or'ed in, exclusive use (create only) + OREAD = 0x00 // open for read + OWRITE = 0x01 // write + ORDWR = 0x02 // read and write + OEXEC = 0x03 // execute, == read but check execute permission + + // PROPOSAL(stevvooe): Possible protocal extension to allow the create of + // symlinks. Initially, the link is created with no value. Read and write + // to read and set the link value. + OSYMLINK = 0x04 + + // or'd in + OTRUNC = 0x10 // or'ed in (except for exec), truncate file first + OCEXEC = 0x20 // or'ed in, close on exec + ORCLOSE = 0x40 // or'ed in, remove on close ) type QType uint8 @@ -42,28 +60,58 @@ const ( QTFILE QType = 0x00 // plain file ) +func (qt QType) String() string { + switch qt { + case QTDIR: + return "dir" + case QTAPPEND: + return "append" + case QTEXCL: + return "excl" + case QTMOUNT: + return "mount" + case QTAUTH: + return "auth" + case QTTMP: + return "tmp" + case QTFILE: + return "file" + } + + return "unknown" +} + type Fid uint32 type Qid struct { - Type QType + Type QType `9p:type,1` Version uint32 Path uint64 } +func (qid Qid) String() string { + return fmt.Sprintf("Qid(%v, version=%x, path=%x)", + qid.Type, qid.Version, qid.Path) +} + type Dir struct { - Type uint16 - Dev uint32 - Qid Qid - Mode uint32 + Type uint16 + Dev uint32 + Qid Qid + Mode uint32 + + // BUG(stevvooe): The Year 2038 is coming soon. 9p wire protocol has these + // as 4 byte epoch times. Some possibilities include time dilation fields + // or atemporal files. We can also just not use them and set them to zero. + AccessTime time.Time ModTime time.Time - Length uint64 - Name string - UID string - GID string - MUID string - // TODO(stevvooe): 9p2000.u/L should go here. + Length uint64 + Name string + UID string + GID string + MUID string } //