├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS ├── README.md ├── channel.go ├── channel_test.go ├── client.go ├── cmd ├── 9pr │ └── main.go └── 9ps │ └── main.go ├── context.go ├── dispatcher.go ├── doc.go ├── encoding.go ├── encoding_test.go ├── errors.go ├── fcall.go ├── logging.go ├── messages.go ├── overflow.go ├── readdir.go ├── server.go ├── session.go ├── transport.go ├── types.go ├── ufs ├── fileref.go ├── session.go ├── util.go ├── util_darwin.go └── util_linux.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - 1.8 6 | - tip 7 | 8 | script: 9 | - go test -coverprofile=coverage.txt -covermode=atomic -race 10 | 11 | # CODECOV_TOKEN set in travisCI ENV 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Docker open source projects 2 | 3 | Want to hack on this project? Awesome! Here are instructions to get you started. 4 | 5 | This project is a part of the [Docker](https://www.docker.com) project, and follows 6 | the same rules and principles. If you're already familiar with the way 7 | Docker does things, you'll feel right at home. 8 | 9 | Otherwise, go read Docker's 10 | [contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), 11 | [issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md), 12 | [review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and 13 | 14 | For an in-depth description of our contribution process, visit the 15 | contributors guide: [Understand how to contribute](https://docs.docker.com/opensource/workflow/make-a-contribution/) 16 | 17 | ### Sign your work 18 | 19 | The sign-off is a simple line at the end of the explanation for the patch. Your 20 | signature certifies that you wrote the patch or otherwise have the right to pass 21 | it on as an open-source patch. The rules are pretty simple: if you can certify 22 | the below (from [developercertificate.org](http://developercertificate.org/)): 23 | 24 | ``` 25 | Developer Certificate of Origin 26 | Version 1.1 27 | 28 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 29 | 660 York Street, Suite 102, 30 | San Francisco, CA 94110 USA 31 | 32 | Everyone is permitted to copy and distribute verbatim copies of this 33 | license document, but changing it is not allowed. 34 | 35 | Developer's Certificate of Origin 1.1 36 | 37 | By making a contribution to this project, I certify that: 38 | 39 | (a) The contribution was created in whole or in part by me and I 40 | have the right to submit it under the open source license 41 | indicated in the file; or 42 | 43 | (b) The contribution is based upon previous work that, to the best 44 | of my knowledge, is covered under an appropriate open source 45 | license and I have the right under that license to submit that 46 | work with modifications, whether created in whole or in part 47 | by me, under the same open source license (unless I am 48 | permitted to submit under a different license), as indicated 49 | in the file; or 50 | 51 | (c) The contribution was provided directly to me by some other 52 | person who certified (a), (b) or (c) and I have not modified 53 | it. 54 | 55 | (d) I understand and agree that this project and the contribution 56 | are public and that a record of the contribution (including all 57 | personal information I submit with it, including my sign-off) is 58 | maintained indefinitely and may be redistributed consistent with 59 | this project or the open source license(s) involved. 60 | ``` 61 | 62 | Then you just add a line to every git commit message: 63 | 64 | Signed-off-by: Joe Smith 65 | 66 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 67 | 68 | If you set your `user.name` and `user.email` git configs, you can sign your 69 | commit automatically with `git commit -s`. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Docker, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # go-p9p maintainers file 2 | # 3 | # This file describes who runs the docker/go-p9p project and how. 4 | # This is a living document - if you see something out of date or missing, speak up! 5 | # 6 | # It is structured to be consumable by both humans and programs. 7 | # To extract its contents programmatically, use any TOML-compliant parser. 8 | # 9 | # This file is compiled into the MAINTAINERS file in docker/opensource. 10 | # 11 | [Org] 12 | [Org."Core maintainers"] 13 | people = [ 14 | "aduermael", 15 | "djs55", 16 | "FrenchBen", 17 | "justincormack", 18 | "stevvooe", 19 | ] 20 | 21 | [Org."Docs maintainers"] 22 | 23 | people = [ 24 | "thajeztah" 25 | ] 26 | 27 | [people] 28 | 29 | # A reference list of all people associated with the project. 30 | # All other sections should refer to people by their canonical key 31 | # in the people section. 32 | 33 | # ADD YOURSELF HERE IN ALPHABETICAL ORDER 34 | 35 | [people.aduermael] 36 | Name = "Adrien Duermael" 37 | Email = "adrien@docker.com" 38 | GitHub = "aduermael" 39 | 40 | [people.djs55] 41 | Name = "David Scott" 42 | Email = "dave.scott@docker.com" 43 | GitHub = "djs55" 44 | 45 | [people.FrenchBen] 46 | Name = "Ben French" 47 | Email = "frenchben@docker.com" 48 | GitHub = "FrenchBen" 49 | 50 | [people.justincormack] 51 | Name = "Justin Cormack" 52 | Email = "justin.cormack@docker.com" 53 | GitHub = "justincormack" 54 | 55 | [people.stevvooe] 56 | Name = "Stephen Day" 57 | Email = "stephen.day@docker.com" 58 | GitHub = "stevvooe" 59 | 60 | [people.thajeztah] 61 | Name = "Sebastiaan van Stijn" 62 | Email = "github@gone.nl" 63 | GitHub = "thaJeztah" 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # p9p [![GoDoc](https://godoc.org/github.com/docker/go-p9p?status.svg)](https://godoc.org/github.com/docker/go-p9p) [![Apache licensed](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/docker/go-p9p/master/LICENSE) [![CircleCI](https://circleci.com/gh/docker/go-p9p.svg?style=shield)](https://circleci.com/gh/docker/go-p9p) [![TravisCI](https://travis-ci.org/docker/go-p9p.svg?branch=master)](https://travis-ci.org/docker/go-p9p) [![Go Report Card](https://goreportcard.com/badge/github.com/docker/go-p9p)](https://goreportcard.com/report/github.com/docker/go-p9p) [![Badge Badge](http://doyouevenbadge.com/github.com/docker/go-p9p)](http://doyouevenbadge.com/report/github.com/docker/go-p9p) 2 | 3 | 4 | A modern, performant 9P library for Go. 5 | 6 | For information on usage, please see the [GoDoc](https://godoc.org/github.com/docker/go-p9p). 7 | 8 | Refer to [9P's documentation](http://9p.cat-v.org/documentation) for more details on the protocol. 9 | 10 | ## Copyright and license 11 | 12 | Copyright © 2015 Docker, Inc. go-p9p is licensed under the Apache License, 13 | Version 2.0. See [LICENSE](LICENSE) for the full license text. 14 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/binary" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "time" 12 | ) 13 | 14 | const ( 15 | // channelMessageHeaderSize is the overhead for sending the size of a 16 | // message on the wire. 17 | channelMessageHeaderSize = 4 18 | ) 19 | 20 | // Channel defines the operations necessary to implement a 9p message channel 21 | // interface. Typically, message channels do no protocol processing except to 22 | // send and receive message frames. 23 | type Channel interface { 24 | // ReadFcall reads one fcall frame into the provided fcall structure. The 25 | // Fcall may be cleared whether there is an error or not. If the operation 26 | // is successful, the contents of the fcall will be populated in the 27 | // argument. ReadFcall cannot be called concurrently with other calls to 28 | // ReadFcall. This both to preserve message ordering and to allow lockless 29 | // buffer reusage. 30 | ReadFcall(ctx context.Context, fcall *Fcall) error 31 | 32 | // WriteFcall writes the provided fcall to the channel. WriteFcall cannot 33 | // be called concurrently with other calls to WriteFcall. 34 | WriteFcall(ctx context.Context, fcall *Fcall) error 35 | 36 | // MSize returns the current msize for the channel. 37 | MSize() int 38 | 39 | // SetMSize sets the maximum message size for the channel. This must never 40 | // be called currently with ReadFcall or WriteFcall. 41 | SetMSize(msize int) 42 | } 43 | 44 | // NewChannel returns a new channel to read and write Fcalls with the provided 45 | // connection and message size. 46 | func NewChannel(conn net.Conn, msize int) Channel { 47 | return newChannel(conn, codec9p{}, msize) 48 | } 49 | 50 | const ( 51 | defaultRWTimeout = 30 * time.Second // default read/write timeout if not set in context 52 | ) 53 | 54 | // channel provides bidirectional protocol framing for 9p over net.Conn. 55 | // Operations are not thread-safe but reads and writes may be carried out 56 | // concurrently, supporting separate read and write loops. 57 | // 58 | // Lifecyle 59 | // 60 | // A connection, or message channel abstraction, has a lifecycle delineated by 61 | // Tversion/Rversion request response cycles. For now, this is part of the 62 | // channel itself but doesn't necessarily influence the channels state, except 63 | // the msize. Visually, it might look something like this: 64 | // 65 | // [Established] -> [Version] -> [Session] -> [Version]---+ 66 | // ^ | 67 | // |_________________________________| 68 | // 69 | // The connection is established, then we negotiate a version, run a session, 70 | // then negotiate a version and so on. For most purposes, we are likely going 71 | // to terminate the connection after the session but we may want to support 72 | // connection pooling. Pooling may result in possible security leaks if the 73 | // connections are shared among contexts, since the version is negotiated at 74 | // the start of the session. To avoid this, we can actually use a "tombstone" 75 | // version message which clears the server's session state without starting a 76 | // new session. The next version message would then prepare the session 77 | // without leaking any Fid's. 78 | type channel struct { 79 | conn net.Conn 80 | codec Codec 81 | brd *bufio.Reader 82 | bwr *bufio.Writer 83 | closed chan struct{} 84 | msize int 85 | rdbuf []byte 86 | } 87 | 88 | func newChannel(conn net.Conn, codec Codec, msize int) *channel { 89 | return &channel{ 90 | conn: conn, 91 | codec: codec, 92 | brd: bufio.NewReaderSize(conn, msize), // msize may not be optimal buffer size 93 | bwr: bufio.NewWriterSize(conn, msize), 94 | closed: make(chan struct{}), 95 | msize: msize, 96 | rdbuf: make([]byte, msize), 97 | } 98 | } 99 | 100 | func (ch *channel) MSize() int { 101 | return ch.msize 102 | } 103 | 104 | // setmsize resizes the buffers for use with a separate msize. This call must 105 | // be protected by a mutex or made before passing to other goroutines. 106 | func (ch *channel) SetMSize(msize int) { 107 | // NOTE(stevvooe): We cannot safely resize the buffered reader and writer. 108 | // Proceed assuming that original size is sufficient. 109 | 110 | ch.msize = msize 111 | if msize < len(ch.rdbuf) { 112 | // just change the cap 113 | ch.rdbuf = ch.rdbuf[:msize] 114 | return 115 | } 116 | 117 | ch.rdbuf = make([]byte, msize) 118 | } 119 | 120 | // ReadFcall reads the next message from the channel into fcall. 121 | // 122 | // If the incoming message overflows the msize, Overflow(err) will return 123 | // nonzero with the number of bytes overflowed. 124 | func (ch *channel) ReadFcall(ctx context.Context, fcall *Fcall) error { 125 | select { 126 | case <-ctx.Done(): 127 | return ctx.Err() 128 | case <-ch.closed: 129 | return ErrClosed 130 | default: 131 | } 132 | 133 | deadline, ok := ctx.Deadline() 134 | if !ok { 135 | deadline = time.Now().Add(defaultRWTimeout) 136 | } 137 | 138 | if err := ch.conn.SetReadDeadline(deadline); err != nil { 139 | log.Printf("p9p: transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err) 140 | } 141 | 142 | n, err := readmsg(ch.brd, ch.rdbuf) 143 | if err != nil { 144 | // TODO(stevvooe): There may be more we can do here to detect partial 145 | // reads. For now, we just propagate the error untouched. 146 | return err 147 | } 148 | 149 | if n > len(ch.rdbuf) { 150 | return overflowErr{size: n - len(ch.rdbuf)} 151 | } 152 | 153 | // clear out the fcall 154 | *fcall = Fcall{} 155 | if err := ch.codec.Unmarshal(ch.rdbuf[:n], fcall); err != nil { 156 | return err 157 | } 158 | 159 | if err := ch.maybeTruncate(fcall); err != nil { 160 | return err 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // WriteFcall writes the message to the connection. 167 | // 168 | // If a message destined for the wire will overflow MSize, an Overflow error 169 | // may be returned. For Twrite calls, the buffer will simply be truncated to 170 | // the optimal msize, with the caller detecting this condition with 171 | // Rwrite.Count. 172 | func (ch *channel) WriteFcall(ctx context.Context, fcall *Fcall) error { 173 | select { 174 | case <-ctx.Done(): 175 | return ctx.Err() 176 | case <-ch.closed: 177 | return ErrClosed 178 | default: 179 | } 180 | 181 | deadline, ok := ctx.Deadline() 182 | if !ok { 183 | deadline = time.Now().Add(defaultRWTimeout) 184 | } 185 | 186 | if err := ch.conn.SetWriteDeadline(deadline); err != nil { 187 | log.Printf("p9p: transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err) 188 | } 189 | 190 | if err := ch.maybeTruncate(fcall); err != nil { 191 | return err 192 | } 193 | 194 | p, err := ch.codec.Marshal(fcall) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | if err := sendmsg(ch.bwr, p); err != nil { 200 | return err 201 | } 202 | 203 | return ch.bwr.Flush() 204 | } 205 | 206 | // maybeTruncate will truncate the message to fit into msize on the wire, if 207 | // possible, or modify the message to ensure the response won't overflow. 208 | // 209 | // If the message cannot be truncated, an error will be returned and the 210 | // message should not be sent. 211 | // 212 | // A nil return value means the message can be sent without 213 | func (ch *channel) maybeTruncate(fcall *Fcall) error { 214 | 215 | // for certain message types, just remove the extra bytes from the data portion. 216 | switch msg := fcall.Message.(type) { 217 | // TODO(stevvooe): There is one more problematic message type: 218 | // 219 | // Rread: while we can employ the same truncation fix as Twrite, we 220 | // need to make it observable to upstream handlers. 221 | 222 | case MessageTread: 223 | // We can rewrite msg.Count so that a return message will be under 224 | // msize. This is more defensive than anything but will ensure that 225 | // calls don't fail on sloppy servers. 226 | 227 | // first, craft the shape of the response message 228 | resp := newFcall(fcall.Tag, MessageRread{}) 229 | overflow := uint32(ch.msgmsize(resp)) + msg.Count - uint32(ch.msize) 230 | 231 | if msg.Count < overflow { 232 | // Let the bad thing happen; msize too small to even support valid 233 | // rewrite. This will result in a Terror from the server-side or 234 | // just work. 235 | return nil 236 | } 237 | 238 | msg.Count -= overflow 239 | fcall.Message = msg 240 | 241 | return nil 242 | case MessageTwrite: 243 | // If we are going to overflow the msize, we need to truncate the write to 244 | // appropriate size or throw an error in all other conditions. 245 | size := ch.msgmsize(fcall) 246 | if size <= ch.msize { 247 | return nil 248 | } 249 | 250 | // overflow the msize, including the channel message size fields. 251 | overflow := size - ch.msize 252 | 253 | if len(msg.Data) < overflow { 254 | // paranoid: if msg.Data is not big enough to handle the 255 | // overflow, we should get an overflow error. MSize would have 256 | // to be way too small to be realistic. 257 | return overflowErr{size: overflow} 258 | } 259 | 260 | // The truncation is reflected in the return message (Rwrite) by 261 | // the server, so we don't need a return value or error condition 262 | // to communicate it. 263 | msg.Data = msg.Data[:len(msg.Data)-overflow] 264 | fcall.Message = msg // since we have a local copy 265 | 266 | return nil 267 | default: 268 | size := ch.msgmsize(fcall) 269 | if size > ch.msize { 270 | // overflow the msize, including the channel message size fields. 271 | return overflowErr{size: size - ch.msize} 272 | } 273 | 274 | return nil 275 | } 276 | 277 | } 278 | 279 | // msgmsize returns the on-wire msize of the Fcall, including the size header. 280 | // Typically, this can be used to detect whether or not the message overflows 281 | // the msize buffer. 282 | func (ch *channel) msgmsize(fcall *Fcall) int { 283 | return channelMessageHeaderSize + ch.codec.Size(fcall) 284 | } 285 | 286 | // readmsg reads a 9p message into p from rd, ensuring that all bytes are 287 | // consumed from the size header. If the size header indicates the message is 288 | // larger than p, the entire message will be discarded, leaving a truncated 289 | // portion in p. Any error should be treated as a framing error unless n is 290 | // zero. The caller must check that n is less than or equal to len(p) to 291 | // ensure that a valid message has been read. 292 | func readmsg(rd io.Reader, p []byte) (n int, err error) { 293 | var msize uint32 294 | 295 | if err := binary.Read(rd, binary.LittleEndian, &msize); err != nil { 296 | return 0, err 297 | } 298 | 299 | n += binary.Size(msize) 300 | mbody := int(msize) - 4 301 | 302 | if mbody < len(p) { 303 | p = p[:mbody] 304 | } 305 | 306 | np, err := io.ReadFull(rd, p) 307 | if err != nil { 308 | return np + n, err 309 | } 310 | n += np 311 | 312 | if mbody > len(p) { 313 | // message has been read up to len(p) but we must consume the entire 314 | // message. This is an error condition but is non-fatal if we can 315 | // consume msize bytes. 316 | nn, err := io.CopyN(ioutil.Discard, rd, int64(mbody-len(p))) 317 | n += int(nn) 318 | if err != nil { 319 | return n, err 320 | } 321 | } 322 | 323 | return n, nil 324 | } 325 | 326 | // sendmsg writes a message of len(p) to wr with a 9p size header. All errors 327 | // should be considered terminal. 328 | func sendmsg(wr io.Writer, p []byte) error { 329 | size := uint32(len(p) + 4) // message size plus 4-bytes for size. 330 | if err := binary.Write(wr, binary.LittleEndian, size); err != nil { 331 | return err 332 | } 333 | 334 | // This assume partial writes to wr aren't possible. Not sure if this 335 | // valid. Matters during timeout retries. 336 | if n, err := wr.Write(p); err != nil { 337 | return err 338 | } else if n < len(p) { 339 | return io.ErrShortWrite 340 | } 341 | 342 | return nil 343 | } 344 | -------------------------------------------------------------------------------- /channel_test.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | // TestTwriteOverflow ensures that a Twrite message will have the data field 13 | // truncated if the msize would be exceeded. 14 | func TestTwriteOverflow(t *testing.T) { 15 | const ( 16 | msize = 512 17 | 18 | // size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] | count = 0 19 | overhead = 4 + 1 + 2 + 4 + 8 + 4 20 | ) 21 | 22 | var ( 23 | ctx = context.Background() 24 | conn = &mockConn{} 25 | ch = NewChannel(conn, msize) 26 | ) 27 | 28 | for _, testcase := range []struct { 29 | name string 30 | overflow int // amount to overflow the message by. 31 | }{ 32 | { 33 | name: "BoundedOverflow", 34 | overflow: msize / 2, 35 | }, 36 | { 37 | name: "LargeOverflow", 38 | overflow: msize * 3, 39 | }, 40 | { 41 | name: "HeaderOverflow", 42 | overflow: overhead, 43 | }, 44 | { 45 | name: "HeaderOffsetOverflow", 46 | overflow: overhead - 1, 47 | }, 48 | { 49 | name: "OverflowByOne", 50 | overflow: 1, 51 | }, 52 | } { 53 | 54 | t.Run(testcase.name, func(t *testing.T) { 55 | var ( 56 | fcall = overflowMessage(ch.(*channel).codec, msize, testcase.overflow) 57 | data = fcall.Message.(MessageTwrite).Data 58 | size uint32 59 | ) 60 | 61 | t.Logf("overflow: %v, len(data): %v, expected overflow: %v", testcase.overflow, len(data), overhead+len(data)-msize) 62 | conn.buf.Reset() 63 | if err := ch.WriteFcall(ctx, fcall); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if err := binary.Read(bytes.NewReader(conn.buf.Bytes()), binary.LittleEndian, &size); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if size != msize { 72 | t.Fatalf("should have truncated size header: %d != %d", size, msize) 73 | } 74 | 75 | if conn.buf.Len() != msize { 76 | t.Fatalf("should have truncated message: conn.buf.Len(%v) != msize(%v)", conn.buf.Len(), msize) 77 | } 78 | }) 79 | } 80 | 81 | } 82 | 83 | // TestWriteOverflowError ensures that we return an error in cases when there 84 | // will certainly be an overflow and it cannot be resolved. 85 | func TestWriteOverflowError(t *testing.T) { 86 | const ( 87 | msize = 4 88 | overflowMSize = msize + 1 89 | ) 90 | 91 | var ( 92 | ctx = context.Background() 93 | conn = &mockConn{} 94 | ch = NewChannel(conn, msize) 95 | data = bytes.Repeat([]byte{'A'}, 4) 96 | fcall = newFcall(1, MessageTwrite{ 97 | Data: data, 98 | }) 99 | messageSize = 4 + ch.(*channel).codec.Size(fcall) 100 | ) 101 | 102 | err := ch.WriteFcall(ctx, fcall) 103 | if err == nil { 104 | t.Fatal("error expected when overflowing message") 105 | } 106 | 107 | if Overflow(err) != messageSize-msize { 108 | t.Fatalf("overflow should reflect messageSize and msize, %d != %d", Overflow(err), messageSize-msize) 109 | } 110 | } 111 | 112 | // TestReadOverflow ensures that messages coming over a network connection do 113 | // not overflow the msize. Invalid messages will cause `ReadFcall` to return an 114 | // Overflow error. 115 | func TestReadFcallOverflow(t *testing.T) { 116 | const ( 117 | msize = 256 118 | ) 119 | 120 | var ( 121 | ctx = context.Background() 122 | conn = &mockConn{} 123 | ch = NewChannel(conn, msize) 124 | codec = ch.(*channel).codec 125 | ) 126 | 127 | for _, testcase := range []struct { 128 | name string 129 | overflow int 130 | }{ 131 | { 132 | name: "OverflowByOne", 133 | overflow: 1, 134 | }, 135 | { 136 | name: "HeaderOverflow", 137 | overflow: overheadMessage(codec, MessageTwrite{}), 138 | }, 139 | { 140 | name: "HeaderOffsetOverflow", 141 | overflow: overheadMessage(codec, MessageTwrite{}) - 1, 142 | }, 143 | } { 144 | t.Run(testcase.name, func(t *testing.T) { 145 | fcall := overflowMessage(codec, msize, testcase.overflow) 146 | 147 | // prepare the raw message 148 | p, err := ch.(*channel).codec.Marshal(fcall) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | // "send" the message into the buffer 154 | // this message is crafted to overflow the read buffer. 155 | if err := sendmsg(&conn.buf, p); err != nil { 156 | t.Fatal(err) 157 | } 158 | 159 | var incoming Fcall 160 | err = ch.ReadFcall(ctx, &incoming) 161 | if err == nil { 162 | t.Fatal("expected error on fcall") 163 | } 164 | 165 | // sanity check to ensure our test code has the right overflow 166 | if testcase.overflow != ch.(*channel).msgmsize(fcall)-msize { 167 | t.Fatalf("overflow calculation incorrect: %v != %v", testcase.overflow, ch.(*channel).msgmsize(fcall)-msize) 168 | } 169 | 170 | if Overflow(err) != testcase.overflow { 171 | t.Fatalf("unexpected overflow on error: %v !=%v", Overflow(err), testcase.overflow) 172 | } 173 | }) 174 | } 175 | } 176 | 177 | // TestTreadRewrite ensures that messages that whose response would overflow 178 | // the msize will have be adjusted before sending. 179 | func TestTreadRewrite(t *testing.T) { 180 | const ( 181 | msize = 256 182 | overflowMSize = msize + 1 183 | ) 184 | 185 | var ( 186 | ctx = context.Background() 187 | conn = &mockConn{} 188 | ch = NewChannel(conn, msize) 189 | buf = make([]byte, overflowMSize) 190 | // data = bytes.Repeat([]byte{'A'}, overflowMSize) 191 | fcall = newFcall(1, MessageTread{ 192 | Count: overflowMSize, 193 | }) 194 | responseMSize = ch.(*channel).msgmsize(newFcall(1, MessageRread{ 195 | Data: buf, 196 | })) 197 | ) 198 | 199 | if err := ch.WriteFcall(ctx, fcall); err != nil { 200 | t.Fatal(err) 201 | } 202 | 203 | // just read the message off the buffer 204 | n, err := readmsg(&conn.buf, buf) 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | *fcall = Fcall{} 210 | if err := ch.(*channel).codec.Unmarshal(buf[:n], fcall); err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | tread, ok := fcall.Message.(MessageTread) 215 | if !ok { 216 | t.Fatalf("unexpected message: %v", fcall) 217 | } 218 | 219 | if tread.Count != overflowMSize-(uint32(responseMSize)-msize) { 220 | t.Fatalf("count not rewritten: %v != %v", tread.Count, overflowMSize-(uint32(responseMSize)-msize)) 221 | } 222 | } 223 | 224 | type mockConn struct { 225 | net.Conn 226 | buf bytes.Buffer 227 | } 228 | 229 | func (m mockConn) SetWriteDeadline(t time.Time) error { return nil } 230 | func (m mockConn) SetReadDeadline(t time.Time) error { return nil } 231 | 232 | func (m *mockConn) Write(p []byte) (int, error) { 233 | return m.buf.Write(p) 234 | } 235 | 236 | func (m *mockConn) Read(p []byte) (int, error) { 237 | return m.buf.Read(p) 238 | } 239 | 240 | func overheadMessage(codec Codec, msg Message) int { 241 | return 4 + codec.Size(newFcall(1, msg)) 242 | } 243 | 244 | // overflowMessage returns message that overflows the msize by overflow bytes, 245 | // returning the message size and the fcall. 246 | func overflowMessage(codec Codec, msize, overflow int) *Fcall { 247 | var ( 248 | overhead = overheadMessage(codec, MessageTwrite{}) 249 | data = bytes.Repeat([]byte{'A'}, (msize-overhead)+overflow) 250 | fcall = newFcall(1, MessageTwrite{ 251 | Data: data, 252 | }) 253 | ) 254 | 255 | return fcall 256 | } 257 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | "context" 8 | ) 9 | 10 | type client struct { 11 | version string 12 | msize int 13 | ctx context.Context 14 | transport roundTripper 15 | } 16 | 17 | // NewSession returns a session using the connection. The Context ctx provides 18 | // a context for out of bad messages, such as flushes, that may be sent by the 19 | // session. The session can effectively shutdown with this context. 20 | func NewSession(ctx context.Context, conn net.Conn) (Session, error) { 21 | ch := newChannel(conn, codec9p{}, DefaultMSize) // sets msize, effectively. 22 | 23 | // negotiate the protocol version 24 | version, err := clientnegotiate(ctx, ch, DefaultVersion) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &client{ 30 | version: version, 31 | msize: ch.MSize(), 32 | ctx: ctx, 33 | transport: newTransport(ctx, ch), 34 | }, nil 35 | } 36 | 37 | var _ Session = &client{} 38 | 39 | func (c *client) Version() (int, string) { 40 | return c.msize, c.version 41 | } 42 | 43 | func (c *client) Auth(ctx context.Context, afid Fid, uname, aname string) (Qid, error) { 44 | m := MessageTauth{ 45 | Afid: afid, 46 | Uname: uname, 47 | Aname: aname, 48 | } 49 | 50 | resp, err := c.transport.send(ctx, m) 51 | if err != nil { 52 | return Qid{}, err 53 | } 54 | 55 | rauth, ok := resp.(MessageRauth) 56 | if !ok { 57 | return Qid{}, ErrUnexpectedMsg 58 | } 59 | 60 | return rauth.Qid, nil 61 | } 62 | 63 | func (c *client) Attach(ctx context.Context, fid, afid Fid, uname, aname string) (Qid, error) { 64 | m := MessageTattach{ 65 | Fid: fid, 66 | Afid: afid, 67 | Uname: uname, 68 | Aname: aname, 69 | } 70 | 71 | resp, err := c.transport.send(ctx, m) 72 | if err != nil { 73 | return Qid{}, err 74 | } 75 | 76 | rattach, ok := resp.(MessageRattach) 77 | if !ok { 78 | return Qid{}, ErrUnexpectedMsg 79 | } 80 | 81 | return rattach.Qid, nil 82 | } 83 | 84 | func (c *client) Clunk(ctx context.Context, fid Fid) error { 85 | resp, err := c.transport.send(ctx, MessageTclunk{ 86 | Fid: fid, 87 | }) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | _, ok := resp.(MessageRclunk) 93 | if !ok { 94 | return ErrUnexpectedMsg 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (c *client) Remove(ctx context.Context, fid Fid) error { 101 | resp, err := c.transport.send(ctx, MessageTremove{ 102 | Fid: fid, 103 | }) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | _, ok := resp.(MessageRremove) 109 | if !ok { 110 | return ErrUnexpectedMsg 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (c *client) Walk(ctx context.Context, fid Fid, newfid Fid, names ...string) ([]Qid, error) { 117 | if len(names) > 16 { 118 | return nil, ErrWalkLimit 119 | } 120 | 121 | resp, err := c.transport.send(ctx, MessageTwalk{ 122 | Fid: fid, 123 | Newfid: newfid, 124 | Wnames: names, 125 | }) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | rwalk, ok := resp.(MessageRwalk) 131 | if !ok { 132 | return nil, ErrUnexpectedMsg 133 | } 134 | 135 | return rwalk.Qids, nil 136 | } 137 | 138 | func (c *client) Read(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) { 139 | resp, err := c.transport.send(ctx, MessageTread{ 140 | Fid: fid, 141 | Offset: uint64(offset), 142 | Count: uint32(len(p)), 143 | }) 144 | if err != nil { 145 | return 0, err 146 | } 147 | 148 | rread, ok := resp.(MessageRread) 149 | if !ok { 150 | return 0, ErrUnexpectedMsg 151 | } 152 | 153 | n = copy(p, rread.Data) 154 | switch { 155 | case len(rread.Data) == 0: 156 | err = io.EOF 157 | case n < len(p): 158 | // TODO(stevvooe): Technically, we should treat this as an io.EOF. 159 | // However, we cannot tell if the short read was due to EOF or due to 160 | // truncation. 161 | } 162 | 163 | return n, err 164 | } 165 | 166 | func (c *client) Write(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) { 167 | resp, err := c.transport.send(ctx, MessageTwrite{ 168 | Fid: fid, 169 | Offset: uint64(offset), 170 | Data: p, 171 | }) 172 | if err != nil { 173 | return 0, err 174 | } 175 | 176 | rwrite, ok := resp.(MessageRwrite) 177 | if !ok { 178 | return 0, ErrUnexpectedMsg 179 | } 180 | 181 | if int(rwrite.Count) < len(p) { 182 | err = io.ErrShortWrite 183 | } 184 | 185 | return int(rwrite.Count), err 186 | } 187 | 188 | func (c *client) Open(ctx context.Context, fid Fid, mode Flag) (Qid, uint32, error) { 189 | resp, err := c.transport.send(ctx, MessageTopen{ 190 | Fid: fid, 191 | Mode: mode, 192 | }) 193 | if err != nil { 194 | return Qid{}, 0, err 195 | } 196 | 197 | ropen, ok := resp.(MessageRopen) 198 | if !ok { 199 | return Qid{}, 0, ErrUnexpectedMsg 200 | } 201 | 202 | return ropen.Qid, ropen.IOUnit, nil 203 | } 204 | 205 | func (c *client) Create(ctx context.Context, parent Fid, name string, perm uint32, mode Flag) (Qid, uint32, error) { 206 | resp, err := c.transport.send(ctx, MessageTcreate{ 207 | Fid: parent, 208 | Name: name, 209 | Perm: perm, 210 | Mode: mode, 211 | }) 212 | if err != nil { 213 | return Qid{}, 0, err 214 | } 215 | 216 | rcreate, ok := resp.(MessageRcreate) 217 | if !ok { 218 | return Qid{}, 0, ErrUnexpectedMsg 219 | } 220 | 221 | return rcreate.Qid, rcreate.IOUnit, nil 222 | } 223 | 224 | func (c *client) Stat(ctx context.Context, fid Fid) (Dir, error) { 225 | resp, err := c.transport.send(ctx, MessageTstat{Fid: fid}) 226 | if err != nil { 227 | return Dir{}, err 228 | } 229 | 230 | rstat, ok := resp.(MessageRstat) 231 | if !ok { 232 | return Dir{}, ErrUnexpectedMsg 233 | } 234 | 235 | return rstat.Stat, nil 236 | } 237 | 238 | func (c *client) WStat(ctx context.Context, fid Fid, dir Dir) error { 239 | resp, err := c.transport.send(ctx, MessageTwstat{ 240 | Fid: fid, 241 | Stat: dir, 242 | }) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | _, ok := resp.(MessageRwstat) 248 | if !ok { 249 | return ErrUnexpectedMsg 250 | } 251 | 252 | return nil 253 | } 254 | -------------------------------------------------------------------------------- /cmd/9pr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | _ "net/http/pprof" 12 | "os" 13 | "path" 14 | "strings" 15 | "text/tabwriter" 16 | "time" 17 | 18 | "github.com/chzyer/readline" 19 | "github.com/docker/go-p9p" 20 | "golang.org/x/net/context" 21 | ) 22 | 23 | var addr string 24 | 25 | func init() { 26 | flag.StringVar(&addr, "addr", ":5640", "addr of 9p service") 27 | } 28 | 29 | func main() { 30 | go func() { 31 | log.Println(http.ListenAndServe("localhost:6060", nil)) 32 | }() 33 | 34 | ctx := context.Background() 35 | log.SetFlags(0) 36 | flag.Parse() 37 | 38 | proto := "tcp" 39 | if strings.HasPrefix(addr, "unix:") { 40 | proto = "unix" 41 | addr = addr[5:] 42 | } 43 | 44 | log.Println("dialing", addr) 45 | conn, err := net.Dial(proto, addr) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | csession, err := p9p.NewSession(ctx, conn) 51 | if err != nil { 52 | log.Fatalln(err) 53 | } 54 | 55 | commander := &fsCommander{ 56 | ctx: context.Background(), 57 | session: csession, 58 | pwd: "/", 59 | stdout: os.Stdout, 60 | stderr: os.Stderr, 61 | } 62 | 63 | completer := readline.NewPrefixCompleter( 64 | readline.PcItem("ls"), 65 | // readline.PcItem("find"), 66 | readline.PcItem("stat"), 67 | readline.PcItem("cat"), 68 | readline.PcItem("cd"), 69 | readline.PcItem("pwd"), 70 | ) 71 | 72 | rl, err := readline.NewEx(&readline.Config{ 73 | HistoryFile: ".history", 74 | AutoComplete: completer, 75 | }) 76 | if err != nil { 77 | log.Fatalln(err) 78 | } 79 | commander.readline = rl 80 | 81 | msize, version := commander.session.Version() 82 | if err != nil { 83 | log.Fatalln(err) 84 | } 85 | log.Println("9p version", version, msize) 86 | 87 | // attach root 88 | commander.nextfid = 1 89 | if _, err := commander.session.Attach(commander.ctx, commander.nextfid, p9p.NOFID, "anyone", "/"); err != nil { 90 | log.Fatalln(err) 91 | } 92 | commander.rootfid = commander.nextfid 93 | commander.nextfid++ 94 | 95 | // clone the pwd fid so we can clunk it 96 | if _, err := commander.session.Walk(commander.ctx, commander.rootfid, commander.nextfid); err != nil { 97 | log.Fatalln(err) 98 | } 99 | commander.pwdfid = commander.nextfid 100 | commander.nextfid++ 101 | 102 | for { 103 | commander.readline.SetPrompt(fmt.Sprintf("%s 🐳 > ", commander.pwd)) 104 | 105 | line, err := rl.Readline() 106 | if err != nil { 107 | log.Fatalln("error: ", err) 108 | } 109 | 110 | if line == "" { 111 | continue 112 | } 113 | 114 | args := strings.Fields(line) 115 | 116 | name := args[0] 117 | var cmd func(ctx context.Context, args ...string) error 118 | 119 | switch name { 120 | case "ls": 121 | cmd = commander.cmdls 122 | case "cd": 123 | cmd = commander.cmdcd 124 | case "pwd": 125 | cmd = commander.cmdpwd 126 | case "cat": 127 | cmd = commander.cmdcat 128 | case "stat": 129 | cmd = commander.cmdstat 130 | default: 131 | cmd = func(ctx context.Context, args ...string) error { 132 | return fmt.Errorf("command not implemented") 133 | } 134 | } 135 | 136 | ctx, _ = context.WithTimeout(commander.ctx, 5*time.Second) 137 | if err := cmd(ctx, args[1:]...); err != nil { 138 | if err == p9p.ErrClosed { 139 | log.Println("connection closed, shutting down") 140 | return 141 | } 142 | 143 | log.Printf("👹 %s: %v", name, err) 144 | } 145 | } 146 | } 147 | 148 | type fsCommander struct { 149 | ctx context.Context 150 | session p9p.Session 151 | pwd string 152 | pwdfid p9p.Fid 153 | rootfid p9p.Fid 154 | 155 | nextfid p9p.Fid 156 | 157 | readline *readline.Instance 158 | stdout io.Writer 159 | stderr io.Writer 160 | } 161 | 162 | func (c *fsCommander) cmdls(ctx context.Context, args ...string) error { 163 | ps := []string{c.pwd} 164 | if len(args) > 0 { 165 | ps = args 166 | } 167 | 168 | wr := tabwriter.NewWriter(c.stdout, 0, 8, 8, ' ', 0) 169 | 170 | for _, p := range ps { 171 | // create a header if have more than one path. 172 | if len(ps) > 1 { 173 | fmt.Fprintln(wr, p+":") 174 | } 175 | 176 | if !path.IsAbs(p) { 177 | p = path.Join(c.pwd, p) 178 | } 179 | 180 | targetfid := c.nextfid 181 | c.nextfid++ 182 | components := strings.Split(strings.Trim(p, "/"), "/") 183 | if _, err := c.session.Walk(ctx, c.rootfid, targetfid, components...); err != nil { 184 | return err 185 | } 186 | defer c.session.Clunk(ctx, targetfid) 187 | 188 | _, iounit, err := c.session.Open(ctx, targetfid, p9p.OREAD) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | if iounit < 1 { 194 | msize, _ := c.session.Version() 195 | iounit = uint32(msize - 24) // size of message max minus fcall io header (Rread) 196 | } 197 | 198 | p := make([]byte, iounit) 199 | 200 | n, err := c.session.Read(ctx, targetfid, p, 0) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | rd := bytes.NewReader(p[:n]) 206 | codec := p9p.NewCodec() // TODO(stevvooe): Need way to resolve codec based on session. 207 | for { 208 | var d p9p.Dir 209 | if err := p9p.DecodeDir(codec, rd, &d); err != nil { 210 | if err == io.EOF { 211 | break 212 | } 213 | 214 | return err 215 | } 216 | 217 | fmt.Fprintf(wr, "%v\t%v\t%v\t%s\n", os.FileMode(d.Mode), d.Length, d.ModTime, d.Name) 218 | } 219 | 220 | if len(ps) > 1 { 221 | fmt.Fprintln(wr, "") 222 | } 223 | } 224 | 225 | // all output is dumped only after success. 226 | return wr.Flush() 227 | } 228 | 229 | func (c *fsCommander) cmdcd(ctx context.Context, args ...string) error { 230 | var p string 231 | switch len(args) { 232 | case 0: 233 | p = "/" 234 | case 1: 235 | p = args[0] 236 | default: 237 | return fmt.Errorf("cd: invalid args: %v", args) 238 | } 239 | 240 | if !path.IsAbs(p) { 241 | p = path.Join(c.pwd, p) 242 | } 243 | 244 | targetfid := c.nextfid 245 | c.nextfid++ 246 | components := strings.Split(strings.TrimSpace(strings.Trim(p, "/")), "/") 247 | if _, err := c.session.Walk(c.ctx, c.rootfid, targetfid, components...); err != nil { 248 | return err 249 | } 250 | defer c.session.Clunk(c.ctx, c.pwdfid) 251 | 252 | log.Println("cd", p, targetfid) 253 | c.pwd = p 254 | c.pwdfid = targetfid 255 | 256 | return nil 257 | } 258 | 259 | func (c *fsCommander) cmdstat(ctx context.Context, args ...string) error { 260 | ps := []string{c.pwd} 261 | if len(args) > 0 { 262 | ps = args 263 | } 264 | 265 | wr := tabwriter.NewWriter(c.stdout, 0, 8, 8, ' ', 0) 266 | 267 | for _, p := range ps { 268 | targetfid := c.nextfid 269 | c.nextfid++ 270 | components := strings.Split(strings.Trim(p, "/"), "/") 271 | if _, err := c.session.Walk(ctx, c.rootfid, targetfid, components...); err != nil { 272 | return err 273 | } 274 | defer c.session.Clunk(ctx, targetfid) 275 | 276 | d, err := c.session.Stat(ctx, targetfid) 277 | if err != nil { 278 | return err 279 | } 280 | 281 | fmt.Fprintf(wr, "%v\t%v\t%v\t%s\n", os.FileMode(d.Mode), d.Length, d.ModTime, d.Name) 282 | } 283 | 284 | return wr.Flush() 285 | } 286 | 287 | func (c *fsCommander) cmdpwd(ctx context.Context, args ...string) error { 288 | if len(args) != 0 { 289 | return fmt.Errorf("pwd takes no arguments") 290 | } 291 | 292 | fmt.Println(c.pwd) 293 | return nil 294 | } 295 | 296 | func (c *fsCommander) cmdcat(ctx context.Context, args ...string) error { 297 | var p string 298 | switch len(args) { 299 | case 0: 300 | p = "/" 301 | case 1: 302 | p = args[0] 303 | default: 304 | return fmt.Errorf("cd: invalid args: %v", args) 305 | } 306 | 307 | if !path.IsAbs(p) { 308 | p = path.Join(c.pwd, p) 309 | } 310 | 311 | targetfid := c.nextfid 312 | c.nextfid++ 313 | components := strings.Split(strings.TrimSpace(strings.Trim(p, "/")), "/") 314 | if _, err := c.session.Walk(ctx, c.rootfid, targetfid, components...); err != nil { 315 | return err 316 | } 317 | defer c.session.Clunk(ctx, c.pwdfid) 318 | 319 | _, iounit, err := c.session.Open(ctx, targetfid, p9p.OREAD) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | if iounit < 1 { 325 | msize, _ := c.session.Version() 326 | iounit = uint32(msize - 24) // size of message max minus fcall io header (Rread) 327 | } 328 | 329 | b := make([]byte, iounit) 330 | 331 | n, err := c.session.Read(ctx, targetfid, b, 0) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | if _, err := os.Stdout.Write(b[:n]); err != nil { 337 | return err 338 | } 339 | 340 | os.Stdout.Write([]byte("\n")) 341 | 342 | return nil 343 | } 344 | -------------------------------------------------------------------------------- /cmd/9ps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net" 7 | "strings" 8 | 9 | "github.com/docker/go-p9p" 10 | "github.com/docker/go-p9p/ufs" 11 | "golang.org/x/net/context" 12 | ) 13 | 14 | var ( 15 | root string 16 | addr string 17 | ) 18 | 19 | func init() { 20 | flag.StringVar(&root, "root", "~/", "root of filesystem to serve over 9p") 21 | flag.StringVar(&addr, "addr", ":5640", "bind addr for 9p server, prefix with unix: for unix socket") 22 | } 23 | 24 | func main() { 25 | ctx := context.Background() 26 | log.SetFlags(0) 27 | flag.Parse() 28 | 29 | proto := "tcp" 30 | if strings.HasPrefix(addr, "unix:") { 31 | proto = "unix" 32 | addr = addr[5:] 33 | } 34 | 35 | listener, err := net.Listen(proto, addr) 36 | if err != nil { 37 | log.Fatalln("error listening:", err) 38 | } 39 | defer listener.Close() 40 | 41 | for { 42 | c, err := listener.Accept() 43 | if err != nil { 44 | log.Fatalln("error accepting:", err) 45 | continue 46 | } 47 | 48 | go func(conn net.Conn) { 49 | defer conn.Close() 50 | 51 | ctx := context.WithValue(ctx, "conn", conn) 52 | log.Println("connected", conn.RemoteAddr()) 53 | session, err := ufs.NewSession(ctx, root) 54 | if err != nil { 55 | log.Println("error creating session") 56 | return 57 | } 58 | 59 | if err := p9p.ServeConn(ctx, conn, p9p.Dispatch(session)); err != nil { 60 | log.Printf("serving conn: %v", err) 61 | } 62 | }(c) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey string 8 | 9 | const ( 10 | versionKey contextKey = "9p.version" 11 | ) 12 | 13 | func withVersion(ctx context.Context, version string) context.Context { 14 | return context.WithValue(ctx, versionKey, version) 15 | } 16 | 17 | // GetVersion returns the protocol version from the context. If the version is 18 | // not known, an empty string is returned. This is typically set on the 19 | // context passed into function calls in a server implementation. 20 | func GetVersion(ctx context.Context) string { 21 | v, ok := ctx.Value(versionKey).(string) 22 | if !ok { 23 | return "" 24 | } 25 | return v 26 | } 27 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import "context" 4 | 5 | // Handler defines an interface for 9p message handlers. A handler 6 | // implementation could be used to intercept calls of all types before sending 7 | // them to the next handler. 8 | type Handler interface { 9 | Handle(ctx context.Context, msg Message) (Message, error) 10 | 11 | // TODO(stevvooe): Right now, this interface is functianally identical to 12 | // roundtripper. If we find that this is sufficient on the server-side, we 13 | // may unify the types. For now, we leave them separated to differentiate 14 | // between them. 15 | } 16 | 17 | // HandlerFunc is a convenience type for defining inline handlers. 18 | type HandlerFunc func(ctx context.Context, msg Message) (Message, error) 19 | 20 | // Handle implements the requirements for the Handler interface. 21 | func (fn HandlerFunc) Handle(ctx context.Context, msg Message) (Message, error) { 22 | return fn(ctx, msg) 23 | } 24 | 25 | // Dispatch returns a handler that dispatches messages to the target session. 26 | // No concurrency is managed by the returned handler. It simply turns messages 27 | // into function calls on the session. 28 | func Dispatch(session Session) Handler { 29 | return HandlerFunc(func(ctx context.Context, msg Message) (Message, error) { 30 | switch msg := msg.(type) { 31 | case MessageTauth: 32 | qid, err := session.Auth(ctx, msg.Afid, msg.Uname, msg.Aname) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return MessageRauth{Qid: qid}, nil 38 | case MessageTattach: 39 | qid, err := session.Attach(ctx, msg.Fid, msg.Afid, msg.Uname, msg.Aname) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return MessageRattach{ 45 | Qid: qid, 46 | }, nil 47 | case MessageTwalk: 48 | // TODO(stevvooe): This is one of the places where we need to manage 49 | // fid allocation lifecycle. We need to reserve the fid, then, if this 50 | // call succeeds, we should alloc the fid for future uses. Also need 51 | // to interact correctly with concurrent clunk and the flush of this 52 | // walk message. 53 | qids, err := session.Walk(ctx, msg.Fid, msg.Newfid, msg.Wnames...) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return MessageRwalk{ 59 | Qids: qids, 60 | }, nil 61 | case MessageTopen: 62 | qid, iounit, err := session.Open(ctx, msg.Fid, msg.Mode) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return MessageRopen{ 68 | Qid: qid, 69 | IOUnit: iounit, 70 | }, nil 71 | case MessageTcreate: 72 | qid, iounit, err := session.Create(ctx, msg.Fid, msg.Name, msg.Perm, msg.Mode) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return MessageRcreate{ 78 | Qid: qid, 79 | IOUnit: iounit, 80 | }, nil 81 | case MessageTread: 82 | p := make([]byte, int(msg.Count)) 83 | n, err := session.Read(ctx, msg.Fid, p, int64(msg.Offset)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return MessageRread{ 89 | Data: p[:n], 90 | }, nil 91 | case MessageTwrite: 92 | n, err := session.Write(ctx, msg.Fid, msg.Data, int64(msg.Offset)) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return MessageRwrite{ 98 | Count: uint32(n), 99 | }, nil 100 | case MessageTclunk: 101 | // TODO(stevvooe): Manage the clunking of file descriptors based on 102 | // walk and attach call progression. 103 | if err := session.Clunk(ctx, msg.Fid); err != nil { 104 | return nil, err 105 | } 106 | 107 | return MessageRclunk{}, nil 108 | case MessageTremove: 109 | if err := session.Remove(ctx, msg.Fid); err != nil { 110 | return nil, err 111 | } 112 | 113 | return MessageRremove{}, nil 114 | case MessageTstat: 115 | dir, err := session.Stat(ctx, msg.Fid) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return MessageRstat{ 121 | Stat: dir, 122 | }, nil 123 | case MessageTwstat: 124 | if err := session.WStat(ctx, msg.Fid, msg.Stat); err != nil { 125 | return nil, err 126 | } 127 | 128 | return MessageRwstat{}, nil 129 | default: 130 | return nil, ErrUnknownMsg 131 | } 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package p9p implements a compliant 9P2000 client and server library for use 3 | in modern, production Go services. This package differentiates itself in that 4 | is has departed from the plan 9 implementation primitives and better follows 5 | idiomatic Go style. 6 | 7 | The package revolves around the session type, which is an enumeration of raw 8 | 9p message calls. A few calls, such as flush and version, have been elided, 9 | deferring their usage to the server implementation. Sessions can be trivially 10 | proxied through clients and servers. 11 | 12 | Getting Started 13 | 14 | The best place to get started is with Serve. Serve can be provided a 15 | connection and a handler. A typical implementation will call Serve as part of 16 | a listen/accept loop. As each network connection is created, Serve can be 17 | called with a handler for the specific connection. The handler can be 18 | implemented with a Session via the Dispatch function or can generate sessions 19 | for dispatch in response to client messages. (See cmd/9ps for an example) 20 | 21 | On the client side, NewSession provides a 9p session from a connection. After 22 | a version negotiation, methods can be called on the session, in parallel, and 23 | calls will be sent over the connection. Call timeouts can be controlled via 24 | the context provided to each method call. 25 | 26 | Framework 27 | 28 | This package has the beginning of a nice client-server framework for working 29 | with 9p. Some of the abstractions aren't entirely fleshed out, but most of 30 | this can center around the Handler. 31 | 32 | Missing from this are a number of tools for implementing 9p servers. The most 33 | glaring are directory read and walk helpers. Other, more complex additions 34 | might be a system to manage in memory filesystem trees that expose multi-user 35 | sessions. 36 | 37 | Differences 38 | 39 | The largest difference between this package and other 9p packages is 40 | simplification of the types needed to implement a server. To avoid confusing 41 | bugs and odd behavior, the components are separated by each level of the 42 | protocol. One example is that requests and responses are separated and they no 43 | longer hold mutable state. This means that framing, transport management, 44 | encoding, and dispatching are componentized. Little work will be required to 45 | swap out encodings, transports or connection implementations. 46 | 47 | Context Integration 48 | 49 | This package has been wired from top to bottom to support context-based 50 | resource management. Everything from startup to shutdown can have timeouts 51 | using contexts. Not all close methods are fully in place, but we are very 52 | close to having controlled, predictable cleanup for both servers and clients. 53 | Timeouts can be very granular or very course, depending on the context of the 54 | timeout. For example, it is very easy to set a short timeout for a stat call 55 | but a long timeout for reading data. 56 | 57 | Multiversion Support 58 | 59 | Currently, there is not multiversion support. The hooks and functionality are 60 | in place to add multi-version support. Generally, the correct space to do this 61 | is in the codec. Types, such as Dir, simply need to be extended to support the 62 | possibility of extra fields. 63 | 64 | The real question to ask here is what is the role of the version number in the 65 | 9p protocol. It really comes down to the level of support required. Do we just 66 | need it at the protocol level, or do handlers and sessions need to be have 67 | differently based on negotiated versions? 68 | 69 | Caveats 70 | 71 | This package has a number of TODOs to make it easier to use. Most of the 72 | existing code provides a solid base to work from. Don't be discouraged by the 73 | sawdust. 74 | 75 | In addition, the testing is embarassingly lacking. With time, we can get full 76 | testing going and ensure we have confidence in the implementation. 77 | */ 78 | package p9p 79 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Codec defines the interface for encoding and decoding of 9p types. 14 | // Unsupported types will throw an error. 15 | type Codec interface { 16 | // Unmarshal from data into the value pointed to by v. 17 | Unmarshal(data []byte, v interface{}) error 18 | 19 | // Marshal the value v into a byte slice. 20 | Marshal(v interface{}) ([]byte, error) 21 | 22 | // Size returns the encoded size for the target of v. 23 | Size(v interface{}) int 24 | } 25 | 26 | // NewCodec returns a new, standard 9P2000 codec, ready for use. 27 | func NewCodec() Codec { 28 | return codec9p{} 29 | } 30 | 31 | type codec9p struct{} 32 | 33 | func (c codec9p) Unmarshal(data []byte, v interface{}) error { 34 | dec := &decoder{bytes.NewReader(data)} 35 | return dec.decode(v) 36 | } 37 | 38 | func (c codec9p) Marshal(v interface{}) ([]byte, error) { 39 | var b bytes.Buffer 40 | enc := &encoder{&b} 41 | 42 | if err := enc.encode(v); err != nil { 43 | return nil, err 44 | } 45 | 46 | return b.Bytes(), nil 47 | } 48 | 49 | func (c codec9p) Size(v interface{}) int { 50 | return int(size9p(v)) 51 | } 52 | 53 | // DecodeDir decodes a directory entry from rd using the provided codec. 54 | func DecodeDir(codec Codec, rd io.Reader, d *Dir) error { 55 | var ll uint16 56 | 57 | // pull the size off the wire 58 | if err := binary.Read(rd, binary.LittleEndian, &ll); err != nil { 59 | return err 60 | } 61 | 62 | p := make([]byte, ll+2) 63 | binary.LittleEndian.PutUint16(p, ll) // must have size at start 64 | 65 | // read out the rest of the record 66 | if _, err := io.ReadFull(rd, p[2:]); err != nil { 67 | return err 68 | } 69 | 70 | return codec.Unmarshal(p, d) 71 | } 72 | 73 | // EncodeDir writes the directory to wr. 74 | func EncodeDir(codec Codec, wr io.Writer, d *Dir) error { 75 | p, err := codec.Marshal(d) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | _, err = wr.Write(p) 81 | return err 82 | } 83 | 84 | type encoder struct { 85 | wr io.Writer 86 | } 87 | 88 | func (e *encoder) encode(vs ...interface{}) error { 89 | for _, v := range vs { 90 | switch v := v.(type) { 91 | case uint8, uint16, uint32, uint64, FcallType, Tag, QType, Fid, Flag, 92 | *uint8, *uint16, *uint32, *uint64, *FcallType, *Tag, *QType, *Fid, *Flag: 93 | if err := binary.Write(e.wr, binary.LittleEndian, v); err != nil { 94 | return err 95 | } 96 | case []byte: 97 | if err := e.encode(uint32(len(v))); err != nil { 98 | return err 99 | } 100 | 101 | if err := binary.Write(e.wr, binary.LittleEndian, v); err != nil { 102 | return err 103 | } 104 | 105 | case *[]byte: 106 | if err := e.encode(*v); err != nil { 107 | return err 108 | } 109 | case string: 110 | if err := binary.Write(e.wr, binary.LittleEndian, uint16(len(v))); err != nil { 111 | return err 112 | } 113 | 114 | _, err := io.WriteString(e.wr, v) 115 | if err != nil { 116 | return err 117 | } 118 | case *string: 119 | if err := e.encode(*v); err != nil { 120 | return err 121 | } 122 | 123 | case []string: 124 | if err := e.encode(uint16(len(v))); err != nil { 125 | return err 126 | } 127 | 128 | for _, m := range v { 129 | if err := e.encode(m); err != nil { 130 | return err 131 | } 132 | } 133 | case *[]string: 134 | if err := e.encode(*v); err != nil { 135 | return err 136 | } 137 | case time.Time: 138 | if err := e.encode(uint32(v.Unix())); err != nil { 139 | return err 140 | } 141 | case *time.Time: 142 | if err := e.encode(*v); err != nil { 143 | return err 144 | } 145 | case Qid: 146 | if err := e.encode(v.Type, v.Version, v.Path); err != nil { 147 | return err 148 | } 149 | case *Qid: 150 | if err := e.encode(*v); err != nil { 151 | return err 152 | } 153 | case []Qid: 154 | if err := e.encode(uint16(len(v))); err != nil { 155 | return err 156 | } 157 | 158 | elements := make([]interface{}, len(v)) 159 | for i := range v { 160 | elements[i] = &v[i] 161 | } 162 | 163 | if err := e.encode(elements...); err != nil { 164 | return err 165 | } 166 | case *[]Qid: 167 | if err := e.encode(*v); err != nil { 168 | return err 169 | } 170 | case Dir: 171 | elements, err := fields9p(v) 172 | if err != nil { 173 | return err 174 | } 175 | 176 | if err := e.encode(uint16(size9p(elements...))); err != nil { 177 | return err 178 | } 179 | 180 | if err := e.encode(elements...); err != nil { 181 | return err 182 | } 183 | case *Dir: 184 | if err := e.encode(*v); err != nil { 185 | return err 186 | } 187 | case []Dir: 188 | elements := make([]interface{}, len(v)) 189 | for i := range v { 190 | elements[i] = &v[i] 191 | } 192 | 193 | if err := e.encode(elements...); err != nil { 194 | return err 195 | } 196 | case *[]Dir: 197 | if err := e.encode(*v); err != nil { 198 | return err 199 | } 200 | case Fcall: 201 | if err := e.encode(v.Type, v.Tag, v.Message); err != nil { 202 | return err 203 | } 204 | case *Fcall: 205 | if err := e.encode(*v); err != nil { 206 | return err 207 | } 208 | case Message: 209 | elements, err := fields9p(v) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | switch v.(type) { 215 | case MessageRstat, *MessageRstat: 216 | // NOTE(stevvooe): Prepend size preceeding Dir. See bugs in 217 | // http://man.cat-v.org/plan_9/5/stat to make sense of this. 218 | // The field has been included here but we need to make sure 219 | // to double emit it for Rstat. 220 | if err := e.encode(uint16(size9p(elements...))); err != nil { 221 | return err 222 | } 223 | } 224 | 225 | if err := e.encode(elements...); err != nil { 226 | return err 227 | } 228 | } 229 | } 230 | 231 | return nil 232 | } 233 | 234 | type decoder struct { 235 | rd io.Reader 236 | } 237 | 238 | // read9p extracts values from rd and unmarshals them to the targets of vs. 239 | func (d *decoder) decode(vs ...interface{}) error { 240 | for _, v := range vs { 241 | switch v := v.(type) { 242 | case *uint8, *uint16, *uint32, *uint64, *FcallType, *Tag, *QType, *Fid, *Flag: 243 | if err := binary.Read(d.rd, binary.LittleEndian, v); err != nil { 244 | return err 245 | } 246 | case *[]byte: 247 | var ll uint32 248 | 249 | if err := d.decode(&ll); err != nil { 250 | return err 251 | } 252 | 253 | if ll > 0 { 254 | *v = make([]byte, int(ll)) 255 | } 256 | 257 | if err := binary.Read(d.rd, binary.LittleEndian, v); err != nil { 258 | return err 259 | } 260 | case *string: 261 | var ll uint16 262 | 263 | // implement string[s] encoding 264 | if err := d.decode(&ll); err != nil { 265 | return err 266 | } 267 | 268 | b := make([]byte, ll) 269 | 270 | n, err := io.ReadFull(d.rd, b) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | if n != int(ll) { 276 | return fmt.Errorf("unexpected string length") 277 | } 278 | 279 | *v = string(b) 280 | case *[]string: 281 | var ll uint16 282 | 283 | if err := d.decode(&ll); err != nil { 284 | return err 285 | } 286 | 287 | elements := make([]interface{}, int(ll)) 288 | *v = make([]string, int(ll)) 289 | for i := range elements { 290 | elements[i] = &(*v)[i] 291 | } 292 | 293 | if err := d.decode(elements...); err != nil { 294 | return err 295 | } 296 | case *time.Time: 297 | var epoch uint32 298 | if err := d.decode(&epoch); err != nil { 299 | return err 300 | } 301 | 302 | *v = time.Unix(int64(epoch), 0).UTC() 303 | case *Qid: 304 | if err := d.decode(&v.Type, &v.Version, &v.Path); err != nil { 305 | return err 306 | } 307 | case *[]Qid: 308 | var ll uint16 309 | 310 | if err := d.decode(&ll); err != nil { 311 | return err 312 | } 313 | 314 | elements := make([]interface{}, int(ll)) 315 | *v = make([]Qid, int(ll)) 316 | for i := range elements { 317 | elements[i] = &(*v)[i] 318 | } 319 | 320 | if err := d.decode(elements...); err != nil { 321 | return err 322 | } 323 | case *Dir: 324 | var ll uint16 325 | 326 | if err := d.decode(&ll); err != nil { 327 | return err 328 | } 329 | 330 | b := make([]byte, ll) 331 | // must consume entire dir entry. 332 | if _, err := io.ReadFull(d.rd, b); err != nil { 333 | return err 334 | } 335 | 336 | elements, err := fields9p(v) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | dec := &decoder{bytes.NewReader(b)} 342 | 343 | if err := dec.decode(elements...); err != nil { 344 | return err 345 | } 346 | case *[]Dir: 347 | *v = make([]Dir, 0) 348 | for { 349 | element := Dir{} 350 | if err := d.decode(&element); err != nil { 351 | if err == io.EOF { 352 | return nil 353 | } 354 | return err 355 | } 356 | *v = append(*v, element) 357 | } 358 | case *Fcall: 359 | if err := d.decode(&v.Type, &v.Tag); err != nil { 360 | return err 361 | } 362 | 363 | message, err := newMessage(v.Type) 364 | if err != nil { 365 | return err 366 | } 367 | 368 | // NOTE(stevvooe): We do a little pointer dance to allocate the 369 | // new type, write to it, then assign it back to the interface as 370 | // a concrete type, avoiding a pointer (the interface) to a 371 | // pointer. 372 | rv := reflect.New(reflect.TypeOf(message)) 373 | if err := d.decode(rv.Interface()); err != nil { 374 | return err 375 | } 376 | 377 | v.Message = rv.Elem().Interface().(Message) 378 | case Message: 379 | elements, err := fields9p(v) 380 | if err != nil { 381 | return err 382 | } 383 | 384 | switch v.(type) { 385 | case *MessageRstat, MessageRstat: 386 | // NOTE(stevvooe): Consume extra size preceeding Dir. See bugs 387 | // in http://man.cat-v.org/plan_9/5/stat to make sense of 388 | // this. The field has been included here but we need to make 389 | // sure to double emit it for Rstat. decode extra size header 390 | // for stat structure. 391 | var ll uint16 392 | if err := d.decode(&ll); err != nil { 393 | return err 394 | } 395 | } 396 | 397 | if err := d.decode(elements...); err != nil { 398 | return err 399 | } 400 | } 401 | } 402 | 403 | return nil 404 | } 405 | 406 | // size9p calculates the projected size of the values in vs when encoded into 407 | // 9p binary protocol. If an element or elements are not valid for 9p encoded, 408 | // the value 0 will be used for the size. The error will be detected when 409 | // encoding. 410 | func size9p(vs ...interface{}) uint32 { 411 | var s uint32 412 | for _, v := range vs { 413 | if v == nil { 414 | continue 415 | } 416 | 417 | switch v := v.(type) { 418 | case uint8, uint16, uint32, uint64, FcallType, Tag, QType, Fid, Flag, 419 | *uint8, *uint16, *uint32, *uint64, *FcallType, *Tag, *QType, *Fid, *Flag: 420 | s += uint32(binary.Size(v)) 421 | case []byte: 422 | s += uint32(binary.Size(uint32(0)) + len(v)) 423 | case *[]byte: 424 | s += size9p(uint32(0), *v) 425 | case string: 426 | s += uint32(binary.Size(uint16(0)) + len(v)) 427 | case *string: 428 | s += size9p(*v) 429 | case []string: 430 | s += size9p(uint16(0)) 431 | 432 | for _, sv := range v { 433 | s += size9p(sv) 434 | } 435 | case *[]string: 436 | s += size9p(*v) 437 | case time.Time, *time.Time: 438 | // BUG(stevvooe): Y2038 is coming. 439 | s += size9p(uint32(0)) 440 | case Qid: 441 | s += size9p(v.Type, v.Version, v.Path) 442 | case *Qid: 443 | s += size9p(*v) 444 | case []Qid: 445 | s += size9p(uint16(0)) 446 | elements := make([]interface{}, len(v)) 447 | for i := range elements { 448 | elements[i] = &v[i] 449 | } 450 | s += size9p(elements...) 451 | case *[]Qid: 452 | s += size9p(*v) 453 | 454 | case Dir: 455 | // walk the fields of the message to get the total size. we just 456 | // use the field order from the message struct. We may add tag 457 | // ignoring if needed. 458 | elements, err := fields9p(v) 459 | if err != nil { 460 | // BUG(stevvooe): The options here are to return 0, panic or 461 | // make this return an error. Ideally, we make it safe to 462 | // return 0 and have the rest of the package do the right 463 | // thing. For now, we do this, but may want to panic until 464 | // things are stable. 465 | panic(err) 466 | } 467 | 468 | s += size9p(elements...) + size9p(uint16(0)) 469 | case *Dir: 470 | s += size9p(*v) 471 | case []Dir: 472 | elements := make([]interface{}, len(v)) 473 | for i := range elements { 474 | elements[i] = &v[i] 475 | } 476 | s += size9p(elements...) 477 | case *[]Dir: 478 | s += size9p(*v) 479 | case Fcall: 480 | s += size9p(v.Type, v.Tag, v.Message) 481 | case *Fcall: 482 | s += size9p(*v) 483 | case Message: 484 | // special case twstat and rstat for size fields. See bugs in 485 | // http://man.cat-v.org/plan_9/5/stat to make sense of this. 486 | switch v.(type) { 487 | case *MessageRstat, MessageRstat: 488 | s += size9p(uint16(0)) // for extra size field before dir 489 | } 490 | 491 | // walk the fields of the message to get the total size. we just 492 | // use the field order from the message struct. We may add tag 493 | // ignoring if needed. 494 | elements, err := fields9p(v) 495 | if err != nil { 496 | // BUG(stevvooe): The options here are to return 0, panic or 497 | // make this return an error. Ideally, we make it safe to 498 | // return 0 and have the rest of the package do the right 499 | // thing. For now, we do this, but may want to panic until 500 | // things are stable. 501 | panic(err) 502 | } 503 | 504 | s += size9p(elements...) 505 | } 506 | } 507 | 508 | return s 509 | } 510 | 511 | // fields9p lists the settable fields from a struct type for reading and 512 | // writing. We are using a lot of reflection here for fairly static 513 | // serialization but we can replace this in the future with generated code if 514 | // performance is an issue. 515 | func fields9p(v interface{}) ([]interface{}, error) { 516 | rv := reflect.Indirect(reflect.ValueOf(v)) 517 | 518 | if rv.Kind() != reflect.Struct { 519 | return nil, fmt.Errorf("cannot extract fields from non-struct: %v", rv) 520 | } 521 | 522 | var elements []interface{} 523 | for i := 0; i < rv.NumField(); i++ { 524 | f := rv.Field(i) 525 | 526 | if !f.CanInterface() { 527 | // unexported field, skip it. 528 | continue 529 | } 530 | 531 | if f.CanAddr() { 532 | f = f.Addr() 533 | } 534 | 535 | elements = append(elements, f.Interface()) 536 | } 537 | 538 | return elements, nil 539 | } 540 | 541 | func string9p(v interface{}) string { 542 | if v == nil { 543 | return "nil" 544 | } 545 | 546 | rv := reflect.Indirect(reflect.ValueOf(v)) 547 | 548 | if rv.Kind() != reflect.Struct { 549 | panic("not a struct") 550 | } 551 | 552 | var s string 553 | 554 | for i := 0; i < rv.NumField(); i++ { 555 | f := rv.Field(i) 556 | 557 | s += fmt.Sprintf(" %v=%v", strings.ToLower(rv.Type().Field(i).Name), f.Interface()) 558 | } 559 | 560 | return s 561 | } 562 | -------------------------------------------------------------------------------- /encoding_test.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestEncodeDecode(t *testing.T) { 12 | codec := NewCodec() 13 | for _, testcase := range []struct { 14 | description string 15 | target interface{} 16 | marshaled []byte 17 | }{ 18 | { 19 | description: "uint8", 20 | target: uint8('U'), 21 | marshaled: []byte{0x55}, 22 | }, 23 | { 24 | description: "uint16", 25 | target: uint16(0x5544), 26 | marshaled: []byte{0x44, 0x55}, 27 | }, 28 | { 29 | description: "string", 30 | target: "asdf", 31 | marshaled: []byte{0x4, 0x0, 0x61, 0x73, 0x64, 0x66}, 32 | }, 33 | { 34 | description: "StringSlice", 35 | target: []string{"asdf", "qwer", "zxcv"}, 36 | marshaled: []byte{ 37 | 0x3, 0x0, // len(target) 38 | 0x4, 0x0, 0x61, 0x73, 0x64, 0x66, 39 | 0x4, 0x0, 0x71, 0x77, 0x65, 0x72, 40 | 0x4, 0x0, 0x7a, 0x78, 0x63, 0x76}, 41 | }, 42 | { 43 | description: "Qid", 44 | target: Qid{ 45 | Type: QTDIR, 46 | Version: 0x10203040, 47 | Path: 0x1020304050607080}, 48 | marshaled: []byte{ 49 | byte(QTDIR), // qtype 50 | 0x40, 0x30, 0x20, 0x10, // version 51 | 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, // path 52 | }, 53 | }, 54 | // Dir 55 | { 56 | description: "TversionFcall", 57 | target: &Fcall{ 58 | Type: Tversion, 59 | Tag: 2255, 60 | Message: MessageTversion{ 61 | MSize: uint32(1024), 62 | Version: "9PTEST", 63 | }, 64 | }, 65 | marshaled: []byte{ 66 | 0x64, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0, 67 | 0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54}, 68 | }, 69 | { 70 | description: "RversionFcall", 71 | target: &Fcall{ 72 | Type: Rversion, 73 | Tag: 2255, 74 | Message: MessageRversion{ 75 | MSize: uint32(1024), 76 | Version: "9PTEST", 77 | }, 78 | }, 79 | marshaled: []byte{ 80 | 0x65, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0, 81 | 0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54}, 82 | }, 83 | { 84 | description: "TwalkFcall", 85 | target: &Fcall{ 86 | Type: Twalk, 87 | Tag: 5666, 88 | Message: MessageTwalk{ 89 | Fid: 1010, 90 | Newfid: 1011, 91 | Wnames: []string{"a", "b", "c"}, 92 | }, 93 | }, 94 | marshaled: []byte{ 95 | 0x6e, 0x22, 0x16, 0xf2, 0x3, 0x0, 0x0, 0xf3, 0x3, 0x0, 0x0, 96 | 0x3, 0x0, // len(wnames) 97 | 0x1, 0x0, 0x61, // "a" 98 | 0x1, 0x0, 0x62, // "b" 99 | 0x1, 0x0, 0x63}, // "c" 100 | }, 101 | { 102 | description: "RwalkFcall", 103 | target: &Fcall{ 104 | Type: Rwalk, 105 | Tag: 5556, 106 | Message: MessageRwalk{ 107 | Qids: []Qid{ 108 | Qid{ 109 | Type: QTDIR, 110 | Path: 1111, 111 | Version: 11112, 112 | }, 113 | Qid{Type: QTFILE, 114 | Version: 1112, 115 | Path: 11114}, 116 | }, 117 | }, 118 | }, 119 | marshaled: []byte{ 120 | 0x6f, 0xb4, 0x15, 121 | 0x2, 0x0, 122 | 0x80, 0x68, 0x2b, 0x0, 0x0, 0x57, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 123 | 0x0, 0x58, 0x4, 0x0, 0x0, 0x6a, 0x2b, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 124 | }, 125 | { 126 | description: "EmptyRreadFcall", 127 | target: &Fcall{ 128 | Type: Rread, 129 | Tag: 5556, 130 | Message: MessageRread{}, 131 | }, 132 | marshaled: []byte{ 133 | 0x75, 0xb4, 0x15, 134 | 0x0, 0x0, 0x0, 0x0}, 135 | }, 136 | { 137 | description: "EmptyTwriteFcall", 138 | target: &Fcall{ 139 | Type: Twrite, 140 | Tag: 5556, 141 | Message: MessageTwrite{}, 142 | }, 143 | marshaled: []byte{ 144 | byte(Twrite), 0xb4, 0x15, 145 | 0x0, 0x0, 0x0, 0x0, 146 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 147 | 0x0, 0x0, 0x0, 0x0}, 148 | }, 149 | { 150 | description: "RreadFcall", 151 | target: &Fcall{ 152 | Type: Rread, 153 | Tag: 5556, 154 | Message: MessageRread{ 155 | Data: []byte("a lot of byte data"), 156 | }, 157 | }, 158 | marshaled: []byte{ 159 | 0x75, 0xb4, 0x15, 160 | 0x12, 0x0, 0x0, 0x0, 161 | 0x61, 0x20, 0x6c, 0x6f, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x64, 0x61, 0x74, 0x61}, 162 | }, 163 | { 164 | description: "RstatFcall", 165 | target: &Fcall{ 166 | Type: Rstat, 167 | Tag: 5556, 168 | Message: MessageRstat{ 169 | Stat: Dir{ 170 | Type: ^uint16(0), 171 | Dev: ^uint32(0), 172 | Qid: Qid{ 173 | Type: QTDIR, 174 | Version: ^uint32(0), 175 | Path: ^uint64(0), 176 | }, 177 | Mode: DMDIR | DMREAD, 178 | AccessTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 179 | ModTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 180 | Length: ^uint64(0), 181 | Name: "somedir", 182 | UID: "uid", 183 | GID: "gid", 184 | MUID: "muid", 185 | }, 186 | }, 187 | }, 188 | marshaled: []byte{ 189 | 0x7d, 0xb4, 0x15, 190 | 0x42, 0x0, // TODO(stevvooe): Include Dir size. Not straightforward. 191 | 0x40, 0x0, // TODO(stevvooe): Include Dir size. Not straightforward. 192 | 0xff, 0xff, // type 193 | 0xff, 0xff, 0xff, 0xff, // dev 194 | 0x80, 0xff, 0xff, 0xff, 0xff, // qid.type, qid.version 195 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // qid.path 196 | 0x4, 0x0, 0x0, 0x80, // mode 197 | 0x25, 0x98, 0xb8, 0x43, // atime 198 | 0x25, 0x98, 0xb8, 0x43, // mtime 199 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // length 200 | 0x7, 0x0, 0x73, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 201 | 0x3, 0x0, 0x75, 0x69, 0x64, // uid 202 | 0x3, 0x0, 0x67, 0x69, 0x64, // gid 203 | 0x4, 0x0, 0x6d, 0x75, 0x69, 0x64}, // muid 204 | }, 205 | { 206 | description: "DirSlice", 207 | target: []Dir{ 208 | { 209 | Type: uint16(0), 210 | Dev: uint32(0), 211 | Qid: Qid{ 212 | Type: QTDIR, 213 | Version: uint32(0), 214 | Path: ^uint64(0), 215 | }, 216 | Mode: DMDIR | DMREAD, 217 | AccessTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 218 | ModTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 219 | Length: 0x88, 220 | Name: ".", 221 | UID: "501", 222 | GID: "20", 223 | MUID: "none", 224 | }, 225 | { 226 | Type: uint16(0), 227 | Dev: uint32(0), 228 | Qid: Qid{ 229 | Type: QTDIR, 230 | Version: uint32(0), 231 | Path: ^uint64(0), 232 | }, 233 | Mode: DMDIR | DMREAD, 234 | AccessTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 235 | ModTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 236 | Length: 0x63e, 237 | Name: "..", 238 | UID: "501", 239 | GID: "20", 240 | MUID: "none", 241 | }, 242 | { 243 | Type: uint16(0), 244 | Dev: uint32(0), 245 | Qid: Qid{ 246 | Type: QTDIR, 247 | Version: uint32(0), 248 | Path: ^uint64(0), 249 | }, 250 | Mode: DMDIR | DMREAD, 251 | AccessTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 252 | ModTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 253 | Length: 0x44, 254 | Name: "hello", 255 | UID: "501", 256 | GID: "20", 257 | MUID: "none", 258 | }, 259 | { 260 | Type: uint16(0), 261 | Dev: uint32(0), 262 | Qid: Qid{ 263 | Type: QTDIR, 264 | Version: uint32(0), 265 | Path: ^uint64(0), 266 | }, 267 | Mode: DMDIR | DMREAD, 268 | AccessTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 269 | ModTime: time.Date(2006, 01, 02, 03, 04, 05, 0, time.UTC), 270 | Length: 0x44, 271 | Name: "there", 272 | UID: "501", 273 | GID: "20", 274 | MUID: "none", 275 | }, 276 | }, 277 | marshaled: []byte{ 278 | 0x39, 0x0, // size 279 | 0x0, 0x0, // type 280 | 0x0, 0x0, 0x0, 0x0, // dev 281 | 0x80, // qid.type == QTDIR 282 | 0x0, 0x0, 0x0, 0x0, // qid.vers 283 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // qid.path 284 | 0x4, 0x0, 0x0, 0x80, // mode 285 | 0x25, 0x98, 0xb8, 0x43, // atime 286 | 0x25, 0x98, 0xb8, 0x43, // mtime 287 | 0x88, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // length 288 | 0x1, 0x0, 289 | 0x2e, // . 290 | 0x3, 0x0, 291 | 0x35, 0x30, 0x31, // 501 292 | 0x2, 0x0, 293 | 0x32, 0x30, // 20 294 | 0x4, 0x0, 295 | 0x6e, 0x6f, 0x6e, 0x65, // none 296 | 297 | 0x3a, 0x0, 298 | 0x0, 0x0, // type 299 | 0x0, 0x0, 0x0, 0x0, // dev 300 | 0x80, // qid.type == QTDIR 301 | 0x0, 0x0, 0x0, 0x0, // qid.vers 302 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // qid.path 303 | 0x4, 0x0, 0x0, 0x80, // mode 304 | 0x25, 0x98, 0xb8, 0x43, // atime 305 | 0x25, 0x98, 0xb8, 0x43, // mtime 306 | 0x3e, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // length 307 | 0x2, 0x0, 308 | 0x2e, 0x2e, // .. 309 | 0x3, 0x0, 310 | 0x35, 0x30, 0x31, // 501 311 | 0x2, 0x0, 312 | 0x32, 0x30, // 20 313 | 0x4, 0x0, 314 | 0x6e, 0x6f, 0x6e, 0x65, // none 315 | 316 | 0x3d, 0x0, 317 | 0x0, 0x0, // type 318 | 0x0, 0x0, 0x0, 0x0, // dev 319 | 0x80, // qid.type == QTDIR 320 | 0x0, 0x0, 0x0, 0x0, // qid.vers 321 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // qid.Path 322 | 0x4, 0x0, 0x0, 0x80, // mode 323 | 0x25, 0x98, 0xb8, 0x43, // atime 324 | 0x25, 0x98, 0xb8, 0x43, // mtime 325 | 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // length 326 | 0x5, 0x0, 327 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, // hello 328 | 0x3, 0x0, 329 | 0x35, 0x30, 0x31, // 501 330 | 0x2, 0x0, 331 | 0x32, 0x30, // 20 332 | 0x4, 0x0, 333 | 0x6e, 0x6f, 0x6e, 0x65, // none 334 | 335 | 0x3d, 0x0, 336 | 0x0, 0x0, // type 337 | 0x0, 0x0, 0x0, 0x0, // dev 338 | 0x80, // qid.type == QTDIR 339 | 0x0, 0x0, 0x0, 0x0, //qid.vers 340 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // qid.path 341 | 0x4, 0x0, 0x0, 0x80, // mode 342 | 0x25, 0x98, 0xb8, 0x43, // atime 343 | 0x25, 0x98, 0xb8, 0x43, // mtime 344 | 0x44, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // length 345 | 0x5, 0x0, 346 | 0x74, 0x68, 0x65, 0x72, 0x65, // there 347 | 0x3, 0x0, 348 | 0x35, 0x30, 0x31, // 501 349 | 0x2, 0x0, 350 | 0x32, 0x30, // 20 351 | 0x4, 0x0, 352 | 0x6e, 0x6f, 0x6e, 0x65, // none 353 | }, 354 | }, 355 | { 356 | description: "RerrorFcall", 357 | target: newErrorFcall(5556, errors.New("A serious error")), 358 | marshaled: []byte{ 359 | 0x6b, // Rerror 360 | 0xb4, 0x15, // Tag 361 | 0xf, 0x0, // String size. 362 | 0x41, 0x20, 0x73, 0x65, 0x72, 0x69, 0x6f, 0x75, 0x73, 0x20, 0x65, 0x72, 0x72, 0x6f, 0x72}, 363 | }, 364 | } { 365 | 366 | t.Run(testcase.description, func(t *testing.T) { 367 | p, err := codec.Marshal(testcase.target) 368 | if err != nil { 369 | t.Fatalf("error writing fcall: %v", err) 370 | } 371 | 372 | if !bytes.Equal(p, testcase.marshaled) { 373 | t.Fatalf("unexpected bytes for fcall: \n%#v != \n%#v", p, testcase.marshaled) 374 | } 375 | 376 | if size9p(testcase.target) == 0 { 377 | t.Fatalf("size of target should never be zero") 378 | } 379 | 380 | // check that size9p is working correctly 381 | if int(size9p(testcase.target)) != len(testcase.marshaled) { 382 | t.Fatalf("size not correct: %v != %v", int(size9p(testcase.target)), len(testcase.marshaled)) 383 | } 384 | 385 | var v interface{} 386 | targetType := reflect.TypeOf(testcase.target) 387 | 388 | if targetType.Kind() == reflect.Ptr { 389 | v = reflect.New(targetType.Elem()).Interface() 390 | } else { 391 | v = reflect.New(targetType).Interface() 392 | } 393 | 394 | if err := codec.Unmarshal(p, v); err != nil { 395 | t.Fatalf("error reading: %v", err) 396 | } 397 | 398 | if targetType.Kind() != reflect.Ptr { 399 | v = reflect.Indirect(reflect.ValueOf(v)).Interface() 400 | } 401 | 402 | if !reflect.DeepEqual(v, testcase.target) { 403 | t.Fatalf("not equal: %v != %v (\n%#v\n%#v\n)", 404 | v, testcase.target, 405 | v, testcase.target) 406 | } 407 | }) 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // MessageRerror provides both a Go error type and message type. 9 | type MessageRerror struct { 10 | Ename string 11 | } 12 | 13 | // 9p wire errors returned by Session interface methods 14 | var ( 15 | ErrBadattach = new9pError("unknown specifier in attach") 16 | ErrBadoffset = new9pError("bad offset") 17 | ErrBadcount = new9pError("bad count") 18 | ErrBotch = new9pError("9P protocol botch") 19 | ErrCreatenondir = new9pError("create in non-directory") 20 | ErrDupfid = new9pError("duplicate fid") 21 | ErrDuptag = new9pError("duplicate tag") 22 | ErrIsdir = new9pError("is a directory") 23 | ErrNocreate = new9pError("create prohibited") 24 | ErrNomem = new9pError("out of memory") 25 | ErrNoremove = new9pError("remove prohibited") 26 | ErrNostat = new9pError("stat prohibited") 27 | ErrNotfound = new9pError("file not found") 28 | ErrNowrite = new9pError("write prohibited") 29 | ErrNowstat = new9pError("wstat prohibited") 30 | ErrPerm = new9pError("permission denied") 31 | ErrUnknownfid = new9pError("unknown fid") 32 | ErrBaddir = new9pError("bad directory in wstat") 33 | ErrWalknodir = new9pError("walk in non-directory") 34 | 35 | // extra errors not part of the normal protocol 36 | 37 | ErrTimeout = new9pError("fcall timeout") // returned when timing out on the fcall 38 | ErrUnknownTag = new9pError("unknown tag") 39 | ErrUnknownMsg = new9pError("unknown message") // returned when encountering unknown message type 40 | ErrUnexpectedMsg = new9pError("unexpected message") // returned when an unexpected message is encountered 41 | ErrWalkLimit = new9pError("too many wnames in walk") 42 | ErrClosed = errors.New("closed") 43 | ) 44 | 45 | // new9pError returns a new 9p error ready for the wire. 46 | func new9pError(s string) error { 47 | return MessageRerror{Ename: s} 48 | } 49 | 50 | // Type ensures that 9p errors can be transparently used as a 9p message in an 51 | // Fcall. 52 | func (MessageRerror) Type() FcallType { 53 | return Rerror 54 | } 55 | 56 | func (e MessageRerror) Error() string { 57 | return fmt.Sprintf("9p: %v", e.Ename) 58 | } 59 | -------------------------------------------------------------------------------- /fcall.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import "fmt" 4 | 5 | // FcallType encodes the message type for the target Fcall. 6 | type FcallType uint8 7 | 8 | // Definitions for Fcall's used in 9P2000. 9 | const ( 10 | Tversion FcallType = iota + 100 11 | Rversion 12 | Tauth 13 | Rauth 14 | Tattach 15 | Rattach 16 | Terror 17 | Rerror 18 | Tflush 19 | Rflush 20 | Twalk 21 | Rwalk 22 | Topen 23 | Ropen 24 | Tcreate 25 | Rcreate 26 | Tread 27 | Rread 28 | Twrite 29 | Rwrite 30 | Tclunk 31 | Rclunk 32 | Tremove 33 | Rremove 34 | Tstat 35 | Rstat 36 | Twstat 37 | Rwstat 38 | Tmax 39 | ) 40 | 41 | func (fct FcallType) String() string { 42 | switch fct { 43 | case Tversion: 44 | return "Tversion" 45 | case Rversion: 46 | return "Rversion" 47 | case Tauth: 48 | return "Tauth" 49 | case Rauth: 50 | return "Rauth" 51 | case Tattach: 52 | return "Tattach" 53 | case Rattach: 54 | return "Rattach" 55 | case Terror: 56 | // invalid. 57 | return "Terror" 58 | case Rerror: 59 | return "Rerror" 60 | case Tflush: 61 | return "Tflush" 62 | case Rflush: 63 | return "Rflush" 64 | case Twalk: 65 | return "Twalk" 66 | case Rwalk: 67 | return "Rwalk" 68 | case Topen: 69 | return "Topen" 70 | case Ropen: 71 | return "Ropen" 72 | case Tcreate: 73 | return "Tcreate" 74 | case Rcreate: 75 | return "Rcreate" 76 | case Tread: 77 | return "Tread" 78 | case Rread: 79 | return "Rread" 80 | case Twrite: 81 | return "Twrite" 82 | case Rwrite: 83 | return "Rwrite" 84 | case Tclunk: 85 | return "Tclunk" 86 | case Rclunk: 87 | return "Rclunk" 88 | case Tremove: 89 | return "Tremove" 90 | case Rremove: 91 | return "Rremove" 92 | case Tstat: 93 | return "Tstat" 94 | case Rstat: 95 | return "Rstat" 96 | case Twstat: 97 | return "Twstat" 98 | case Rwstat: 99 | return "Rwstat" 100 | default: 101 | return "Tunknown" 102 | } 103 | } 104 | 105 | // Fcall defines the fields for sending a 9p formatted message. The type will 106 | // be introspected from the Message implementation. 107 | type Fcall struct { 108 | Type FcallType 109 | Tag Tag 110 | Message Message 111 | } 112 | 113 | func newFcall(tag Tag, msg Message) *Fcall { 114 | return &Fcall{ 115 | Type: msg.Type(), 116 | Tag: tag, 117 | Message: msg, 118 | } 119 | } 120 | 121 | func newErrorFcall(tag Tag, err error) *Fcall { 122 | var msg Message 123 | 124 | switch v := err.(type) { 125 | case MessageRerror: 126 | msg = v 127 | case *MessageRerror: 128 | msg = *v 129 | default: 130 | msg = MessageRerror{Ename: v.Error()} 131 | } 132 | 133 | return &Fcall{ 134 | Type: Rerror, 135 | Tag: tag, 136 | Message: msg, 137 | } 138 | } 139 | 140 | func (fc *Fcall) String() string { 141 | return fmt.Sprintf("%v(%v) %v", fc.Type, fc.Tag, string9p(fc.Message)) 142 | } 143 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package p9p 4 | 5 | import ( 6 | "log" 7 | "os" 8 | ) 9 | 10 | type logging struct { 11 | session Session 12 | logger log.Logger 13 | } 14 | 15 | var _ Session = &logging{} 16 | 17 | func NewLogger(prefix string, session Session) Session { 18 | return &logging{ 19 | session: session, 20 | logger: *log.New(os.Stdout, prefix, 0), 21 | } 22 | } 23 | 24 | func (l *logging) Auth(afid Fid, uname, aname string) (Qid, error) { 25 | qid, err := l.session.Auth(afid, uname, aname) 26 | l.logger.Printf("Auth(%v, %s, %s) -> (%v, %v)", afid, uname, aname, qid, err) 27 | return qid, err 28 | } 29 | 30 | func (l *logging) Attach(fid, afid Fid, uname, aname string) (Qid, error) { 31 | qid, err := l.session.Attach(fid, afid, uname, aname) 32 | l.logger.Printf("Attach(%v, %v, %s, %s) -> (%v, %v)", fid, afid, uname, aname, qid, err) 33 | return qid, err 34 | } 35 | 36 | func (l *logging) Clunk(fid Fid) error { 37 | return l.session.Clunk(fid) 38 | } 39 | 40 | func (l *logging) Remove(fid Fid) (err error) { 41 | defer func() { 42 | l.logger.Printf("Remove(%v) -> %v", fid, err) 43 | }() 44 | return l.session.Remove(fid) 45 | } 46 | 47 | func (l *logging) Walk(fid Fid, newfid Fid, names ...string) ([]Qid, error) { 48 | return l.session.Walk(fid, newfid, names...) 49 | } 50 | 51 | func (l *logging) Read(fid Fid, p []byte, offset int64) (n int, err error) { 52 | return l.session.Read(fid, p, offset) 53 | } 54 | 55 | func (l *logging) Write(fid Fid, p []byte, offset int64) (n int, err error) { 56 | return l.session.Write(fid, p, offset) 57 | } 58 | 59 | func (l *logging) Open(fid Fid, mode int32) (Qid, error) { 60 | return l.session.Open(fid, mode) 61 | } 62 | 63 | func (l *logging) Create(parent Fid, name string, perm uint32, mode uint32) (Qid, error) { 64 | return l.session.Create(parent, name, perm, mode) 65 | } 66 | 67 | func (l *logging) Stat(fid Fid) (Dir, error) { 68 | return l.session.Stat(fid) 69 | } 70 | 71 | func (l *logging) WStat(fid Fid, dir Dir) error { 72 | return l.session.WStat(fid, dir) 73 | } 74 | 75 | func (l *logging) Version(msize int32, version string) (int32, string, error) { 76 | return l.session.Version(msize, version) 77 | } 78 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import "fmt" 4 | 5 | // Message represents the target of an fcall. 6 | type Message interface { 7 | // Type returns the type of call for the target message. 8 | Type() FcallType 9 | } 10 | 11 | // newMessage returns a new instance of the message based on the Fcall type. 12 | func newMessage(typ FcallType) (Message, error) { 13 | switch typ { 14 | case Tversion: 15 | return MessageTversion{}, nil 16 | case Rversion: 17 | return MessageRversion{}, nil 18 | case Tauth: 19 | return MessageTauth{}, nil 20 | case Rauth: 21 | return MessageRauth{}, nil 22 | case Tattach: 23 | return MessageTattach{}, nil 24 | case Rattach: 25 | return MessageRattach{}, nil 26 | case Rerror: 27 | return MessageRerror{}, nil 28 | case Tflush: 29 | return MessageTflush{}, nil 30 | case Rflush: 31 | return MessageRflush{}, nil // No message body for this response. 32 | case Twalk: 33 | return MessageTwalk{}, nil 34 | case Rwalk: 35 | return MessageRwalk{}, nil 36 | case Topen: 37 | return MessageTopen{}, nil 38 | case Ropen: 39 | return MessageRopen{}, nil 40 | case Tcreate: 41 | return MessageTcreate{}, nil 42 | case Rcreate: 43 | return MessageRcreate{}, nil 44 | case Tread: 45 | return MessageTread{}, nil 46 | case Rread: 47 | return MessageRread{}, nil 48 | case Twrite: 49 | return MessageTwrite{}, nil 50 | case Rwrite: 51 | return MessageRwrite{}, nil 52 | case Tclunk: 53 | return MessageTclunk{}, nil 54 | case Rclunk: 55 | return MessageRclunk{}, nil // no response body 56 | case Tremove: 57 | return MessageTremove{}, nil 58 | case Rremove: 59 | return MessageRremove{}, nil 60 | case Tstat: 61 | return MessageTstat{}, nil 62 | case Rstat: 63 | return MessageRstat{}, nil 64 | case Twstat: 65 | return MessageTwstat{}, nil 66 | case Rwstat: 67 | return MessageRwstat{}, nil 68 | } 69 | 70 | return nil, fmt.Errorf("unknown message type") 71 | } 72 | 73 | // MessageVersion encodes the message body for Tversion and Rversion RPC 74 | // calls. The body is identical in both directions. 75 | type MessageTversion struct { 76 | MSize uint32 77 | Version string 78 | } 79 | 80 | type MessageRversion struct { 81 | MSize uint32 82 | Version string 83 | } 84 | 85 | type MessageTauth struct { 86 | Afid Fid 87 | Uname string 88 | Aname string 89 | } 90 | 91 | type MessageRauth struct { 92 | Qid Qid 93 | } 94 | 95 | type MessageTflush struct { 96 | Oldtag Tag 97 | } 98 | 99 | type MessageRflush struct{} 100 | 101 | type MessageTattach struct { 102 | Fid Fid 103 | Afid Fid 104 | Uname string 105 | Aname string 106 | } 107 | 108 | type MessageRattach struct { 109 | Qid Qid 110 | } 111 | 112 | type MessageTwalk struct { 113 | Fid Fid 114 | Newfid Fid 115 | Wnames []string 116 | } 117 | 118 | type MessageRwalk struct { 119 | Qids []Qid 120 | } 121 | 122 | type MessageTopen struct { 123 | Fid Fid 124 | Mode Flag 125 | } 126 | 127 | type MessageRopen struct { 128 | Qid Qid 129 | IOUnit uint32 130 | } 131 | 132 | type MessageTcreate struct { 133 | Fid Fid 134 | Name string 135 | Perm uint32 136 | Mode Flag 137 | } 138 | 139 | type MessageRcreate struct { 140 | Qid Qid 141 | IOUnit uint32 142 | } 143 | 144 | type MessageTread struct { 145 | Fid Fid 146 | Offset uint64 147 | Count uint32 148 | } 149 | 150 | type MessageRread struct { 151 | Data []byte 152 | } 153 | 154 | type MessageTwrite struct { 155 | Fid Fid 156 | Offset uint64 157 | Data []byte 158 | } 159 | 160 | type MessageRwrite struct { 161 | Count uint32 162 | } 163 | 164 | type MessageTclunk struct { 165 | Fid Fid 166 | } 167 | 168 | type MessageRclunk struct{} 169 | 170 | type MessageTremove struct { 171 | Fid Fid 172 | } 173 | 174 | type MessageRremove struct{} 175 | 176 | type MessageTstat struct { 177 | Fid Fid 178 | } 179 | 180 | type MessageRstat struct { 181 | Stat Dir 182 | } 183 | 184 | type MessageTwstat struct { 185 | Fid Fid 186 | Stat Dir 187 | } 188 | 189 | type MessageRwstat struct{} 190 | 191 | func (MessageTversion) Type() FcallType { return Tversion } 192 | func (MessageRversion) Type() FcallType { return Rversion } 193 | func (MessageTauth) Type() FcallType { return Tauth } 194 | func (MessageRauth) Type() FcallType { return Rauth } 195 | func (MessageTflush) Type() FcallType { return Tflush } 196 | func (MessageRflush) Type() FcallType { return Rflush } 197 | func (MessageTattach) Type() FcallType { return Tattach } 198 | func (MessageRattach) Type() FcallType { return Rattach } 199 | func (MessageTwalk) Type() FcallType { return Twalk } 200 | func (MessageRwalk) Type() FcallType { return Rwalk } 201 | func (MessageTopen) Type() FcallType { return Topen } 202 | func (MessageRopen) Type() FcallType { return Ropen } 203 | func (MessageTcreate) Type() FcallType { return Tcreate } 204 | func (MessageRcreate) Type() FcallType { return Rcreate } 205 | func (MessageTread) Type() FcallType { return Tread } 206 | func (MessageRread) Type() FcallType { return Rread } 207 | func (MessageTwrite) Type() FcallType { return Twrite } 208 | func (MessageRwrite) Type() FcallType { return Rwrite } 209 | func (MessageTclunk) Type() FcallType { return Tclunk } 210 | func (MessageRclunk) Type() FcallType { return Rclunk } 211 | func (MessageTremove) Type() FcallType { return Tremove } 212 | func (MessageRremove) Type() FcallType { return Rremove } 213 | func (MessageTstat) Type() FcallType { return Tstat } 214 | func (MessageRstat) Type() FcallType { return Rstat } 215 | func (MessageTwstat) Type() FcallType { return Twstat } 216 | func (MessageRwstat) Type() FcallType { return Rwstat } 217 | -------------------------------------------------------------------------------- /overflow.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import "fmt" 4 | 5 | // Overflow will return a positive number, indicating there was an overflow for 6 | // the error. 7 | func Overflow(err error) int { 8 | if of, ok := err.(overflow); ok { 9 | return of.Size() 10 | } 11 | 12 | // traverse cause, if above fails. 13 | if causal, ok := err.(interface { 14 | Cause() error 15 | }); ok { 16 | return Overflow(causal.Cause()) 17 | } 18 | 19 | return 0 20 | } 21 | 22 | // overflow is a resolvable error type that can help callers negotiate 23 | // session msize. If this error is encountered, no message was sent. 24 | // 25 | // The return value of `Size()` represents the number of bytes that would have 26 | // been truncated if the message were sent. This IS NOT the optimal buffer size 27 | // for operations like read and write. 28 | // 29 | // In the case of `Twrite`, the caller can Size() from the local size to get an 30 | // optimally size buffer or the write can simply be truncated to `len(buf) - 31 | // err.Size()`. 32 | // 33 | // For the most part, no users of this package should see this error in 34 | // practice. If this escapes the Session interface, it is a bug. 35 | type overflow interface { 36 | Size() int // number of bytes overflowed. 37 | } 38 | 39 | type overflowErr struct { 40 | size int // number of bytes overflowed 41 | } 42 | 43 | func (o overflowErr) Error() string { 44 | return fmt.Sprintf("message overflowed %d bytes", o.size) 45 | } 46 | 47 | func (o overflowErr) Size() int { 48 | return o.size 49 | } 50 | -------------------------------------------------------------------------------- /readdir.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "io" 5 | 6 | "context" 7 | ) 8 | 9 | // ReaddirAll reads all the directory entries for the resource fid. 10 | func ReaddirAll(session Session, fid Fid) ([]Dir, error) { 11 | panic("not implemented") 12 | } 13 | 14 | // Readdir helps one to implement the server-side of Session.Read on 15 | // directories. 16 | type Readdir struct { 17 | nextfn func() (Dir, error) 18 | buf *Dir // one-item buffer 19 | codec Codec 20 | offset int64 21 | } 22 | 23 | // NewReaddir returns a new Readdir to assist implementing server-side Readdir. 24 | // The codec will be used to decode messages with Dir entries. The provided 25 | // function next will be called until io.EOF is returned. 26 | func NewReaddir(codec Codec, next func() (Dir, error)) *Readdir { 27 | return &Readdir{ 28 | nextfn: next, 29 | codec: codec, 30 | } 31 | } 32 | 33 | // NewFixedReaddir returns a Readdir that will returned a fixed set of 34 | // directory entries. 35 | func NewFixedReaddir(codec Codec, dir []Dir) *Readdir { 36 | dirs := make([]Dir, len(dir)) 37 | copy(dirs, dir) // make our own copy! 38 | 39 | return NewReaddir(codec, 40 | func() (Dir, error) { 41 | if len(dirs) == 0 { 42 | return Dir{}, io.EOF 43 | } 44 | 45 | d := dirs[0] 46 | dirs = dirs[1:] 47 | return d, nil 48 | }) 49 | } 50 | 51 | func (rd *Readdir) Read(ctx context.Context, p []byte, offset int64) (n int, err error) { 52 | if rd.offset != offset { 53 | return 0, ErrBadoffset 54 | } 55 | 56 | p = p[:0:len(p)] 57 | for len(p) < cap(p) { 58 | var d Dir 59 | if rd.buf != nil { 60 | d = *rd.buf 61 | rd.buf = nil 62 | } else { 63 | d, err = rd.nextfn() 64 | if err != nil { 65 | goto done 66 | } 67 | } 68 | 69 | var dp []byte 70 | dp, err = rd.codec.Marshal(d) 71 | if err != nil { 72 | goto done 73 | } 74 | 75 | if len(p)+len(dp) > cap(p) { 76 | // will over fill buffer. save item and exit. 77 | rd.buf = &d 78 | goto done 79 | } 80 | 81 | p = append(p, dp...) 82 | } 83 | 84 | done: 85 | if err == io.EOF { 86 | // Don't let io.EOF escape. EOF is indicated by a zero-length result 87 | // with no error. 88 | err = nil 89 | } 90 | 91 | rd.offset += int64(len(p)) 92 | return len(p), err 93 | } 94 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "context" 11 | ) 12 | 13 | // TODO(stevvooe): Add net/http.Server-like type here to manage connections. 14 | // Coupled with Handler mux, we can get a very http-like experience for 9p 15 | // servers. 16 | 17 | // ServeConn the 9p handler over the provided network connection. 18 | func ServeConn(ctx context.Context, cn net.Conn, handler Handler) error { 19 | 20 | // TODO(stevvooe): It would be nice if the handler could declare the 21 | // supported version. Before we had handler, we used the session to get 22 | // the version (msize, version := session.Version()). We must decided if 23 | // we want to proxy version and message size decisions all the back to the 24 | // origin server or make those decisions at each link of a proxy chain. 25 | 26 | ch := newChannel(cn, codec9p{}, DefaultMSize) 27 | negctx, cancel := context.WithTimeout(ctx, 1*time.Second) 28 | defer cancel() 29 | 30 | // TODO(stevvooe): For now, we negotiate here. It probably makes sense to 31 | // do this outside of this function and then pass in a ready made channel. 32 | // We are not really ready to export the channel type yet. 33 | 34 | if err := servernegotiate(negctx, ch, DefaultVersion); err != nil { 35 | // TODO(stevvooe): Need better error handling and retry support here. 36 | return fmt.Errorf("error negotiating version: %s", err) 37 | } 38 | 39 | ctx = withVersion(ctx, DefaultVersion) 40 | 41 | c := &conn{ 42 | ctx: ctx, 43 | ch: ch, 44 | handler: handler, 45 | closed: make(chan struct{}), 46 | } 47 | 48 | return c.serve() 49 | } 50 | 51 | // conn plays role of session dispatch for handler in a server. 52 | type conn struct { 53 | ctx context.Context 54 | session Session 55 | ch Channel 56 | handler Handler 57 | 58 | once sync.Once 59 | closed chan struct{} 60 | err error // terminal error for the conn 61 | } 62 | 63 | // activeRequest includes information about the active request. 64 | type activeRequest struct { 65 | ctx context.Context 66 | request *Fcall 67 | cancel context.CancelFunc 68 | } 69 | 70 | // serve messages on the connection until an error is encountered. 71 | func (c *conn) serve() error { 72 | tags := map[Tag]*activeRequest{} // active requests 73 | 74 | requests := make(chan *Fcall) // sync, read-limited 75 | responses := make(chan *Fcall) // sync, goroutine consumed 76 | completed := make(chan *Fcall) // sync, send in goroutine per request 77 | 78 | // read loop 79 | go c.read(requests) 80 | go c.write(responses) 81 | 82 | for { 83 | select { 84 | case req := <-requests: 85 | if _, ok := tags[req.Tag]; ok { 86 | select { 87 | case responses <- newErrorFcall(req.Tag, ErrDuptag): 88 | // Send to responses, bypass tag management. 89 | case <-c.ctx.Done(): 90 | return c.ctx.Err() 91 | case <-c.closed: 92 | return c.err 93 | } 94 | continue 95 | } 96 | 97 | switch msg := req.Message.(type) { 98 | case MessageTflush: 99 | var resp *Fcall 100 | // check if we have actually know about the requested flush 101 | active, ok := tags[msg.Oldtag] 102 | if ok { 103 | active.cancel() // propagate cancellation to callees 104 | delete(tags, msg.Oldtag) 105 | resp = newFcall(req.Tag, MessageRflush{}) 106 | } else { 107 | resp = newErrorFcall(req.Tag, ErrUnknownTag) 108 | } 109 | 110 | select { 111 | case responses <- resp: 112 | // bypass tag management in completed. 113 | case <-c.ctx.Done(): 114 | return c.ctx.Err() 115 | case <-c.closed: 116 | return c.err 117 | } 118 | default: 119 | // Allows us to session handlers to cancel processing of the fcall 120 | // through context. 121 | ctx, cancel := context.WithCancel(c.ctx) 122 | 123 | // The contents of these instances are only writable in the main 124 | // server loop. The value of tag will not change. 125 | tags[req.Tag] = &activeRequest{ 126 | ctx: ctx, 127 | request: req, 128 | cancel: cancel, 129 | } 130 | 131 | go func(ctx context.Context, req *Fcall) { 132 | // TODO(stevvooe): Re-write incoming Treads so that handler 133 | // can always respond with a message of the correct msize. 134 | 135 | var resp *Fcall 136 | msg, err := c.handler.Handle(ctx, req.Message) 137 | if err != nil { 138 | // all handler errors are forwarded as protocol errors. 139 | resp = newErrorFcall(req.Tag, err) 140 | } else { 141 | resp = newFcall(req.Tag, msg) 142 | } 143 | 144 | select { 145 | case completed <- resp: 146 | case <-ctx.Done(): 147 | return 148 | case <-c.closed: 149 | return 150 | } 151 | }(ctx, req) 152 | } 153 | case resp := <-completed: 154 | // only responses that flip the tag state traverse this section. 155 | active, ok := tags[resp.Tag] 156 | if !ok { 157 | // The tag is no longer active. Likely a flushed message. 158 | continue 159 | } 160 | 161 | select { 162 | case responses <- resp: 163 | case <-active.ctx.Done(): 164 | // the context was canceled for some reason, perhaps timeout or 165 | // due to a flush call. We treat this as a condition where a 166 | // response should not be sent. 167 | } 168 | delete(tags, resp.Tag) 169 | case <-c.ctx.Done(): 170 | return c.ctx.Err() 171 | case <-c.closed: 172 | return c.err 173 | } 174 | } 175 | } 176 | 177 | // read takes requests off the channel and sends them on requests. 178 | func (c *conn) read(requests chan *Fcall) { 179 | for { 180 | req := new(Fcall) 181 | if err := c.ch.ReadFcall(c.ctx, req); err != nil { 182 | if err, ok := err.(net.Error); ok { 183 | if err.Timeout() || err.Temporary() { 184 | // TODO(stevvooe): A full idle timeout on the connection 185 | // should be enforced here. No logging because it is quite 186 | // chatty. 187 | continue 188 | } 189 | } 190 | 191 | c.CloseWithError(fmt.Errorf("error reading fcall: %v", err)) 192 | return 193 | } 194 | 195 | select { 196 | case requests <- req: 197 | case <-c.ctx.Done(): 198 | c.CloseWithError(c.ctx.Err()) 199 | return 200 | case <-c.closed: 201 | return 202 | } 203 | } 204 | } 205 | 206 | func (c *conn) write(responses chan *Fcall) { 207 | for { 208 | select { 209 | case resp := <-responses: 210 | // TODO(stevvooe): Correctly protect againt overflowing msize from 211 | // handler. This can be done above, in the main message handler 212 | // loop, by adjusting incoming Tread calls to have a Count that 213 | // won't overflow the msize. 214 | 215 | if err := c.ch.WriteFcall(c.ctx, resp); err != nil { 216 | if err, ok := err.(net.Error); ok { 217 | if err.Timeout() || err.Temporary() { 218 | // TODO(stevvooe): A full idle timeout on the 219 | // connection should be enforced here. We log here, 220 | // since this is less common. 221 | log.Printf("p9p: temporary error writing fcall: %v", err) 222 | continue 223 | } 224 | } 225 | 226 | c.CloseWithError(fmt.Errorf("error writing fcall: %v", err)) 227 | return 228 | } 229 | case <-c.ctx.Done(): 230 | c.CloseWithError(c.ctx.Err()) 231 | return 232 | case <-c.closed: 233 | return 234 | } 235 | } 236 | } 237 | 238 | func (c *conn) Close() error { 239 | return c.CloseWithError(nil) 240 | } 241 | 242 | func (c *conn) CloseWithError(err error) error { 243 | c.once.Do(func() { 244 | if err == nil { 245 | err = ErrClosed 246 | } 247 | 248 | c.err = err 249 | close(c.closed) 250 | }) 251 | 252 | return c.err 253 | } 254 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import "context" 4 | 5 | // Session provides the central abstraction for a 9p connection. Clients 6 | // implement sessions and servers serve sessions. Sessions can be proxied by 7 | // serving up a client session. 8 | // 9 | // The interface is also wired up with full context support to manage timeouts 10 | // and resource clean up. 11 | // 12 | // Session represents the operations covered in section 5 of the plan 9 manual 13 | // (http://man.cat-v.org/plan_9/5/). Requests are managed internally, so the 14 | // Flush method is handled by the internal implementation. Consider preceeding 15 | // these all with context to control request timeout. 16 | type Session interface { 17 | Auth(ctx context.Context, afid Fid, uname, aname string) (Qid, error) 18 | Attach(ctx context.Context, fid, afid Fid, uname, aname string) (Qid, error) 19 | Clunk(ctx context.Context, fid Fid) error 20 | Remove(ctx context.Context, fid Fid) error 21 | Walk(ctx context.Context, fid Fid, newfid Fid, names ...string) ([]Qid, error) 22 | 23 | // Read follows the semantics of io.ReaderAt.ReadAtt method except it takes 24 | // a contxt and Fid. 25 | Read(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) 26 | 27 | // Write follows the semantics of io.WriterAt.WriteAt except takes a context and an Fid. 28 | // 29 | // If n == len(p), no error is returned. 30 | // If n < len(p), io.ErrShortWrite will be returned. 31 | Write(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error) 32 | 33 | Open(ctx context.Context, fid Fid, mode Flag) (Qid, uint32, error) 34 | Create(ctx context.Context, parent Fid, name string, perm uint32, mode Flag) (Qid, uint32, error) 35 | Stat(ctx context.Context, fid Fid) (Dir, error) 36 | WStat(ctx context.Context, fid Fid, dir Dir) error 37 | 38 | // Version returns the supported version and msize of the session. This 39 | // can be affected by negotiating or the level of support provided by the 40 | // session implementation. 41 | Version() (msize int, version string) 42 | } 43 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "sync" 9 | 10 | "context" 11 | ) 12 | 13 | // roundTripper manages the request and response from the client-side. A 14 | // roundTripper must abide by similar rules to the http.RoundTripper. 15 | // Typically, the roundTripper will manage tag assignment and message 16 | // serialization. 17 | type roundTripper interface { 18 | send(ctx context.Context, msg Message) (Message, error) 19 | } 20 | 21 | // transport plays the role of being a client channel manager. It multiplexes 22 | // function calls onto the wire and dispatches responses to blocking calls to 23 | // send. On the whole, transport is thread-safe for calling send 24 | type transport struct { 25 | ctx context.Context 26 | ch Channel 27 | requests chan *fcallRequest 28 | 29 | shutdown chan struct{} 30 | once sync.Once // protect closure of shutdown 31 | closed chan struct{} 32 | 33 | tags uint16 34 | } 35 | 36 | var _ roundTripper = &transport{} 37 | 38 | func newTransport(ctx context.Context, ch Channel) roundTripper { 39 | t := &transport{ 40 | ctx: ctx, 41 | ch: ch, 42 | requests: make(chan *fcallRequest), 43 | shutdown: make(chan struct{}), 44 | closed: make(chan struct{}), 45 | } 46 | 47 | go t.handle() 48 | 49 | return t 50 | } 51 | 52 | // fcallRequest encompasses the request to send a message via fcall. 53 | type fcallRequest struct { 54 | ctx context.Context 55 | message Message 56 | response chan *Fcall 57 | err chan error 58 | } 59 | 60 | func newFcallRequest(ctx context.Context, msg Message) *fcallRequest { 61 | return &fcallRequest{ 62 | ctx: ctx, 63 | message: msg, 64 | response: make(chan *Fcall, 1), 65 | err: make(chan error, 1), 66 | } 67 | } 68 | 69 | func (t *transport) send(ctx context.Context, msg Message) (Message, error) { 70 | req := newFcallRequest(ctx, msg) 71 | 72 | // dispatch the request. 73 | select { 74 | case <-t.closed: 75 | return nil, ErrClosed 76 | case <-ctx.Done(): 77 | return nil, ctx.Err() 78 | case t.requests <- req: 79 | } 80 | 81 | // wait for the response. 82 | select { 83 | case <-t.closed: 84 | return nil, ErrClosed 85 | case <-ctx.Done(): 86 | return nil, ctx.Err() 87 | case err := <-req.err: 88 | return nil, err 89 | case resp := <-req.response: 90 | if resp.Type == Rerror { 91 | // pack the error into something useful 92 | respmesg, ok := resp.Message.(MessageRerror) 93 | if !ok { 94 | return nil, fmt.Errorf("invalid error response: %v", resp) 95 | } 96 | 97 | return nil, respmesg 98 | } 99 | 100 | return resp.Message, nil 101 | } 102 | } 103 | 104 | // allocateTag returns a valid tag given a tag pool map. It receives a hint as 105 | // to where to start the tag search. It returns an error if the allocation is 106 | // not possible. The provided map must not contain NOTAG as a key. 107 | func allocateTag(r *fcallRequest, m map[Tag]*fcallRequest, hint Tag) (Tag, error) { 108 | // The tag pool is depleted if 65535 (0xFFFF) tags are taken. 109 | if len(m) >= 0xFFFF { 110 | return 0, errors.New("tag pool depleted") 111 | } 112 | 113 | // Look for the first tag that doesn't exist in the map and return it. 114 | for i := 0; i < 0xFFFF; i++ { 115 | hint++ 116 | if hint == NOTAG { 117 | hint = 0 118 | } 119 | 120 | if _, exists := m[hint]; !exists { 121 | return hint, nil 122 | } 123 | } 124 | 125 | return 0, errors.New("allocateTag: unexpected error") 126 | } 127 | 128 | // handle takes messages off the wire and wakes up the waiting tag call. 129 | func (t *transport) handle() { 130 | defer func() { 131 | close(t.closed) 132 | }() 133 | 134 | // the following variable block are protected components owned by this thread. 135 | var ( 136 | responses = make(chan *Fcall) 137 | // outstanding provides a map of tags to outstanding requests. 138 | outstanding = map[Tag]*fcallRequest{} 139 | selected Tag 140 | ) 141 | 142 | // loop to read messages off of the connection 143 | go func() { 144 | defer func() { 145 | t.close() // single main loop 146 | }() 147 | loop: 148 | for { 149 | fcall := new(Fcall) 150 | if err := t.ch.ReadFcall(t.ctx, fcall); err != nil { 151 | switch err := err.(type) { 152 | case net.Error: 153 | if err.Timeout() || err.Temporary() { 154 | // BUG(stevvooe): There may be partial reads under 155 | // timeout errors where this is actually fatal. 156 | 157 | // can only retry if we haven't offset the frame. 158 | continue loop 159 | } 160 | } 161 | 162 | log.Println("p9p: fatal error reading msg:", err) 163 | return 164 | } 165 | 166 | select { 167 | case <-t.ctx.Done(): 168 | return 169 | case <-t.closed: 170 | return 171 | case responses <- fcall: 172 | } 173 | } 174 | }() 175 | 176 | for { 177 | select { 178 | case req := <-t.requests: 179 | var err error 180 | 181 | selected, err = allocateTag(req, outstanding, selected) 182 | if err != nil { 183 | req.err <- err 184 | continue 185 | } 186 | 187 | outstanding[selected] = req 188 | fcall := newFcall(selected, req.message) 189 | 190 | // TODO(stevvooe): Consider the case of requests that never 191 | // receive a response. We need to remove the fcall context from 192 | // the tag map and dealloc the tag. We may also want to send a 193 | // flush for the tag. 194 | if err := t.ch.WriteFcall(req.ctx, fcall); err != nil { 195 | delete(outstanding, fcall.Tag) 196 | req.err <- err 197 | } 198 | case b := <-responses: 199 | req, ok := outstanding[b.Tag] 200 | if !ok { 201 | // BUG(stevvooe): The exact handling of an unknown tag is 202 | // unclear at this point. These may not necessarily fatal to 203 | // the session, since they could be messages that the client no 204 | // longer cares for. When we figure this out, replace this 205 | // panic with something more sensible. 206 | panic(fmt.Sprintf("unknown tag received: %v", b)) 207 | } 208 | 209 | // BUG(stevvooe): Must detect duplicate tag and ensure that we are 210 | // waking up the right caller. If a duplicate is received, the 211 | // entry should not be deleted. 212 | delete(outstanding, b.Tag) 213 | 214 | req.response <- b 215 | 216 | // TODO(stevvooe): Reclaim tag id. 217 | case <-t.shutdown: 218 | return 219 | case <-t.ctx.Done(): 220 | return 221 | } 222 | } 223 | } 224 | 225 | func (t *transport) flush(ctx context.Context, tag Tag) error { 226 | // TODO(stevvooe): We need to fire and forget flush messages when a call 227 | // context gets cancelled. 228 | panic("not implemented") 229 | } 230 | 231 | func (t *transport) Close() error { 232 | t.close() 233 | 234 | select { 235 | case <-t.closed: 236 | return nil 237 | case <-t.ctx.Done(): 238 | return t.ctx.Err() 239 | } 240 | } 241 | 242 | // close starts the shutdown process. 243 | func (t *transport) close() { 244 | t.once.Do(func() { 245 | close(t.shutdown) 246 | }) 247 | } 248 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // DefaultMSize messages size used to establish a session. 10 | DefaultMSize = 64 << 10 11 | 12 | // DefaultVersion for this package. Currently, the only supported version. 13 | DefaultVersion = "9P2000" 14 | ) 15 | 16 | // Mode constants for use Dir.Mode. 17 | const ( 18 | DMDIR = 0x80000000 // mode bit for directories 19 | DMAPPEND = 0x40000000 // mode bit for append only files 20 | DMEXCL = 0x20000000 // mode bit for exclusive use files 21 | DMMOUNT = 0x10000000 // mode bit for mounted channel 22 | DMAUTH = 0x08000000 // mode bit for authentication file 23 | DMTMP = 0x04000000 // mode bit for non-backed-up files 24 | 25 | // 9p2000.u extensions 26 | 27 | DMSYMLINK = 0x02000000 28 | DMDEVICE = 0x00800000 29 | DMNAMEDPIPE = 0x00200000 30 | DMSOCKET = 0x00100000 31 | DMSETUID = 0x00080000 32 | DMSETGID = 0x00040000 33 | 34 | DMREAD = 0x4 // mode bit for read permission 35 | DMWRITE = 0x2 // mode bit for write permission 36 | DMEXEC = 0x1 // mode bit for execute permission 37 | ) 38 | 39 | // Flag defines the flag type for use with open and create 40 | type Flag uint8 41 | 42 | // Constants to use when opening files. 43 | const ( 44 | OREAD Flag = 0x00 // open for read 45 | OWRITE Flag = 0x01 // write 46 | ORDWR Flag = 0x02 // read and write 47 | OEXEC Flag = 0x03 // execute, == read but check execute permission 48 | 49 | // PROPOSAL(stevvooe): Possible protocal extension to allow the create of 50 | // symlinks. Initially, the link is created with no value. Read and write 51 | // to read and set the link value. 52 | 53 | OSYMLINK Flag = 0x04 54 | 55 | OTRUNC Flag = 0x10 // or'ed in (except for exec), truncate file first 56 | OCEXEC Flag = 0x20 // or'ed in, close on exec 57 | ORCLOSE Flag = 0x40 // or'ed in, remove on close 58 | ) 59 | 60 | // QType indicates the type of a resource within the Qid. 61 | type QType uint8 62 | 63 | // Constants for use in Qid to indicate resource type. 64 | const ( 65 | QTDIR QType = 0x80 // type bit for directories 66 | QTAPPEND QType = 0x40 // type bit for append only files 67 | QTEXCL QType = 0x20 // type bit for exclusive use files 68 | QTMOUNT QType = 0x10 // type bit for mounted channel 69 | QTAUTH QType = 0x08 // type bit for authentication file 70 | QTTMP QType = 0x04 // type bit for not-backed-up file 71 | QTFILE QType = 0x00 // plain file 72 | ) 73 | 74 | func (qt QType) String() string { 75 | switch qt { 76 | case QTDIR: 77 | return "dir" 78 | case QTAPPEND: 79 | return "append" 80 | case QTEXCL: 81 | return "excl" 82 | case QTMOUNT: 83 | return "mount" 84 | case QTAUTH: 85 | return "auth" 86 | case QTTMP: 87 | return "tmp" 88 | case QTFILE: 89 | return "file" 90 | } 91 | 92 | return "unknown" 93 | } 94 | 95 | // Tag uniquely identifies an outstanding fcall in a 9p session. 96 | type Tag uint16 97 | 98 | // NOTAG is a reserved values for messages sent before establishing a session, 99 | // such as Tversion. 100 | const NOTAG Tag = ^Tag(0) 101 | 102 | // Fid defines a type to hold Fid values. 103 | type Fid uint32 104 | 105 | // NOFID indicates the lack of an Fid. 106 | const NOFID Fid = ^Fid(0) 107 | 108 | // Qid indicates the type, path and version of the resource returned by a 109 | // server. It is only valid for a session. 110 | // 111 | // Typically, a client maintains a mapping of Fid-Qid as Qids are returned by 112 | // the server. 113 | type Qid struct { 114 | Type QType `9p:"type,1"` 115 | Version uint32 116 | Path uint64 117 | } 118 | 119 | func (qid Qid) String() string { 120 | return fmt.Sprintf("qid(%v, v=%x, p=%x)", 121 | qid.Type, qid.Version, qid.Path) 122 | } 123 | 124 | // Dir defines the structure used for expressing resources in stat/wstat and 125 | // when reading directories. 126 | type Dir struct { 127 | Type uint16 128 | Dev uint32 129 | Qid Qid 130 | Mode uint32 131 | 132 | // BUG(stevvooe): The Year 2038 is coming soon. 9p wire protocol has these 133 | // as 4 byte epoch times. Some possibilities include time dilation fields 134 | // or atemporal files. We can also just not use them and set them to zero. 135 | 136 | AccessTime time.Time 137 | ModTime time.Time 138 | 139 | Length uint64 140 | Name string 141 | UID string 142 | GID string 143 | MUID string 144 | } 145 | 146 | func (d Dir) String() string { 147 | return fmt.Sprintf("dir(%v mode=%v atime=%v mtime=%v length=%v name=%v uid=%v gid=%v muid=%v)", 148 | d.Qid, d.Mode, d.AccessTime, d.ModTime, d.Length, d.Name, d.UID, d.GID, d.MUID) 149 | } 150 | -------------------------------------------------------------------------------- /ufs/fileref.go: -------------------------------------------------------------------------------- 1 | package ufs 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | p9p "github.com/docker/go-p9p" 8 | ) 9 | 10 | type FileRef struct { 11 | sync.Mutex 12 | Path string 13 | Info p9p.Dir 14 | File *os.File 15 | Readdir *p9p.Readdir 16 | } 17 | 18 | func (f *FileRef) Stat() error { 19 | f.Lock() 20 | defer f.Unlock() 21 | return f.statLocked() 22 | } 23 | 24 | func (f *FileRef) statLocked() error { 25 | info, err := os.Lstat(f.Path) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | f.Info = dirFromInfo(info) 31 | return nil 32 | } 33 | 34 | func (f *FileRef) IsDir() bool { 35 | return f.Info.Mode&p9p.DMDIR > 0 36 | } 37 | -------------------------------------------------------------------------------- /ufs/session.go: -------------------------------------------------------------------------------- 1 | package ufs 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | "strconv" 11 | "sync" 12 | "syscall" 13 | 14 | "github.com/docker/go-p9p" 15 | ) 16 | 17 | type session struct { 18 | sync.Mutex 19 | rootRef *FileRef 20 | refs map[p9p.Fid]*FileRef 21 | } 22 | 23 | func NewSession(ctx context.Context, root string) (p9p.Session, error) { 24 | return &session{ 25 | rootRef: &FileRef{Path: root}, 26 | refs: make(map[p9p.Fid]*FileRef), 27 | }, nil 28 | } 29 | 30 | func (sess *session) getRef(fid p9p.Fid) (*FileRef, error) { 31 | sess.Lock() 32 | defer sess.Unlock() 33 | 34 | if fid == p9p.NOFID { 35 | return nil, p9p.ErrUnknownfid 36 | } 37 | 38 | ref, found := sess.refs[fid] 39 | if !found { 40 | return nil, p9p.ErrUnknownfid 41 | } 42 | 43 | if err := ref.Stat(); err != nil { 44 | return nil, err 45 | } 46 | 47 | return ref, nil 48 | } 49 | 50 | func (sess *session) newRef(fid p9p.Fid, path string) (*FileRef, error) { 51 | sess.Lock() 52 | defer sess.Unlock() 53 | 54 | if fid == p9p.NOFID { 55 | return nil, p9p.ErrUnknownfid 56 | } 57 | 58 | _, found := sess.refs[fid] 59 | if found { 60 | return nil, p9p.ErrDupfid 61 | } 62 | 63 | ref := &FileRef{Path: path} 64 | if err := ref.Stat(); err != nil { 65 | return nil, err 66 | } 67 | 68 | sess.refs[fid] = ref 69 | return ref, nil 70 | } 71 | 72 | func (sess *session) Auth(ctx context.Context, afid p9p.Fid, uname, aname string) (p9p.Qid, error) { 73 | // TODO: AuthInit? 74 | return p9p.Qid{}, nil //p9p.MessageRerror{Ename: "no auth"} 75 | } 76 | 77 | func (sess *session) Attach(ctx context.Context, fid, afid p9p.Fid, uname, aname string) (p9p.Qid, error) { 78 | if uname == "" { 79 | return p9p.Qid{}, p9p.MessageRerror{Ename: "no user"} 80 | } 81 | 82 | // TODO: AuthCheck? 83 | 84 | // if afid > 0 { 85 | // return p9p.Qid{}, p9p.MessageRerror{Ename: "attach: no auth"} 86 | // } 87 | 88 | if aname == "" { 89 | aname = sess.rootRef.Path 90 | } 91 | 92 | ref, err := sess.newRef(fid, aname) 93 | if err != nil { 94 | return p9p.Qid{}, err 95 | } 96 | 97 | return ref.Info.Qid, nil 98 | } 99 | 100 | func (sess *session) Clunk(ctx context.Context, fid p9p.Fid) error { 101 | ref, err := sess.getRef(fid) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | ref.Lock() 107 | defer ref.Unlock() 108 | if ref.File != nil { 109 | ref.File.Close() 110 | } 111 | 112 | sess.Lock() 113 | defer sess.Unlock() 114 | delete(sess.refs, fid) 115 | 116 | return nil 117 | } 118 | 119 | func (sess *session) Remove(ctx context.Context, fid p9p.Fid) error { 120 | defer sess.Clunk(ctx, fid) 121 | 122 | ref, err := sess.getRef(fid) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // TODO: check write perms on parent 128 | 129 | return os.Remove(ref.Path) 130 | } 131 | 132 | func (sess *session) Walk(ctx context.Context, fid p9p.Fid, newfid p9p.Fid, names ...string) ([]p9p.Qid, error) { 133 | var qids []p9p.Qid 134 | 135 | ref, err := sess.getRef(fid) 136 | if err != nil { 137 | return qids, err 138 | } 139 | 140 | newref, err := sess.newRef(newfid, ref.Path) 141 | if err != nil { 142 | return qids, err 143 | } 144 | 145 | path := newref.Path 146 | for _, name := range names { 147 | newpath := filepath.Join(path, name) 148 | r := &FileRef{Path: newpath} 149 | if err := r.Stat(); err != nil { 150 | break 151 | } 152 | qids = append(qids, r.Info.Qid) 153 | path = newpath 154 | } 155 | 156 | newref.Path = path 157 | return qids, nil 158 | } 159 | 160 | func (sess *session) Read(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) { 161 | ref, err := sess.getRef(fid) 162 | if err != nil { 163 | return 0, err 164 | } 165 | 166 | ref.Lock() 167 | defer ref.Unlock() 168 | 169 | if ref.IsDir() { 170 | if offset == 0 && ref.Readdir == nil { 171 | files, err := ioutil.ReadDir(ref.Path) 172 | if err != nil { 173 | return 0, err 174 | } 175 | var dirs []p9p.Dir 176 | for _, info := range files { 177 | dirs = append(dirs, dirFromInfo(info)) 178 | } 179 | ref.Readdir = p9p.NewFixedReaddir(p9p.NewCodec(), dirs) 180 | } 181 | if ref.Readdir == nil { 182 | return 0, p9p.ErrBadoffset 183 | } 184 | return ref.Readdir.Read(ctx, p, offset) 185 | } 186 | 187 | if ref.File == nil { 188 | return 0, p9p.MessageRerror{Ename: "no file open"} //p9p.ErrClosed 189 | } 190 | 191 | n, err = ref.File.ReadAt(p, offset) 192 | if err != nil && err != io.EOF { 193 | return n, err 194 | } 195 | return n, nil 196 | } 197 | 198 | func (sess *session) Write(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) { 199 | ref, err := sess.getRef(fid) 200 | if err != nil { 201 | return 0, err 202 | } 203 | 204 | ref.Lock() 205 | defer ref.Unlock() 206 | if ref.File == nil { 207 | return 0, p9p.ErrClosed 208 | } 209 | 210 | return ref.File.WriteAt(p, offset) 211 | } 212 | 213 | func (sess *session) Open(ctx context.Context, fid p9p.Fid, mode p9p.Flag) (p9p.Qid, uint32, error) { 214 | ref, err := sess.getRef(fid) 215 | if err != nil { 216 | return p9p.Qid{}, 0, err 217 | } 218 | 219 | ref.Lock() 220 | defer ref.Unlock() 221 | f, err := os.OpenFile(ref.Path, oflags(mode), 0) 222 | if err != nil { 223 | return p9p.Qid{}, 0, err 224 | } 225 | ref.File = f 226 | return ref.Info.Qid, 0, nil 227 | } 228 | 229 | func (sess *session) Create(ctx context.Context, parent p9p.Fid, name string, perm uint32, mode p9p.Flag) (p9p.Qid, uint32, error) { 230 | ref, err := sess.getRef(parent) 231 | if err != nil { 232 | return p9p.Qid{}, 0, err 233 | } 234 | 235 | newpath := filepath.Join(ref.Path, name) 236 | 237 | var file *os.File 238 | switch { 239 | case perm&p9p.DMDIR != 0: 240 | err = os.Mkdir(newpath, os.FileMode(perm&0777)) 241 | 242 | case perm&p9p.DMSYMLINK != 0: 243 | case perm&p9p.DMNAMEDPIPE != 0: 244 | case perm&p9p.DMDEVICE != 0: 245 | err = p9p.MessageRerror{Ename: "not implemented"} 246 | 247 | default: 248 | file, err = os.OpenFile(newpath, oflags(mode)|os.O_CREATE, os.FileMode(perm&0777)) 249 | } 250 | 251 | if file == nil && err == nil { 252 | file, err = os.OpenFile(newpath, oflags(mode), 0) 253 | } 254 | 255 | if err != nil { 256 | return p9p.Qid{}, 0, err 257 | } 258 | 259 | ref.Lock() 260 | defer ref.Unlock() 261 | ref.Path = newpath 262 | ref.File = file 263 | if err := ref.statLocked(); err != nil { 264 | return p9p.Qid{}, 0, err 265 | } 266 | return ref.Info.Qid, 0, err 267 | } 268 | 269 | func (sess *session) Stat(ctx context.Context, fid p9p.Fid) (p9p.Dir, error) { 270 | ref, err := sess.getRef(fid) 271 | if err != nil { 272 | return p9p.Dir{}, err 273 | } 274 | return ref.Info, nil 275 | } 276 | 277 | func (sess *session) WStat(ctx context.Context, fid p9p.Fid, dir p9p.Dir) error { 278 | ref, err := sess.getRef(fid) 279 | if err != nil { 280 | return err 281 | } 282 | 283 | if dir.Mode != ^uint32(0) { 284 | // TODO: 9P2000.u: DMSETUID DMSETGID 285 | err := os.Chmod(ref.Path, os.FileMode(dir.Mode&0777)) 286 | if err != nil { 287 | return err 288 | } 289 | } 290 | 291 | if dir.UID != "" || dir.GID != "" { 292 | usr, err := user.Lookup(dir.UID) 293 | if err != nil { 294 | return err 295 | } 296 | uid, err := strconv.Atoi(usr.Uid) 297 | if err != nil { 298 | return err 299 | } 300 | grp, err := user.LookupGroup(dir.GID) 301 | if err != nil { 302 | return err 303 | } 304 | gid, err := strconv.Atoi(grp.Gid) 305 | if err != nil { 306 | return err 307 | } 308 | if err := os.Chown(ref.Path, uid, gid); err != nil { 309 | return err 310 | } 311 | } 312 | 313 | if dir.Name != "" { 314 | newpath := filepath.Join(filepath.Dir(ref.Path), dir.Name) 315 | if err := syscall.Rename(ref.Path, newpath); err != nil { 316 | return nil 317 | } 318 | ref.Lock() 319 | defer ref.Unlock() 320 | ref.Path = newpath 321 | } 322 | 323 | if dir.Length != ^uint64(0) { 324 | if err := os.Truncate(ref.Path, int64(dir.Length)); err != nil { 325 | return err 326 | } 327 | } 328 | 329 | // If either mtime or atime need to be changed, then 330 | // we must change both. 331 | //if dir.ModTime != time.Time{} || dir.AccessTime != ^uint32(0) { 332 | // mt, at := time.Unix(int64(dir.Mtime), 0), time.Unix(int64(dir.Atime), 0) 333 | // if cmt, cat := (dir.Mtime == ^uint32(0)), (dir.Atime == ^uint32(0)); cmt || cat { 334 | // st, e := os.Stat(fid.path) 335 | // if e != nil { 336 | // req.RespondError(toError(e)) 337 | // return 338 | // } 339 | // switch cmt { 340 | // case true: 341 | // mt = st.ModTime() 342 | // default: 343 | // at = atime(st.Sys().(*syscall.Stat_t)) 344 | // } 345 | // } 346 | // e := os.Chtimes(fid.path, at, mt) 347 | // if e != nil { 348 | // req.RespondError(toError(e)) 349 | // return 350 | // } 351 | //} 352 | return nil 353 | } 354 | 355 | func (sess *session) Version() (msize int, version string) { 356 | return p9p.DefaultMSize, p9p.DefaultVersion 357 | } 358 | -------------------------------------------------------------------------------- /ufs/util.go: -------------------------------------------------------------------------------- 1 | package ufs 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | 7 | p9p "github.com/docker/go-p9p" 8 | ) 9 | 10 | func dirFromInfo(info os.FileInfo) p9p.Dir { 11 | dir := p9p.Dir{} 12 | 13 | dir.Qid.Path = info.Sys().(*syscall.Stat_t).Ino 14 | dir.Qid.Version = uint32(info.ModTime().UnixNano() / 1000000) 15 | 16 | dir.Name = info.Name() 17 | dir.Mode = uint32(info.Mode() & 0777) 18 | dir.Length = uint64(info.Size()) 19 | dir.AccessTime = atime(info.Sys().(*syscall.Stat_t)) 20 | dir.ModTime = info.ModTime() 21 | dir.MUID = "none" 22 | 23 | if info.IsDir() { 24 | dir.Qid.Type |= p9p.QTDIR 25 | dir.Mode |= p9p.DMDIR 26 | } 27 | 28 | return dir 29 | } 30 | 31 | func oflags(mode p9p.Flag) int { 32 | flags := 0 33 | 34 | switch mode & 3 { 35 | case p9p.OREAD: 36 | flags = os.O_RDONLY 37 | break 38 | 39 | case p9p.ORDWR: 40 | flags = os.O_RDWR 41 | break 42 | 43 | case p9p.OWRITE: 44 | flags = os.O_WRONLY 45 | break 46 | 47 | case p9p.OEXEC: 48 | flags = os.O_RDONLY 49 | break 50 | } 51 | 52 | if mode&p9p.OTRUNC != 0 { 53 | flags |= os.O_TRUNC 54 | } 55 | 56 | return flags 57 | } 58 | -------------------------------------------------------------------------------- /ufs/util_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | package ufs 3 | 4 | import ( 5 | "syscall" 6 | "time" 7 | ) 8 | 9 | func atime(stat *syscall.Stat_t) time.Time { 10 | return time.Unix(stat.Atimespec.Unix()) 11 | } 12 | -------------------------------------------------------------------------------- /ufs/util_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package ufs 3 | 4 | import ( 5 | "syscall" 6 | "time" 7 | ) 8 | 9 | func atime(stat *syscall.Stat_t) time.Time { 10 | return time.Unix(stat.Atim.Unix()) 11 | } 12 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package p9p 2 | 3 | import ( 4 | "fmt" 5 | 6 | "context" 7 | ) 8 | 9 | // NOTE(stevvooe): This file contains functions for negotiating version on the 10 | // client and server. There are some nasty details to get right for 11 | // downgrading the connection on the server-side that are not present yet. 12 | // Really, these should be refactored into some sort of channel type that can 13 | // support resets through version messages during the protocol exchange. 14 | 15 | // clientnegotiate negiotiates the protocol version using channel, blocking 16 | // until a response is received. The received value will be the version 17 | // implemented by the server. 18 | func clientnegotiate(ctx context.Context, ch Channel, version string) (string, error) { 19 | req := newFcall(NOTAG, MessageTversion{ 20 | MSize: uint32(ch.MSize()), 21 | Version: version, 22 | }) 23 | 24 | if err := ch.WriteFcall(ctx, req); err != nil { 25 | return "", err 26 | } 27 | 28 | resp := new(Fcall) 29 | if err := ch.ReadFcall(ctx, resp); err != nil { 30 | return "", err 31 | } 32 | 33 | switch v := resp.Message.(type) { 34 | case MessageRversion: 35 | 36 | if v.Version != version { 37 | // TODO(stevvooe): A stubborn client indeed! 38 | return "", fmt.Errorf("unsupported server version: %v", version) 39 | } 40 | 41 | if int(v.MSize) < ch.MSize() { 42 | // upgrade msize if server differs. 43 | ch.SetMSize(int(v.MSize)) 44 | } 45 | 46 | return v.Version, nil 47 | case error: 48 | return "", v 49 | default: 50 | return "", ErrUnexpectedMsg 51 | } 52 | } 53 | 54 | // servernegotiate blocks until a version message is received or a timeout 55 | // occurs. The msize for the tranport will be set from the negotiation. If 56 | // negotiate returns nil, a server may proceed with the connection. 57 | // 58 | // In the future, it might be better to handle the version messages in a 59 | // separate object that manages the session. Each set of version requests 60 | // effectively "reset" a connection, meaning all fids get clunked and all 61 | // outstanding IO is aborted. This is probably slightly racy, in practice with 62 | // a misbehaved client. The main issue is that we cannot tell which session 63 | // messages belong to. 64 | func servernegotiate(ctx context.Context, ch Channel, version string) error { 65 | // wait for the version message over the transport. 66 | req := new(Fcall) 67 | if err := ch.ReadFcall(ctx, req); err != nil { 68 | return err 69 | } 70 | 71 | mv, ok := req.Message.(MessageTversion) 72 | if !ok { 73 | return fmt.Errorf("expected version message: %v", mv) 74 | } 75 | 76 | respmsg := MessageRversion{ 77 | Version: version, 78 | } 79 | 80 | if mv.Version != version { 81 | // TODO(stevvooe): Not the best place to do version handling. We need 82 | // to have a way to pass supported versions into this method then have 83 | // it return the actual version. For now, respond with 9P2000 for 84 | // anything that doesn't match the provided version string. 85 | // 86 | // version(9) says "The server may respond with the client’s 87 | // version string, or a version string identifying an earlier 88 | // defined protocol version. Currently, the only defined 89 | // version is the 6 characters 9P2000." Therefore, it is always 90 | // OK to respond with this. 91 | respmsg.Version = "9P2000" 92 | } 93 | 94 | if int(mv.MSize) < ch.MSize() { 95 | // if the server msize is too large, use the client's suggested msize. 96 | ch.SetMSize(int(mv.MSize)) 97 | respmsg.MSize = mv.MSize 98 | } else { 99 | respmsg.MSize = uint32(ch.MSize()) 100 | } 101 | 102 | resp := newFcall(NOTAG, respmsg) 103 | if err := ch.WriteFcall(ctx, resp); err != nil { 104 | return err 105 | } 106 | 107 | if respmsg.Version == "unknown" { 108 | return fmt.Errorf("bad version negotiation") 109 | } 110 | 111 | return nil 112 | } 113 | --------------------------------------------------------------------------------