Pure Erlang implementation of 9p2000 protocol
filesystem fs 9p2000 erlang 9p

ft: basic parsing of messages and initial protocol work

+916
+21
.gitignore
··· 1 + .rebar3 2 + _build 3 + _checkouts 4 + _vendor 5 + .eunit 6 + *.o 7 + *.beam 8 + *.plt 9 + *.swp 10 + *.swo 11 + .erlang.cookie 12 + ebin 13 + log 14 + erl_crash.dump 15 + .rebar 16 + logs 17 + .idea 18 + *.iml 19 + rebar3.crashdump 20 + *~ 21 + /doc
+186
LICENSE.md
··· 1 + # Apache License 2 + Version 2.0, January 2004 3 + 4 + http://www.apache.org/licenses/ 5 + 6 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 + 8 + ## 1. Definitions. 9 + 10 + "License" shall mean the terms and conditions for use, reproduction, and 11 + distribution as defined by Sections 1 through 9 of this document. 12 + 13 + "Licensor" shall mean the copyright owner or entity authorized by the copyright 14 + owner that is granting the License. 15 + 16 + "Legal Entity" shall mean the union of the acting entity and all other entities 17 + that control, are controlled by, or are under common control with that entity. 18 + For the purposes of this definition, "control" means (i) the power, direct or 19 + indirect, to cause the direction or management of such entity, whether by 20 + contract or 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 exercising 24 + permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, including 27 + but not limited to software source code, documentation source, and configuration 28 + files. 29 + 30 + "Object" form shall mean any form resulting from mechanical transformation or 31 + translation of a Source form, including but not limited to compiled object code, 32 + generated documentation, and conversions to other media types. 33 + 34 + "Work" shall mean the work of authorship, whether in Source or Object form, made 35 + available under the License, as indicated by a copyright notice that is included 36 + in or attached to the work (an example is provided in the Appendix below). 37 + 38 + "Derivative Works" shall mean any work, whether in Source or Object form, that 39 + is based on (or derived from) the Work and for which the editorial revisions, 40 + annotations, elaborations, or other modifications represent, as a whole, an 41 + original work of authorship. For the purposes of this License, Derivative Works 42 + shall not include works that remain separable from, or merely link (or bind by 43 + name) to the interfaces of, the Work and Derivative Works thereof. 44 + 45 + "Contribution" shall mean any work of authorship, including the original version 46 + of the Work and any modifications or additions to that Work or Derivative Works 47 + thereof, that is intentionally submitted to Licensor for inclusion in the Work 48 + by the copyright owner or by an individual or Legal Entity authorized to submit 49 + on behalf of the copyright owner. For the purposes of this definition, 50 + "submitted" means any form of electronic, verbal, or written communication sent 51 + to the Licensor or its representatives, including but not limited to 52 + communication on electronic mailing lists, source code control systems, and 53 + issue tracking systems that are managed by, or on behalf of, the Licensor for 54 + the purpose of discussing and improving the Work, but excluding communication 55 + that is conspicuously marked or otherwise designated in writing by the copyright 56 + owner as "Not a Contribution." 57 + 58 + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 59 + of whom a Contribution has been received by Licensor and subsequently 60 + incorporated within the Work. 61 + 62 + ## 2. Grant of Copyright License. 63 + 64 + Subject to the terms and conditions of this License, each Contributor hereby 65 + grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 66 + irrevocable copyright license to reproduce, prepare Derivative Works of, 67 + publicly display, publicly perform, sublicense, and distribute the Work and such 68 + Derivative Works in Source or Object form. 69 + 70 + ## 3. Grant of Patent License. 71 + 72 + Subject to the terms and conditions of this License, each Contributor hereby 73 + grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 74 + irrevocable (except as stated in this section) patent license to make, have 75 + made, use, offer to sell, sell, import, and otherwise transfer the Work, where 76 + such license applies only to those patent claims licensable by such Contributor 77 + that are necessarily infringed by their Contribution(s) alone or by combination 78 + of their Contribution(s) with the Work to which such Contribution(s) was 79 + submitted. If You institute patent litigation against any entity (including a 80 + cross-claim or counterclaim in a lawsuit) alleging that the Work or a 81 + Contribution incorporated within the Work constitutes direct or contributory 82 + patent infringement, then any patent licenses granted to You under this License 83 + for that Work shall terminate as of the date such litigation is filed. 84 + 85 + ## 4. Redistribution. 86 + 87 + You may reproduce and distribute copies of the Work or Derivative Works thereof 88 + in any medium, with or without modifications, and in Source or Object form, 89 + provided that You meet the following conditions: 90 + 91 + 1. You must give any other recipients of the Work or Derivative Works a copy of 92 + this License; and 93 + 94 + 2. You must cause any modified files to carry prominent notices stating that 95 + You changed the files; and 96 + 97 + 3. You must retain, in the Source form of any Derivative Works that You 98 + distribute, all copyright, patent, trademark, and attribution notices from 99 + the Source form of the Work, excluding those notices that do not pertain to 100 + any part of the Derivative Works; and 101 + 102 + 4. If the Work includes a "NOTICE" text file as part of its distribution, then 103 + any Derivative Works that You distribute must include a readable copy of the 104 + attribution notices contained within such NOTICE file, excluding those 105 + notices that do not pertain to any part of the Derivative Works, in at least 106 + one of the following places: within a NOTICE text file distributed as part 107 + of the Derivative Works; within the Source form or documentation, if 108 + provided along with the Derivative Works; or, within a display generated by 109 + the Derivative Works, if and wherever such third-party notices normally 110 + appear. The contents of the NOTICE file are for informational purposes only 111 + and do not modify the License. You may add Your own attribution notices 112 + within Derivative Works that You distribute, alongside or as an addendum to 113 + the NOTICE text from the Work, provided that such additional attribution 114 + notices cannot be construed as modifying the License. 115 + 116 + You may add Your own copyright statement to Your modifications and may provide 117 + additional or different license terms and conditions for use, reproduction, or 118 + distribution of Your modifications, or for any such Derivative Works as a whole, 119 + provided Your use, reproduction, and distribution of the Work otherwise complies 120 + with the conditions stated in this License. 121 + 122 + ## 5. Submission of Contributions. 123 + 124 + Unless You explicitly state otherwise, any Contribution intentionally submitted 125 + for inclusion in the Work by You to the Licensor shall be under the terms and 126 + conditions of this License, without any additional terms or conditions. 127 + Notwithstanding the above, nothing herein shall supersede or modify the terms of 128 + any separate license agreement you may have executed with Licensor regarding 129 + such Contributions. 130 + 131 + ## 6. Trademarks. 132 + 133 + This License does not grant permission to use the trade names, trademarks, 134 + service marks, or product names of the Licensor, except as required for 135 + reasonable and customary use in describing the origin of the Work and 136 + reproducing the content of the NOTICE file. 137 + 138 + ## 7. Disclaimer of Warranty. 139 + 140 + Unless required by applicable law or agreed to in writing, Licensor provides the 141 + Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 142 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 143 + including, without limitation, any warranties or conditions of TITLE, NON- 144 + INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 145 + solely responsible for determining the appropriateness of using or 146 + redistributing the Work and assume any risks associated with Your exercise of 147 + permissions under this License. 148 + 149 + ## 8. Limitation of Liability. 150 + 151 + In no event and under no legal theory, whether in tort (including negligence), 152 + contract, or otherwise, unless required by applicable law (such as deliberate 153 + and grossly negligent acts) or agreed to in writing, shall any Contributor be 154 + liable to You for damages, including any direct, indirect, special, incidental, 155 + or consequential damages of any character arising as a result of this License or 156 + out of the use or inability to use the Work (including but not limited to 157 + damages for loss of goodwill, work stoppage, computer failure or malfunction, or 158 + any and all other commercial damages or losses), even if such Contributor has 159 + been advised of the possibility of such damages. 160 + 161 + ## 9. Accepting Warranty or Additional Liability. 162 + 163 + While redistributing the Work or Derivative Works thereof, You may choose to 164 + offer, and charge a fee for, acceptance of support, warranty, indemnity, or 165 + other liability obligations and/or rights consistent with this License. However, 166 + in accepting such obligations, You may act only on Your own behalf and on Your 167 + sole responsibility, not on behalf of any other Contributor, and only if You 168 + agree to indemnify, defend, and hold each Contributor harmless for any liability 169 + incurred by, or claims asserted against, such Contributor by reason of your 170 + accepting any such warranty or additional liability. 171 + 172 + END OF TERMS AND CONDITIONS 173 + 174 + Copyright 2023, Łukasz Niemier <lukasz@niemier.pl>. 175 + 176 + Licensed under the Apache License, Version 2.0 (the "License"); 177 + you may not use this file except in compliance with the License. 178 + You may obtain a copy of the License at 179 + 180 + http://www.apache.org/licenses/LICENSE-2.0 181 + 182 + Unless required by applicable law or agreed to in writing, software 183 + distributed under the License is distributed on an "AS IS" BASIS, 184 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 185 + See the License for the specific language governing permissions and 186 + limitations under the License.
+44
README.md
··· 1 + # e9p 2 + 3 + Implementation of [9p2000][] file protocol in Erlang 4 + 5 + ## Goals 6 + 7 + - [x] Message parsing 8 + - [ ] Client implementation 9 + + [x] Establishing connection 10 + + [ ] Tree walking 11 + + [ ] IO server implementation for reading/writing files 12 + + [ ] File/directory creation 13 + + [ ] File/directory deletion 14 + + [ ] File stats 15 + - [ ] Server implementation 16 + + [x] Establishing connection 17 + + [ ] Tree walking 18 + + [ ] File/directory creation 19 + + [ ] File/directory deletion 20 + + [ ] File stats 21 + + [ ] Customisable FS implementations 22 + 23 + ### Example FS 24 + 25 + - [ ] "Passthrough" - which will simply allow accessing some "real" directory in 26 + system FS 27 + - [ ] ErlProcFS - which will expose Erlang process tree and other internal data 28 + via API similar to `procfs` from Linux 29 + 30 + ## Reasoning 31 + 32 + I want to implement `procfs`-like API for Erlang to allow non-Erlang-fluent 33 + operators to navigate through Erlang processes. There is something similar 34 + implemented in [`fuserl`][fuserl], but that uses [`libfuse`][libfuse], which is 35 + [NIF][] implemented. That requires compilation of native code and can be 36 + problematic wrt cross compilation and stuff. On the other hand [9p2000][] is 37 + network protocol that can be implemented fully in Erlang, thus do not require 38 + any additional tools or compilation steps. It can also be accessed remotely if 39 + needed. 40 + 41 + [9p2000]: http://ericvh.github.io/9p-rfc/rfc9p2000.html 42 + [fuserl]: https://github.com/tonyrog/fuserl 43 + [libfuse]: https://github.com/libfuse/libfuse 44 + [NIF]: https://www.erlang.org/doc/system/nif.html#
+4
rebar.config
··· 1 + {erl_opts, [debug_info]}. 2 + {deps, [ranch]}. 3 + 4 + {project_plugins, [rebar3_ex_doc]}.
+8
rebar.lock
··· 1 + {"1.2.0", 2 + [{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}]}. 3 + [ 4 + {pkg_hash,[ 5 + {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}]}, 6 + {pkg_hash_ext,[ 7 + {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}]} 8 + ].
+15
src/e9p.app.src
··· 1 + {application, e9p, 2 + [{description, "An OTP library"}, 3 + {vsn, "0.1.0"}, 4 + {registered, []}, 5 + {applications, 6 + [kernel, 7 + stdlib, 8 + ranch 9 + ]}, 10 + {env,[]}, 11 + {modules, []}, 12 + 13 + {licenses, ["Apache-2.0"]}, 14 + {links, []} 15 + ]}.
+13
src/e9p.erl
··· 1 + -module(e9p). 2 + 3 + -export([]). 4 + 5 + -export_type([qid/0, fid/0]). 6 + 7 + -type qid() :: #{ 8 + type => integer(), 9 + version => integer(), 10 + path => integer() 11 + }. 12 + 13 + -type fid() :: 16#00000000..16#FFFFFFFF.
+94
src/e9p_client.erl
··· 1 + -module(e9p_client). 2 + 3 + -include("e9p_internal.hrl"). 4 + -include_lib("kernel/include/logger.hrl"). 5 + 6 + -behaviour(gen_server). 7 + 8 + -export([attach/3]). 9 + -export([start_link/3]). 10 + -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). 11 + 12 + attach(Client, Uname, Aname) -> 13 + gen_server:call(Client, {attach, noauth, Uname, Aname}). 14 + 15 + start_link(Host, Port, Opts) -> 16 + gen_server:start_link(?MODULE, {Host, Port, Opts}, []). 17 + 18 + init({Host, Port, _Opts}) -> 19 + {ok, Socket} = gen_tcp:connect(Host, Port, [{active, false}, binary]), 20 + case version_negotiation(Socket) of 21 + {ok, #{max_packet_size := MaxPacketSize, version := ?version}} -> 22 + inet:setopts(Socket, [{active, once}]), 23 + {ok, 24 + #{socket => Socket, 25 + buffer => <<>>, 26 + tag => 0, 27 + fid => 0, 28 + msgs => #{}, 29 + max_packet_size => MaxPacketSize, 30 + version => ?version}}; 31 + {ok, _, #{version := OtherVersion}} -> 32 + {error, {unsupported_version, OtherVersion}}; 33 + {error, _} = Error -> 34 + Error 35 + end. 36 + 37 + handle_call({attach, Auth, Uname, Aname}, From, State) -> 38 + #{tag := Tag, 39 + fid := Fid, 40 + socket := Socket, 41 + msgs := Msgs} = 42 + State, 43 + Afid = 44 + case Auth of 45 + noauth -> 46 + ?nofid; 47 + Id -> 48 + Id 49 + end, 50 + Msg = #{type => tattach, 51 + data => 52 + #{fid => Fid, 53 + afid => Afid, 54 + uname => Uname, 55 + aname => Aname}}, 56 + e9p_transport:send(Socket, Tag, Msg), 57 + {noreply, 58 + State#{tag := Tag + 1, 59 + fid := Fid + 1, 60 + msgs := Msgs#{Tag => {From, #{fid => Fid}}}}}; 61 + handle_call(_Msg, _From, State) -> 62 + {reply, {error, not_implemented}, State}. 63 + 64 + handle_cast(_Msg, State) -> 65 + {noreply, State}. 66 + 67 + handle_info({tcp, Socket, Data}, #{socket := Socket} = State) -> 68 + #{buffer := Buffer, msgs := Msgs0} = State, 69 + case e9p_transport:read_stream(<<Buffer/binary, Data/binary>>) of 70 + {ok, Tag, Msg, Rest} -> 71 + Msgs = 72 + case maps:take(Tag, Msgs0) of 73 + {{From, _}, M} -> 74 + gen_server:reply(From, {ok, Msg}), 75 + M; 76 + error -> 77 + ?LOG_WARNING("Unknown tag ~p", [Tag]), 78 + Msgs0 79 + end, 80 + {noreply, State#{buffer := Rest, msgs := Msgs}}; 81 + {more, Data} -> 82 + {noreply, State#{buffer := Data}} 83 + end. 84 + 85 + version_negotiation(Socket) -> 86 + Msg = #{type => tversion, 87 + data => #{max_packet_size => ?max_packet_size, version => ?version}}, 88 + e9p_transport:send(Socket, notag, Msg), 89 + case e9p_transport:read(Socket) of 90 + {ok, _, #{type := rversion, data := Resp}} -> 91 + {ok, Resp}; 92 + {error, _} = Error -> 93 + Error 94 + end.
+89
src/e9p_fs.erl
··· 1 + %% @doc Definition of 9p filesystem 2 + %% @end 3 + -module(e9p_fs). 4 + 5 + % -behaviour(gen_server). 6 + 7 + -export([start_link/2, walk/3]). 8 + 9 + -export([init/1, handle_call/3]). 10 + 11 + -export_type([fs/0, state/0]). 12 + 13 + -opaque fs() :: pid(). 14 + 15 + -type state() :: term(). 16 + 17 + %% Setup state for given filesystem. 18 + -callback init(term()) -> state(). 19 + 20 + %% Returns `QID' for root node. 21 + %% 22 + %% If implementation provides multiple trees then the `AName' will be set to the 23 + %% tree defined by the client. It is left to the implementation to ensure the 24 + %% constraints of the file root (aka `walk(Root, "..", State0) =:= {Root, State1}'. 25 + -callback root(AName :: unicode:chardata(), state()) -> {e9p:qid(), state()}. 26 + 27 + %% Walk through the given path starting at the `QID' 28 + -callback walk(QID :: e9p:qid(), unicode:chardata(), state()) -> 29 + {e9p:qid() | false, state()}. 30 + 31 + %% Return stat data for file indicated by `QID' 32 + -callback stat(QID :: e9p:qid(), state()) -> {ok, map(), state()} | {error, term(), state()}. 33 + 34 + %% Read data from file indicated by `QID' 35 + -callback read(QID :: e9p:qid(), 36 + Offset :: non_neg_integer(), 37 + Length :: non_neg_integer(), 38 + state()) -> {ok, iodata(), state()} | {error, term(), state()}. 39 + 40 + %% Write data to file indicated by `QID' 41 + -callback write(QID :: e9p:qid(), 42 + Offset :: non_neg_integer(), 43 + Data :: iodata(), 44 + state()) -> {ok, non_neg_integer(), state()} | {error, term(), state()}. 45 + 46 + %% @doc Walk through the filesystem. 47 + %% 48 + %% Walks through `List' entries starting at `QID'. It will stop at first path 49 + %% that cannot be walked into. This mean, that returned list length will be 50 + %% equal <b>or less</b> than the `length(List)'. 51 + -spec walk(fs(), e9p:qid(), [unicode:chardata()]) -> {ok, [e9p:qid()]} | {error, term()}. 52 + walk(FS, QID, List) -> 53 + List0 = e9p_utils:normalize_path(List, []), 54 + case List of 55 + [] -> {ok, []}; 56 + [_|_] -> gen_server:call(FS, {walk, QID, List0}) 57 + end. 58 + 59 + %% @private 60 + start_link(Impl, Init) -> 61 + gen_server:start_link(?MODULE, {Impl, Init}, []). 62 + 63 + %% @private 64 + init({Impl, Init}) -> 65 + case Impl:init(Init) of 66 + {ok, State0} -> 67 + {ok, #{mod => Impl, state => State0}}; 68 + {error, _} = Error -> 69 + Error 70 + end. 71 + 72 + %% @private 73 + handle_call({walk, QID, List}, _From, #{mod := Mod, state := State0}) -> 74 + {QIDs, State} = do_walk(Mod, QID, List, State0), 75 + {reply, {ok, QIDs}, State}. 76 + 77 + %% Walk through the FS tree. 78 + do_walk(Mod, QID, List, State) -> 79 + do_walk(Mod, QID, List, State, []). 80 + 81 + do_walk(_Mod, _QID, [], State, Acc) -> 82 + {lists:reverse(Acc), State}; 83 + do_walk(Mod, QID0, [P | Rest], State0, Acc) -> 84 + case Mod:walk(QID0, P, State0) of 85 + {false, State} -> 86 + {lists:reverse(Acc), State}; 87 + {QID1, State} -> 88 + do_walk(Mod, QID1, Rest, State, [QID1 | Acc]) 89 + end.
+40
src/e9p_internal.hrl
··· 1 + -define(version, <<"9P2000">>). 2 + 3 + -define(notag, 16#FFFF). 4 + -define(nofid, 16#FFFFFFFF). 5 + 6 + -define(max_packet_size, 8168). 7 + 8 + -define(int, little-unsigned-unit:8). 9 + -define(len, 2/?int). 10 + 11 + %% Requests 12 + -define(Tversion, 100). 13 + -define(Tauth, 102). 14 + -define(Tattach, 104). 15 + -define(Tflush, 108). 16 + -define(Twalk, 110). 17 + -define(Topen, 112). 18 + -define(Tcreate, 114). 19 + -define(Tread, 116). 20 + -define(Twrite, 118). 21 + -define(Tclunk, 120). 22 + -define(Tremove, 122). 23 + -define(Tstat, 124). 24 + -define(Twstat, 126). 25 + 26 + %% Responses 27 + -define(Rversion, 101). 28 + -define(Rauth, 103). 29 + -define(Rattach, 105). 30 + -define(Rerror, 107). 31 + -define(Rflush, 109). 32 + -define(Rwalk, 111). 33 + -define(Ropen, 113). 34 + -define(Rcreate, 115). 35 + -define(Rread, 117). 36 + -define(Rwrite, 119). 37 + -define(Rclunk, 121). 38 + -define(Rremove, 123). 39 + -define(Rstat, 125). 40 + -define(Rwstat, 127).
+28
src/e9p_io_server.erl
··· 1 + %% @hidden 2 + 3 + -module(e9p_io_server). 4 + 5 + -behaviour(gen_server). 6 + 7 + -include_lib("kernel/include/logger.hrl"). 8 + 9 + -export([start_link/2]). 10 + 11 + -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). 12 + 13 + start_link(QID, Client) -> 14 + gen_server:start_link(?MODULE, {QID, Client}, []). 15 + 16 + init({QID, Client}) -> 17 + {ok, {QID, Client}}. 18 + 19 + handle_call(_Msg, _From, State) -> 20 + {reply, notsupported, State}. 21 + 22 + handle_cast(_Msg, State) -> 23 + {noreply, State}. 24 + 25 + handle_info({io_request, From, ReplyAs, Request}, State) -> 26 + ?LOG_NOTICE("~p", [Request]), 27 + From ! {io_reply, ReplyAs, {error, notimplemented}}, 28 + {noreply, State}.
+274
src/e9p_msg.erl
··· 1 + %% @doc Protocol messages parsing and encoding. 2 + %% @end 3 + -module(e9p_msg). 4 + 5 + -export([parse/2, encode/2, encode/3]). 6 + 7 + -export_type([tag/0, 8 + message_type/0, 9 + request_message_type/0, 10 + response_message_type/0, 11 + message/0]). 12 + 13 + -include("e9p_internal.hrl"). 14 + 15 + -type tag() :: 16#0000..16#FFFF. 16 + 17 + -type request_message_type() :: 18 + tversion | 19 + tauth | 20 + tattach | 21 + tflush | 22 + twalk | 23 + topen | 24 + tcreate | 25 + tread | 26 + twrite | 27 + tclunk | 28 + tremove | 29 + tstat | 30 + twstat. 31 + 32 + -type response_message_type() :: 33 + rversion | 34 + rauth | 35 + rattach | 36 + rerror | 37 + rflush | 38 + rwalk | 39 + ropen | 40 + rcreate | 41 + rread | 42 + rwrite | 43 + rclunk | 44 + rstat | 45 + rwstat. 46 + 47 + -type message_type() :: request_message_type() | response_message_type(). 48 + 49 + -type message() :: #{type => message_type(), 50 + data => map()}. 51 + 52 + -spec parse(Type :: byte(), Message :: binary()) -> {ok, message()} | {error, term()}. 53 + parse(Type, Data) -> 54 + case do_parse(Type, Data) of 55 + {ok, T, Parsed} -> 56 + {ok, #{type => T, data => Parsed}}; 57 + 58 + {error, Reason} -> 59 + {error, Reason} 60 + end. 61 + 62 + %% version - negotiate protocol version 63 + do_parse(?Tversion, <<MSize:4/?int, VSize:?len, Version:VSize/binary>>) -> 64 + {ok, tversion, #{max_packet_size => MSize, version => Version}}; 65 + do_parse(?Rversion, <<MSize:4/?int, VSize:?len, Version:VSize/binary>>) -> 66 + {ok, rversion, #{max_packet_size => MSize, version => Version}}; 67 + 68 + %% attach, auth - messages to establish a connection 69 + do_parse(?Tauth, <<AFID:4/?int, 70 + UnameLen:?len, Uname:UnameLen/binary, 71 + AnameLen:?len, Aname:AnameLen/binary>>) -> 72 + {ok, tauth, #{afid => AFID, 73 + uname => Uname, 74 + aname => Aname}}; 75 + do_parse(?Rauth, <<AQID:13/binary>>) -> 76 + {ok, rauth, #{aqid => binary_to_qid(AQID)}}; 77 + 78 + do_parse(?Tattach, <<FID:4/?int, 79 + AFID:4/?int, 80 + ULen:?len, Uname:ULen/binary, 81 + ALen:?len, Aname:ALen/binary>>) -> 82 + {ok, tattach, #{fid => FID, 83 + afid => AFID, 84 + uname => Uname, 85 + aname => Aname}}; 86 + do_parse(?Rattach, <<QID:13/binary>>) -> 87 + {ok, rattach, #{qid => binary_to_qid(QID)}}; 88 + 89 + %% clunk - forget about a fid 90 + do_parse(?Tclunk, <<FID:4/?int>>) -> 91 + {ok, tclunk, #{fid => FID}}; 92 + do_parse(?Rclunk, <<>>) -> 93 + {ok, rclunk, #{}}; 94 + 95 + %% error - return an error 96 + do_parse(?Rerror, <<ELen:?len, Error:ELen/binary>>) -> 97 + {ok, rerror, #{error => Error}}; 98 + 99 + %% flush - abort a message 100 + do_parse(?Tflush, <<Tag:2/?int>>) -> 101 + {ok, tflush, #{tag => Tag}}; 102 + do_parse(?Rflush, <<>>) -> 103 + {ok, rflush, #{}}; 104 + 105 + %% open, create - prepare a fid for I/O on an existing or new file 106 + do_parse(?Topen, <<FID:4/?int, Mode:1/?int>>) -> 107 + {ok, topen, #{fid => FID, mode => Mode}}; 108 + do_parse(?Ropen, <<QID:13/binary, IOUnit:4/?int>>) -> 109 + {ok, ropen, #{qid => binary_to_qid(QID), io_unit => IOUnit}}; 110 + 111 + do_parse(?Tcreate, <<FID:4/?int, 112 + NLen:?len, Name:NLen/binary, 113 + Perm:4/?int, 114 + Mode:1/?int>>) -> 115 + {ok, tcreate, #{fid => FID, name => Name, perm => Perm, mode => Mode}}; 116 + do_parse(?Rcreate, <<QID:13/binary, IOUnit:4/?int>>) -> 117 + {ok, rcreate, #{qid => binary_to_qid(QID), io_unit => IOUnit}}; 118 + 119 + %% remove - remove a file from a server 120 + do_parse(?Tremove, <<FID:4/?int>>) -> 121 + {ok, tremove, #{fid => FID}}; 122 + do_parse(?Rremove, <<>>) -> 123 + {ok, rremove, #{}}; 124 + 125 + %% stat, wstat - inquire or change file attributes 126 + do_parse(?Tstat, <<FID:4/?int>>) -> 127 + {ok, tstat, #{fid => FID}}; 128 + do_parse(?Rstat, <<DLen:?len, Data:DLen/binary>>) -> 129 + case parse_stat(Data) of 130 + {ok, Stat} -> 131 + {ok, rstat, #{stat => Stat}}; 132 + 133 + {error, _} = Error -> 134 + Error 135 + end; 136 + 137 + do_parse(?Twstat, <<FID:4/?int, DLen:?len, Data:DLen/binary>>) -> 138 + case parse_stat(Data) of 139 + {ok, Stat} -> 140 + {ok, twstat, #{fid => FID, stat => Stat}}; 141 + 142 + {error, _} = Error -> 143 + Error 144 + end; 145 + do_parse(?Rwstat, <<>>) -> 146 + {ok, rwstat, #{}}; 147 + 148 + %% walk - descend a directory hierarchy 149 + do_parse(?Twalk, <<FID:4/?int, NewFID:4/?int, NWNLen:?len, Rest/binary>>) -> 150 + NWNames = [Name || <<NLen:?len, Name:NLen/binary>> <= Rest], 151 + Len = length(NWNames), 152 + if 153 + Len == NWNLen -> 154 + {ok, twalk, #{fid => FID, new_fid => NewFID, names => NWNames}}; 155 + true -> 156 + {error, {invalid_walk_length, NWNLen, Len}} 157 + end; 158 + do_parse(?Rwalk, <<NWQLen:?len, QIDs:(NWQLen * 13)/binary>>) -> 159 + {ok, rwalk, #{qids => [binary_to_qid(QID) || <<QID:13/binary>> <= QIDs]}}; 160 + 161 + do_parse(Type, Data) -> 162 + {error, {invalid_message, Type, Data}}. 163 + 164 + parse_stat(<<_Size:2/?int, 165 + Type:2/?int, 166 + Dev:4/?int, 167 + QID:13/binary, 168 + Mode:4/?int, 169 + Atime:4/?int, 170 + Mtime:4/?int, 171 + Len:8/?int, 172 + NLen:?len, Name:NLen/binary, 173 + ULen:?len, Uid:ULen/binary, 174 + GLen:?len, Gid:GLen/binary, 175 + MULen:?len, MUid:MULen/binary>>) 176 + -> 177 + {ok, #{ 178 + type => Type, 179 + dev => Dev, 180 + qid => binary_to_qid(QID), 181 + mode => Mode, 182 + atime => calendar:system_time_to_universal_time(Atime, seconds), 183 + mtime => calendar:system_time_to_universal_time(Mtime, seconds), 184 + length => Len, 185 + name => Name, 186 + uid => Uid, 187 + gid => Gid, 188 + muid => MUid 189 + }}; 190 + parse_stat(_) -> {error, invalid_stat_data}. 191 + 192 + -spec encode(Tag :: tag() | notag, Message :: message()) -> iodata(). 193 + encode(Tag, #{type := Type, data := Data}) -> 194 + encode(Tag, Type, Data). 195 + 196 + -spec encode(Tag :: tag() | notag, Type :: message_type(), Data :: map()) -> iodata(). 197 + encode(Tag, Type, Data) -> 198 + {MT, Encoded} = do_encode(Type, Data), 199 + Tag0 = case Tag of 200 + notag -> ?notag; 201 + V -> V 202 + end, 203 + Size = iolist_size(Encoded) + 7, 204 + [<<Size:4/?int, MT:1/?int, Tag0:2/?int>> | Encoded]. 205 + 206 + do_encode(tversion, #{max_packet_size := MSize, version := Version}) -> 207 + {?Tversion, [<<MSize:4/?int>> | encode_str(Version)]}; 208 + do_encode(rversion, #{max_packet_size := MSize, version := Version}) -> 209 + {?Rversion, [<<MSize:4/?int>> | encode_str(Version)]}; 210 + 211 + do_encode(tauth, #{afid := AFID, uname := Uname, aname := Aname}) -> 212 + {?Tauth, [<<AFID:4/?int>>, encode_str(Uname), encode_str(Aname)]}; 213 + do_encode(rauth, #{aqid := AQID}) -> 214 + {?Rauth, qid_to_binary(AQID)}; 215 + 216 + do_encode(tattach, #{fid := FID, afid := AFID, uname := Uname, aname := Aname}) -> 217 + {?Tattach, [<<FID:4/?int, AFID:4/?int>>, encode_str(Uname), encode_str(Aname)]}; 218 + do_encode(rattach, #{qid := QID}) -> 219 + {?Rattach, qid_to_binary(QID)}; 220 + 221 + do_encode(tclunk, #{fid := FID}) -> 222 + {?Tclunk, <<FID:4/?int>>}; 223 + do_encode(rclunk, _) -> 224 + {?Rclunk, []}; 225 + 226 + do_encode(rerror, #{error := Error}) -> 227 + {?Rerror, encode_str(Error)}; 228 + 229 + do_encode(tflush, #{tag := Tag}) -> 230 + {?Tflush, <<Tag:2/?int>>}; 231 + do_encode(rflush, _) -> 232 + {?Rflush, []}; 233 + 234 + do_encode(topen, #{fid := FID, mode := Mode}) -> 235 + {?Topen, <<FID:4/?int, Mode:1/?int>>}; 236 + do_encode(ropen, #{qid := QID, io_unit := IOUnit}) -> 237 + {?Ropen, [qid_to_binary(QID), <<IOUnit:4/?int>>]}; 238 + 239 + do_encode(tcreate, #{fid := FID, name := Name, perm := Perm, mode := Mode}) -> 240 + {?Tcreate, [<<FID:4/?int>>, encode_str(Name), <<Perm:4/?int, Mode:1/?int>>]}; 241 + do_encode(rcreate, #{qid := QID, io_unit := IOUnit}) -> 242 + {?Rcreate, [qid_to_binary(QID), <<IOUnit:4/?int>>]}; 243 + 244 + do_encode(tremove, #{fid := FID}) -> 245 + {?Tremove, <<FID:4/?int>>}; 246 + do_encode(rremove, _) -> 247 + {?Rremove, []}; 248 + 249 + do_encode(tstat, #{fid := FID}) -> 250 + {?Tstat, <<FID:4/?int>>}; 251 + do_encode(rstat, _Data) -> 252 + error(unimplemented); 253 + 254 + do_encode(twstat, #{fid := _FID, stat := _Stat}) -> 255 + error(unimplemented); 256 + 257 + do_encode(twalk, #{fid := FID, new_fid := NewFID, names := Names}) -> 258 + ENames = [encode_str(Name) || Name <- Names], 259 + Len = length(ENames), 260 + {?Twalk, [<<FID:4/?int, NewFID:4/?int, Len:?len>> | ENames]}; 261 + do_encode(rwalk, #{qids := QIDs}) -> 262 + EQIDs = [qid_to_binary(QID) || QID <- QIDs], 263 + Len = length(EQIDs), 264 + {?Rwalk, [<<Len:?len>> | EQIDs]}. 265 + 266 + encode_str(Data) -> 267 + Len = iolist_size(Data), 268 + [<<Len:?len>> | Data]. 269 + 270 + binary_to_qid(<<Type:1/?int, Version:4/?int, Path:8/?int>>) -> 271 + #{type => Type, version => Version, path => Path}. 272 + 273 + qid_to_binary(#{type := Type, version := Version, path := Path}) -> 274 + <<Type:1/?int, Version:4/?int, Path:8/?int>>.
+43
src/e9p_proto.erl
··· 1 + -module(e9p_proto). 2 + 3 + -behaviour(ranch_protocol). 4 + 5 + -export([start_link/3]). 6 + 7 + -export([init/1, loop/3]). 8 + 9 + -include("e9p_internal.hrl"). 10 + -include_lib("kernel/include/logger.hrl"). 11 + 12 + start_link(Ref, Transport, Opts) -> 13 + Pid = proc_lib:spawn_link(?MODULE, init, [{Ref, Transport, Opts}]), 14 + {ok, Pid}. 15 + 16 + init({Ref, Transport, Opts}) -> 17 + {ok, Socket} = ranch:handshake(Ref), 18 + ?MODULE:loop(Socket, Transport, Opts). 19 + 20 + loop(Socket, Transport, Opts) -> 21 + Timeout = 300000, 22 + case Transport:recv(Socket, 7, Timeout) of 23 + {ok, <<Size:4/?int, Type:1/?int, Tag:2/?int>>} -> 24 + {ok, Data} = Transport:recv(Socket, Size - 7, 0), 25 + {ok, #{type := TType, data := TMsg}} = e9p_msg:parse(Type, Data), 26 + ?LOG_DEBUG("-> ~4.16.0B: ~s ~p~n", [Tag, TType, TMsg]), 27 + {RType, RMsg} = handle_msg(TType, TMsg), 28 + ?LOG_DEBUG("<- ~4.16.0B: ~s ~p~n", [Tag, RType, RMsg]), 29 + Resp = e9p_msg:encode(Tag, RType, RMsg), 30 + Transport:send(Socket, Resp), 31 + ?MODULE:loop(Socket, Transport, Opts); 32 + _Other -> 33 + ok = Transport:close(Socket) 34 + end. 35 + 36 + handle_msg(tversion, #{version := <<"9P2000">>, max_packet_size := MP}) -> 37 + {rversion, #{version => <<"9P2000">>, max_packet_size => MP}}; 38 + handle_msg(tversion, #{version := Version}) -> 39 + {rerror, #{error => ["Unsupported version: ", Version]}}; 40 + handle_msg(tattach, _) -> 41 + {rattach, #{qid => #{type => 0, version => 0, path => 0}}}; 42 + handle_msg(Msg, _) -> 43 + {rerror, #{error => io_lib:format("Unsupported message ~s", [Msg])}}.
+7
src/e9p_server.erl
··· 1 + -module(e9p_server). 2 + 3 + -export([start/1]). 4 + 5 + start(Opts) -> 6 + ranch:start_listener(e9p, ranch_tcp, #{socket_opts => [{port, 9999}]}, 7 + e9p_proto, Opts).
+37
src/e9p_transport.erl
··· 1 + -module(e9p_transport). 2 + 3 + -include("e9p_internal.hrl"). 4 + 5 + -export([send/3, read/1, read_stream/1]). 6 + 7 + send(Socket, Tag, Message) -> 8 + Encoded = e9p_msg:encode(Tag, Message), 9 + gen_tcp:send(Socket, Encoded). 10 + 11 + read(Socket) -> 12 + case gen_tcp:recv(Socket, 7) of 13 + {ok, <<Size:4/?int, Type, Tag:2/?int>>} -> 14 + case gen_tcp:recv(Socket, Size - 7) of 15 + {ok, Data} -> 16 + case e9p_msg:parse(Type, Data) of 17 + {ok, Msg} -> 18 + {ok, Tag, Msg}; 19 + {error, _} = Error -> 20 + Error 21 + end; 22 + {error, _} = Error -> 23 + Error 24 + end; 25 + {error, _} = Error -> 26 + Error 27 + end. 28 + 29 + read_stream(<<Size:4/?int, Type, Tag:2/?int, Data:(Size - 7)/binary, Rest/binary>> = Input) -> 30 + case e9p_msg:parse(Type, Data) of 31 + {ok, Msg} -> 32 + {ok, Tag, Msg, Rest}; 33 + {error, Error} -> 34 + {error, Error, Input} 35 + end; 36 + read_stream(Input) -> 37 + {more, Input}.
+13
src/e9p_utils.erl
··· 1 + -module(e9p_utils). 2 + 3 + -export([normalize_path/1]). 4 + 5 + normalize_path(List) -> normalize_path(List, []). 6 + 7 + normalize_path([], Acc) -> lists:reverse(Acc); 8 + normalize_path([Dot | Rest], Acc) 9 + when Dot =:= "." orelse Dot =:= <<".">> 10 + -> 11 + normalize_path(Rest, Acc); 12 + normalize_path([P | Rest], Acc) -> 13 + normalize_path(Rest, [P | Acc]).