diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..952492b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.rebar3 +_build +_checkouts +_vendor +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +.idea +*.iml +rebar3.crashdump +*~ +/doc diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cbc6257 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,186 @@ +# Apache License +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +## 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +## 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +## 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +## 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +1. You must give any other recipients of the Work or Derivative Works a copy of + this License; and + +2. You must cause any modified files to carry prominent notices stating that + You changed the files; and + +3. You must retain, in the Source form of any Derivative Works that You + distribute, all copyright, patent, trademark, and attribution notices from + the Source form of the Work, excluding those notices that do not pertain to + any part of the Derivative Works; and + +4. If the Work includes a "NOTICE" text file as part of its distribution, then + any Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those + notices that do not pertain to any part of the Derivative Works, in at least + one of the following places: within a NOTICE text file distributed as part + of the Derivative Works; within the Source form or documentation, if + provided along with the Derivative Works; or, within a display generated by + the Derivative Works, if and wherever such third-party notices normally + appear. The contents of the NOTICE file are for informational purposes only + and do not modify the License. You may add Your own attribution notices + within Derivative Works that You distribute, alongside or as an addendum to + the NOTICE text from the Work, provided that such additional attribution + notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +## 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +## 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +## 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, NON- +INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +## 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +## 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023, Ɓukasz Niemier . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0d62c1 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# e9p + +Implementation of [9p2000][] file protocol in Erlang + +## Goals + +- [x] Message parsing +- [ ] Client implementation + + [x] Establishing connection + + [ ] Tree walking + + [ ] IO server implementation for reading/writing files + + [ ] File/directory creation + + [ ] File/directory deletion + + [ ] File stats +- [ ] Server implementation + + [x] Establishing connection + + [ ] Tree walking + + [ ] File/directory creation + + [ ] File/directory deletion + + [ ] File stats + + [ ] Customisable FS implementations + +### Example FS + +- [ ] "Passthrough" - which will simply allow accessing some "real" directory in + system FS +- [ ] ErlProcFS - which will expose Erlang process tree and other internal data + via API similar to `procfs` from Linux + +## Reasoning + +I want to implement `procfs`-like API for Erlang to allow non-Erlang-fluent +operators to navigate through Erlang processes. There is something similar +implemented in [`fuserl`][fuserl], but that uses [`libfuse`][libfuse], which is +[NIF][] implemented. That requires compilation of native code and can be +problematic wrt cross compilation and stuff. On the other hand [9p2000][] is +network protocol that can be implemented fully in Erlang, thus do not require +any additional tools or compilation steps. It can also be accessed remotely if +needed. + +[9p2000]: http://ericvh.github.io/9p-rfc/rfc9p2000.html +[fuserl]: https://github.com/tonyrog/fuserl +[libfuse]: https://github.com/libfuse/libfuse +[NIF]: https://www.erlang.org/doc/system/nif.html# diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..e0eebc9 --- /dev/null +++ b/rebar.config @@ -0,0 +1,4 @@ +{erl_opts, [debug_info]}. +{deps, [ranch]}. + +{project_plugins, [rebar3_ex_doc]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..66b1272 --- /dev/null +++ b/rebar.lock @@ -0,0 +1,8 @@ +{"1.2.0", +[{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}]}. +[ +{pkg_hash,[ + {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}]}, +{pkg_hash_ext,[ + {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}]} +]. diff --git a/src/e9p.app.src b/src/e9p.app.src new file mode 100644 index 0000000..8f2d383 --- /dev/null +++ b/src/e9p.app.src @@ -0,0 +1,15 @@ +{application, e9p, + [{description, "An OTP library"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, + [kernel, + stdlib, + ranch + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache-2.0"]}, + {links, []} + ]}. diff --git a/src/e9p.erl b/src/e9p.erl new file mode 100644 index 0000000..a2e246e --- /dev/null +++ b/src/e9p.erl @@ -0,0 +1,13 @@ +-module(e9p). + +-export([]). + +-export_type([qid/0, fid/0]). + +-type qid() :: #{ + type => integer(), + version => integer(), + path => integer() + }. + +-type fid() :: 16#00000000..16#FFFFFFFF. diff --git a/src/e9p_client.erl b/src/e9p_client.erl new file mode 100644 index 0000000..e72562e --- /dev/null +++ b/src/e9p_client.erl @@ -0,0 +1,94 @@ +-module(e9p_client). + +-include("e9p_internal.hrl"). +-include_lib("kernel/include/logger.hrl"). + +-behaviour(gen_server). + +-export([attach/3]). +-export([start_link/3]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2]). + +attach(Client, Uname, Aname) -> + gen_server:call(Client, {attach, noauth, Uname, Aname}). + +start_link(Host, Port, Opts) -> + gen_server:start_link(?MODULE, {Host, Port, Opts}, []). + +init({Host, Port, _Opts}) -> + {ok, Socket} = gen_tcp:connect(Host, Port, [{active, false}, binary]), + case version_negotiation(Socket) of + {ok, #{max_packet_size := MaxPacketSize, version := ?version}} -> + inet:setopts(Socket, [{active, once}]), + {ok, + #{socket => Socket, + buffer => <<>>, + tag => 0, + fid => 0, + msgs => #{}, + max_packet_size => MaxPacketSize, + version => ?version}}; + {ok, _, #{version := OtherVersion}} -> + {error, {unsupported_version, OtherVersion}}; + {error, _} = Error -> + Error + end. + +handle_call({attach, Auth, Uname, Aname}, From, State) -> + #{tag := Tag, + fid := Fid, + socket := Socket, + msgs := Msgs} = + State, + Afid = + case Auth of + noauth -> + ?nofid; + Id -> + Id + end, + Msg = #{type => tattach, + data => + #{fid => Fid, + afid => Afid, + uname => Uname, + aname => Aname}}, + e9p_transport:send(Socket, Tag, Msg), + {noreply, + State#{tag := Tag + 1, + fid := Fid + 1, + msgs := Msgs#{Tag => {From, #{fid => Fid}}}}}; +handle_call(_Msg, _From, State) -> + {reply, {error, not_implemented}, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({tcp, Socket, Data}, #{socket := Socket} = State) -> + #{buffer := Buffer, msgs := Msgs0} = State, + case e9p_transport:read_stream(<>) of + {ok, Tag, Msg, Rest} -> + Msgs = + case maps:take(Tag, Msgs0) of + {{From, _}, M} -> + gen_server:reply(From, {ok, Msg}), + M; + error -> + ?LOG_WARNING("Unknown tag ~p", [Tag]), + Msgs0 + end, + {noreply, State#{buffer := Rest, msgs := Msgs}}; + {more, Data} -> + {noreply, State#{buffer := Data}} + end. + +version_negotiation(Socket) -> + Msg = #{type => tversion, + data => #{max_packet_size => ?max_packet_size, version => ?version}}, + e9p_transport:send(Socket, notag, Msg), + case e9p_transport:read(Socket) of + {ok, _, #{type := rversion, data := Resp}} -> + {ok, Resp}; + {error, _} = Error -> + Error + end. diff --git a/src/e9p_fs.erl b/src/e9p_fs.erl new file mode 100644 index 0000000..ff217c5 --- /dev/null +++ b/src/e9p_fs.erl @@ -0,0 +1,89 @@ +%% @doc Definition of 9p filesystem +%% @end +-module(e9p_fs). + +% -behaviour(gen_server). + +-export([start_link/2, walk/3]). + +-export([init/1, handle_call/3]). + +-export_type([fs/0, state/0]). + +-opaque fs() :: pid(). + +-type state() :: term(). + +%% Setup state for given filesystem. +-callback init(term()) -> state(). + +%% Returns `QID' for root node. +%% +%% If implementation provides multiple trees then the `AName' will be set to the +%% tree defined by the client. It is left to the implementation to ensure the +%% constraints of the file root (aka `walk(Root, "..", State0) =:= {Root, State1}'. +-callback root(AName :: unicode:chardata(), state()) -> {e9p:qid(), state()}. + +%% Walk through the given path starting at the `QID' +-callback walk(QID :: e9p:qid(), unicode:chardata(), state()) -> + {e9p:qid() | false, state()}. + +%% Return stat data for file indicated by `QID' +-callback stat(QID :: e9p:qid(), state()) -> {ok, map(), state()} | {error, term(), state()}. + +%% Read data from file indicated by `QID' +-callback read(QID :: e9p:qid(), + Offset :: non_neg_integer(), + Length :: non_neg_integer(), + state()) -> {ok, iodata(), state()} | {error, term(), state()}. + +%% Write data to file indicated by `QID' +-callback write(QID :: e9p:qid(), + Offset :: non_neg_integer(), + Data :: iodata(), + state()) -> {ok, non_neg_integer(), state()} | {error, term(), state()}. + +%% @doc Walk through the filesystem. +%% +%% Walks through `List' entries starting at `QID'. It will stop at first path +%% that cannot be walked into. This mean, that returned list length will be +%% equal or less than the `length(List)'. +-spec walk(fs(), e9p:qid(), [unicode:chardata()]) -> {ok, [e9p:qid()]} | {error, term()}. +walk(FS, QID, List) -> + List0 = e9p_utils:normalize_path(List, []), + case List of + [] -> {ok, []}; + [_|_] -> gen_server:call(FS, {walk, QID, List0}) + end. + +%% @private +start_link(Impl, Init) -> + gen_server:start_link(?MODULE, {Impl, Init}, []). + +%% @private +init({Impl, Init}) -> + case Impl:init(Init) of + {ok, State0} -> + {ok, #{mod => Impl, state => State0}}; + {error, _} = Error -> + Error + end. + +%% @private +handle_call({walk, QID, List}, _From, #{mod := Mod, state := State0}) -> + {QIDs, State} = do_walk(Mod, QID, List, State0), + {reply, {ok, QIDs}, State}. + +%% Walk through the FS tree. +do_walk(Mod, QID, List, State) -> + do_walk(Mod, QID, List, State, []). + +do_walk(_Mod, _QID, [], State, Acc) -> + {lists:reverse(Acc), State}; +do_walk(Mod, QID0, [P | Rest], State0, Acc) -> + case Mod:walk(QID0, P, State0) of + {false, State} -> + {lists:reverse(Acc), State}; + {QID1, State} -> + do_walk(Mod, QID1, Rest, State, [QID1 | Acc]) + end. diff --git a/src/e9p_internal.hrl b/src/e9p_internal.hrl new file mode 100644 index 0000000..205af27 --- /dev/null +++ b/src/e9p_internal.hrl @@ -0,0 +1,40 @@ +-define(version, <<"9P2000">>). + +-define(notag, 16#FFFF). +-define(nofid, 16#FFFFFFFF). + +-define(max_packet_size, 8168). + +-define(int, little-unsigned-unit:8). +-define(len, 2/?int). + +%% Requests +-define(Tversion, 100). +-define(Tauth, 102). +-define(Tattach, 104). +-define(Tflush, 108). +-define(Twalk, 110). +-define(Topen, 112). +-define(Tcreate, 114). +-define(Tread, 116). +-define(Twrite, 118). +-define(Tclunk, 120). +-define(Tremove, 122). +-define(Tstat, 124). +-define(Twstat, 126). + +%% Responses +-define(Rversion, 101). +-define(Rauth, 103). +-define(Rattach, 105). +-define(Rerror, 107). +-define(Rflush, 109). +-define(Rwalk, 111). +-define(Ropen, 113). +-define(Rcreate, 115). +-define(Rread, 117). +-define(Rwrite, 119). +-define(Rclunk, 121). +-define(Rremove, 123). +-define(Rstat, 125). +-define(Rwstat, 127). diff --git a/src/e9p_io_server.erl b/src/e9p_io_server.erl new file mode 100644 index 0000000..434a54f --- /dev/null +++ b/src/e9p_io_server.erl @@ -0,0 +1,28 @@ +%% @hidden + +-module(e9p_io_server). + +-behaviour(gen_server). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/2]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2]). + +start_link(QID, Client) -> + gen_server:start_link(?MODULE, {QID, Client}, []). + +init({QID, Client}) -> + {ok, {QID, Client}}. + +handle_call(_Msg, _From, State) -> + {reply, notsupported, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({io_request, From, ReplyAs, Request}, State) -> + ?LOG_NOTICE("~p", [Request]), + From ! {io_reply, ReplyAs, {error, notimplemented}}, + {noreply, State}. diff --git a/src/e9p_msg.erl b/src/e9p_msg.erl new file mode 100644 index 0000000..fc95156 --- /dev/null +++ b/src/e9p_msg.erl @@ -0,0 +1,274 @@ +%% @doc Protocol messages parsing and encoding. +%% @end +-module(e9p_msg). + +-export([parse/2, encode/2, encode/3]). + +-export_type([tag/0, + message_type/0, + request_message_type/0, + response_message_type/0, + message/0]). + +-include("e9p_internal.hrl"). + +-type tag() :: 16#0000..16#FFFF. + +-type request_message_type() :: + tversion | + tauth | + tattach | + tflush | + twalk | + topen | + tcreate | + tread | + twrite | + tclunk | + tremove | + tstat | + twstat. + +-type response_message_type() :: + rversion | + rauth | + rattach | + rerror | + rflush | + rwalk | + ropen | + rcreate | + rread | + rwrite | + rclunk | + rstat | + rwstat. + +-type message_type() :: request_message_type() | response_message_type(). + +-type message() :: #{type => message_type(), + data => map()}. + +-spec parse(Type :: byte(), Message :: binary()) -> {ok, message()} | {error, term()}. +parse(Type, Data) -> + case do_parse(Type, Data) of + {ok, T, Parsed} -> + {ok, #{type => T, data => Parsed}}; + + {error, Reason} -> + {error, Reason} + end. + +%% version - negotiate protocol version +do_parse(?Tversion, <>) -> + {ok, tversion, #{max_packet_size => MSize, version => Version}}; +do_parse(?Rversion, <>) -> + {ok, rversion, #{max_packet_size => MSize, version => Version}}; + +%% attach, auth - messages to establish a connection +do_parse(?Tauth, <>) -> + {ok, tauth, #{afid => AFID, + uname => Uname, + aname => Aname}}; +do_parse(?Rauth, <>) -> + {ok, rauth, #{aqid => binary_to_qid(AQID)}}; + +do_parse(?Tattach, <>) -> + {ok, tattach, #{fid => FID, + afid => AFID, + uname => Uname, + aname => Aname}}; +do_parse(?Rattach, <>) -> + {ok, rattach, #{qid => binary_to_qid(QID)}}; + +%% clunk - forget about a fid +do_parse(?Tclunk, <>) -> + {ok, tclunk, #{fid => FID}}; +do_parse(?Rclunk, <<>>) -> + {ok, rclunk, #{}}; + +%% error - return an error +do_parse(?Rerror, <>) -> + {ok, rerror, #{error => Error}}; + +%% flush - abort a message +do_parse(?Tflush, <>) -> + {ok, tflush, #{tag => Tag}}; +do_parse(?Rflush, <<>>) -> + {ok, rflush, #{}}; + +%% open, create - prepare a fid for I/O on an existing or new file +do_parse(?Topen, <>) -> + {ok, topen, #{fid => FID, mode => Mode}}; +do_parse(?Ropen, <>) -> + {ok, ropen, #{qid => binary_to_qid(QID), io_unit => IOUnit}}; + +do_parse(?Tcreate, <>) -> + {ok, tcreate, #{fid => FID, name => Name, perm => Perm, mode => Mode}}; +do_parse(?Rcreate, <>) -> + {ok, rcreate, #{qid => binary_to_qid(QID), io_unit => IOUnit}}; + +%% remove - remove a file from a server +do_parse(?Tremove, <>) -> + {ok, tremove, #{fid => FID}}; +do_parse(?Rremove, <<>>) -> + {ok, rremove, #{}}; + +%% stat, wstat - inquire or change file attributes +do_parse(?Tstat, <>) -> + {ok, tstat, #{fid => FID}}; +do_parse(?Rstat, <>) -> + case parse_stat(Data) of + {ok, Stat} -> + {ok, rstat, #{stat => Stat}}; + + {error, _} = Error -> + Error + end; + +do_parse(?Twstat, <>) -> + case parse_stat(Data) of + {ok, Stat} -> + {ok, twstat, #{fid => FID, stat => Stat}}; + + {error, _} = Error -> + Error + end; +do_parse(?Rwstat, <<>>) -> + {ok, rwstat, #{}}; + +%% walk - descend a directory hierarchy +do_parse(?Twalk, <>) -> + NWNames = [Name || <> <= Rest], + Len = length(NWNames), + if + Len == NWNLen -> + {ok, twalk, #{fid => FID, new_fid => NewFID, names => NWNames}}; + true -> + {error, {invalid_walk_length, NWNLen, Len}} + end; +do_parse(?Rwalk, <>) -> + {ok, rwalk, #{qids => [binary_to_qid(QID) || <> <= QIDs]}}; + +do_parse(Type, Data) -> + {error, {invalid_message, Type, Data}}. + +parse_stat(<<_Size:2/?int, + Type:2/?int, + Dev:4/?int, + QID:13/binary, + Mode:4/?int, + Atime:4/?int, + Mtime:4/?int, + Len:8/?int, + NLen:?len, Name:NLen/binary, + ULen:?len, Uid:ULen/binary, + GLen:?len, Gid:GLen/binary, + MULen:?len, MUid:MULen/binary>>) +-> + {ok, #{ + type => Type, + dev => Dev, + qid => binary_to_qid(QID), + mode => Mode, + atime => calendar:system_time_to_universal_time(Atime, seconds), + mtime => calendar:system_time_to_universal_time(Mtime, seconds), + length => Len, + name => Name, + uid => Uid, + gid => Gid, + muid => MUid + }}; +parse_stat(_) -> {error, invalid_stat_data}. + +-spec encode(Tag :: tag() | notag, Message :: message()) -> iodata(). +encode(Tag, #{type := Type, data := Data}) -> + encode(Tag, Type, Data). + +-spec encode(Tag :: tag() | notag, Type :: message_type(), Data :: map()) -> iodata(). +encode(Tag, Type, Data) -> + {MT, Encoded} = do_encode(Type, Data), + Tag0 = case Tag of + notag -> ?notag; + V -> V + end, + Size = iolist_size(Encoded) + 7, + [<> | Encoded]. + +do_encode(tversion, #{max_packet_size := MSize, version := Version}) -> + {?Tversion, [<> | encode_str(Version)]}; +do_encode(rversion, #{max_packet_size := MSize, version := Version}) -> + {?Rversion, [<> | encode_str(Version)]}; + +do_encode(tauth, #{afid := AFID, uname := Uname, aname := Aname}) -> + {?Tauth, [<>, encode_str(Uname), encode_str(Aname)]}; +do_encode(rauth, #{aqid := AQID}) -> + {?Rauth, qid_to_binary(AQID)}; + +do_encode(tattach, #{fid := FID, afid := AFID, uname := Uname, aname := Aname}) -> + {?Tattach, [<>, encode_str(Uname), encode_str(Aname)]}; +do_encode(rattach, #{qid := QID}) -> + {?Rattach, qid_to_binary(QID)}; + +do_encode(tclunk, #{fid := FID}) -> + {?Tclunk, <>}; +do_encode(rclunk, _) -> + {?Rclunk, []}; + +do_encode(rerror, #{error := Error}) -> + {?Rerror, encode_str(Error)}; + +do_encode(tflush, #{tag := Tag}) -> + {?Tflush, <>}; +do_encode(rflush, _) -> + {?Rflush, []}; + +do_encode(topen, #{fid := FID, mode := Mode}) -> + {?Topen, <>}; +do_encode(ropen, #{qid := QID, io_unit := IOUnit}) -> + {?Ropen, [qid_to_binary(QID), <>]}; + +do_encode(tcreate, #{fid := FID, name := Name, perm := Perm, mode := Mode}) -> + {?Tcreate, [<>, encode_str(Name), <>]}; +do_encode(rcreate, #{qid := QID, io_unit := IOUnit}) -> + {?Rcreate, [qid_to_binary(QID), <>]}; + +do_encode(tremove, #{fid := FID}) -> + {?Tremove, <>}; +do_encode(rremove, _) -> + {?Rremove, []}; + +do_encode(tstat, #{fid := FID}) -> + {?Tstat, <>}; +do_encode(rstat, _Data) -> + error(unimplemented); + +do_encode(twstat, #{fid := _FID, stat := _Stat}) -> + error(unimplemented); + +do_encode(twalk, #{fid := FID, new_fid := NewFID, names := Names}) -> + ENames = [encode_str(Name) || Name <- Names], + Len = length(ENames), + {?Twalk, [<> | ENames]}; +do_encode(rwalk, #{qids := QIDs}) -> + EQIDs = [qid_to_binary(QID) || QID <- QIDs], + Len = length(EQIDs), + {?Rwalk, [<> | EQIDs]}. + +encode_str(Data) -> + Len = iolist_size(Data), + [<> | Data]. + +binary_to_qid(<>) -> + #{type => Type, version => Version, path => Path}. + +qid_to_binary(#{type := Type, version := Version, path := Path}) -> + <>. diff --git a/src/e9p_proto.erl b/src/e9p_proto.erl new file mode 100644 index 0000000..ef64213 --- /dev/null +++ b/src/e9p_proto.erl @@ -0,0 +1,43 @@ +-module(e9p_proto). + +-behaviour(ranch_protocol). + +-export([start_link/3]). + +-export([init/1, loop/3]). + +-include("e9p_internal.hrl"). +-include_lib("kernel/include/logger.hrl"). + +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, init, [{Ref, Transport, Opts}]), + {ok, Pid}. + +init({Ref, Transport, Opts}) -> + {ok, Socket} = ranch:handshake(Ref), + ?MODULE:loop(Socket, Transport, Opts). + +loop(Socket, Transport, Opts) -> + Timeout = 300000, + case Transport:recv(Socket, 7, Timeout) of + {ok, <>} -> + {ok, Data} = Transport:recv(Socket, Size - 7, 0), + {ok, #{type := TType, data := TMsg}} = e9p_msg:parse(Type, Data), + ?LOG_DEBUG("-> ~4.16.0B: ~s ~p~n", [Tag, TType, TMsg]), + {RType, RMsg} = handle_msg(TType, TMsg), + ?LOG_DEBUG("<- ~4.16.0B: ~s ~p~n", [Tag, RType, RMsg]), + Resp = e9p_msg:encode(Tag, RType, RMsg), + Transport:send(Socket, Resp), + ?MODULE:loop(Socket, Transport, Opts); + _Other -> + ok = Transport:close(Socket) + end. + +handle_msg(tversion, #{version := <<"9P2000">>, max_packet_size := MP}) -> + {rversion, #{version => <<"9P2000">>, max_packet_size => MP}}; +handle_msg(tversion, #{version := Version}) -> + {rerror, #{error => ["Unsupported version: ", Version]}}; +handle_msg(tattach, _) -> + {rattach, #{qid => #{type => 0, version => 0, path => 0}}}; +handle_msg(Msg, _) -> + {rerror, #{error => io_lib:format("Unsupported message ~s", [Msg])}}. diff --git a/src/e9p_server.erl b/src/e9p_server.erl new file mode 100644 index 0000000..bde4b1c --- /dev/null +++ b/src/e9p_server.erl @@ -0,0 +1,7 @@ +-module(e9p_server). + +-export([start/1]). + +start(Opts) -> + ranch:start_listener(e9p, ranch_tcp, #{socket_opts => [{port, 9999}]}, + e9p_proto, Opts). diff --git a/src/e9p_transport.erl b/src/e9p_transport.erl new file mode 100644 index 0000000..fa557b8 --- /dev/null +++ b/src/e9p_transport.erl @@ -0,0 +1,37 @@ +-module(e9p_transport). + +-include("e9p_internal.hrl"). + +-export([send/3, read/1, read_stream/1]). + +send(Socket, Tag, Message) -> + Encoded = e9p_msg:encode(Tag, Message), + gen_tcp:send(Socket, Encoded). + +read(Socket) -> + case gen_tcp:recv(Socket, 7) of + {ok, <>} -> + case gen_tcp:recv(Socket, Size - 7) of + {ok, Data} -> + case e9p_msg:parse(Type, Data) of + {ok, Msg} -> + {ok, Tag, Msg}; + {error, _} = Error -> + Error + end; + {error, _} = Error -> + Error + end; + {error, _} = Error -> + Error + end. + +read_stream(<> = Input) -> + case e9p_msg:parse(Type, Data) of + {ok, Msg} -> + {ok, Tag, Msg, Rest}; + {error, Error} -> + {error, Error, Input} + end; +read_stream(Input) -> + {more, Input}. diff --git a/src/e9p_utils.erl b/src/e9p_utils.erl new file mode 100644 index 0000000..74739c9 --- /dev/null +++ b/src/e9p_utils.erl @@ -0,0 +1,13 @@ +-module(e9p_utils). + +-export([normalize_path/1]). + +normalize_path(List) -> normalize_path(List, []). + +normalize_path([], Acc) -> lists:reverse(Acc); +normalize_path([Dot | Rest], Acc) + when Dot =:= "." orelse Dot =:= <<".">> + -> + normalize_path(Rest, Acc); +normalize_path([P | Rest], Acc) -> + normalize_path(Rest, [P | Acc]).