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

Hard refactoring to remove Ranch dependency

hauleth.dev e0dea516 06664e6b

verified
+410 -200
+10 -2
rebar.config
··· 3 3 % SPDX-License-Identifier: Apache-2.0 4 4 5 5 {erl_opts, [debug_info]}. 6 - {deps, [ranch]}. 6 + {deps, [telemetry]}. 7 7 8 - {project_plugins, [rebar3_ex_doc]}. 8 + {project_plugins, [ 9 + rebar3_ex_doc, 10 + rebar3_proper 11 + ]}. 12 + 13 + {profiles, [{test, [ 14 + {deps, [proper]}, 15 + {erlc_flags, [nowarn_export_all]} 16 + ]}]}.
+3 -3
rebar.lock
··· 1 1 {"1.2.0", 2 - [{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}]}. 2 + [{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.3.0">>},0}]}. 3 3 [ 4 4 {pkg_hash,[ 5 - {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}]}, 5 + {<<"telemetry">>, <<"FEDEBBAE410D715CF8E7062C96A1EF32EC22E764197F70CDA73D82778D61E7A2">>}]}, 6 6 {pkg_hash_ext,[ 7 - {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}]} 7 + {<<"telemetry">>, <<"7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6">>}]} 8 8 ].
+21 -4
src/e9p.erl
··· 4 4 5 5 -module(e9p). 6 6 7 - -export([]). 7 + -export([make_qid/4]). 8 8 9 9 -export_type([qid/0, fid/0]). 10 10 11 + -export_type([u8/0, u16/0, u32/0, u64/0]). 12 + 13 + -type u8() :: 16#00..16#FF. 14 + -type u16() :: 16#0000..16#FFFF. 15 + -type u32() :: 16#00000000..16#FFFFFFFF. 16 + -type u64() :: 16#0000000000000000..16#FFFFFFFFFFFFFFFF. 17 + 11 18 -type qid() :: #{ 12 - type => integer(), 13 - version => integer(), 14 - path => integer() 19 + type => u8(), 20 + version => u16(), 21 + path => u64(), 22 + state => term() 15 23 }. 16 24 17 25 -type fid() :: 16#00000000..16#FFFFFFFF. 26 + 27 + %-spec make_qid() 28 + make_qid(Type, Version, Path, State) -> 29 + #{ 30 + type => e9p_utils:qtype_from_atom(Type), 31 + version => Version, 32 + path => Path, 33 + state => State 34 + }.
+7 -10
src/e9p_client.erl
··· 51 51 Id -> 52 52 Id 53 53 end, 54 - Msg = #{type => tattach, 55 - data => 56 - #{fid => Fid, 54 + Msg = #{fid => Fid, 57 55 afid => Afid, 58 56 uname => Uname, 59 - aname => Aname}}, 60 - e9p_transport:send(Socket, Tag, Msg), 57 + aname => Aname}, 58 + e9p_transport:send(Socket, Tag, tattach, Msg), 61 59 {noreply, 62 60 State#{tag := Tag + 1, 63 61 fid := Fid + 1, ··· 71 69 handle_info({tcp, Socket, Data}, #{socket := Socket} = State) -> 72 70 #{buffer := Buffer, msgs := Msgs0} = State, 73 71 case e9p_transport:read_stream(<<Buffer/binary, Data/binary>>) of 74 - {ok, Tag, Msg, Rest} -> 72 + {ok, Tag, _Type, Msg, Rest} -> 75 73 Msgs = 76 74 case maps:take(Tag, Msgs0) of 77 75 {{From, _}, M} -> ··· 87 85 end. 88 86 89 87 version_negotiation(Socket) -> 90 - Msg = #{type => tversion, 91 - data => #{max_packet_size => ?max_packet_size, version => ?version}}, 92 - e9p_transport:send(Socket, notag, Msg), 88 + Msg = #{max_packet_size => ?max_packet_size, version => ?version}, 89 + e9p_transport:send(Socket, notag, tversion, Msg), 93 90 case e9p_transport:read(Socket) of 94 - {ok, _, #{type := rversion, data := Resp}} -> 91 + {ok, _, rversion, Resp} -> 95 92 {ok, Resp}; 96 93 {error, _} = Error -> 97 94 Error
+112 -48
src/e9p_fs.erl
··· 6 6 %% @end 7 7 -module(e9p_fs). 8 8 9 - % -behaviour(gen_server). 10 - 11 - -export([start_link/2, walk/3]). 12 - 13 - -export([init/1, handle_call/3]). 14 - 15 - -export_type([fs/0, state/0]). 9 + -export([ 10 + init/1, 11 + root/2, 12 + walk/3, 13 + open/2, 14 + create/5, 15 + read/4, 16 + write/4, 17 + clunk/2, 18 + remove/2, 19 + stat/2, 20 + wstat/3 21 + ]). 16 22 17 - -opaque fs() :: pid(). 23 + -export_type([state/0]). 18 24 19 25 -type state() :: term(). 26 + -type result() :: {ok, state()} | {error, term(), state()}. 27 + -type result(T) :: {ok, T, state()} | {error, term(), state()}. 28 + 29 + -define(if_supported(Code), 30 + case erlang:function_exported(Mod, ?FUNCTION_NAME, ?FUNCTION_ARITY) of 31 + true -> 32 + case (fun() -> Code end)() of 33 + {ok, Ret, {Mod, NewState}} -> 34 + {ok, Ret, {Mod, NewState}}; 35 + {error, Error, NewState} -> 36 + {error, Error, {Mod, NewState}} 37 + end; 38 + false -> {error, nosupport, {Mod, State}} 39 + end). 20 40 21 41 %% Setup state for given filesystem. 22 - -callback init(term()) -> state(). 42 + -callback init(term()) -> 43 + {ok, state()} | 44 + {error, Reason :: term()}. 23 45 24 46 %% Returns `QID' for root node. 25 47 %% 26 48 %% If implementation provides multiple trees then the `AName' will be set to the 27 49 %% tree defined by the client. It is left to the implementation to ensure the 28 50 %% constraints of the file root (aka `walk(Root, "..", State0) =:= {Root, State1}'. 29 - -callback root(AName :: unicode:chardata(), state()) -> {e9p:qid(), state()}. 51 + -callback root(AName :: unicode:chardata(), state()) -> {ok, e9p:qid(), state()}. 52 + 53 + -callback flush(state()) -> {ok, state()} | {error, term(), state()}. 30 54 31 55 %% Walk through the given path starting at the `QID' 32 56 -callback walk(QID :: e9p:qid(), unicode:chardata(), state()) -> 33 57 {e9p:qid() | false, state()}. 34 58 35 - %% Return stat data for file indicated by `QID' 36 - -callback stat(QID :: e9p:qid(), state()) -> {ok, map(), state()} | {error, term(), state()}. 59 + -callback open(QID :: e9p:qid(), state()) -> result(e9p:u32()). 60 + 61 + -callback create(QID :: e9p:qid(), 62 + Name :: unicode:chardata(), 63 + Perm :: e9p:u32(), 64 + Mode :: e9p:u8(), 65 + state()) -> result({e9p:qid(), e9p:u32()}). 37 66 38 67 %% Read data from file indicated by `QID' 39 68 -callback read(QID :: e9p:qid(), 40 69 Offset :: non_neg_integer(), 41 70 Length :: non_neg_integer(), 42 - state()) -> {ok, iodata(), state()} | {error, term(), state()}. 71 + state()) -> result(iodata()). 43 72 44 73 %% Write data to file indicated by `QID' 45 74 -callback write(QID :: e9p:qid(), 46 75 Offset :: non_neg_integer(), 47 76 Data :: iodata(), 48 - state()) -> {ok, non_neg_integer(), state()} | {error, term(), state()}. 77 + state()) -> result(non_neg_integer()). 49 78 50 - %% @doc Walk through the filesystem. 51 - %% 52 - %% Walks through `List' entries starting at `QID'. It will stop at first path 53 - %% that cannot be walked into. This mean, that returned list length will be 54 - %% equal <b>or less</b> than the `length(List)'. 55 - -spec walk(fs(), e9p:qid(), [unicode:chardata()]) -> {ok, [e9p:qid()]} | {error, term()}. 56 - walk(FS, QID, List) -> 57 - List0 = e9p_utils:normalize_path(List, []), 58 - case List of 59 - [] -> {ok, []}; 60 - [_|_] -> gen_server:call(FS, {walk, QID, List0}) 61 - end. 79 + -callback clunk(QID :: e9p:qid(), state()) -> result(). 62 80 63 - %% @private 64 - start_link(Impl, Init) -> 65 - gen_server:start_link(?MODULE, {Impl, Init}, []). 81 + -callback remove(QID :: e9p:qid(), state()) -> result(). 66 82 67 - %% @private 68 - init({Impl, Init}) -> 69 - case Impl:init(Init) of 70 - {ok, State0} -> 71 - {ok, #{mod => Impl, state => State0}}; 72 - {error, _} = Error -> 73 - Error 83 + %% Return stat data for file indicated by `QID' 84 + -callback stat(QID :: e9p:qid(), state()) -> result(map()). 85 + 86 + %% Write stat data for file indicated by `QID' 87 + -callback wstat(QID :: e9p:qid(), map(), state()) -> result(). 88 + 89 + -optional_callbacks([ 90 + flush/1, 91 + walk/3, 92 + open/2, 93 + create/5, 94 + read/4, 95 + write/4, 96 + clunk/2, 97 + remove/2, 98 + stat/2, 99 + wstat/3 100 + ]). 101 + 102 + init({Mod, State}) -> 103 + case Mod:init(State) of 104 + {ok, NewState} -> {ok, {Mod, NewState}}; 105 + Error -> Error 74 106 end. 75 107 76 - %% @private 77 - handle_call({walk, QID, List}, _From, #{mod := Mod, state := State0}) -> 78 - {QIDs, State} = do_walk(Mod, QID, List, State0), 79 - {reply, {ok, QIDs}, State}. 108 + root({Mod, State}, AName) -> 109 + case Mod:root(AName, State) of 110 + {ok, QID, NewState} -> 111 + {ok, QID, {Mod, NewState}} 112 + end. 80 113 81 - %% Walk through the FS tree. 82 - do_walk(Mod, QID, List, State) -> 83 - do_walk(Mod, QID, List, State, []). 114 + -doc """ 115 + Walk through paths starting at QID. 116 + """. 117 + walk({Mod, State}, QID, Paths) when is_atom(Mod) -> 118 + ?if_supported(do_walk(Mod, QID, Paths, State, [])). 84 119 85 - do_walk(_Mod, _QID, [], State, Acc) -> 86 - {lists:reverse(Acc), State}; 120 + do_walk(Mod, QID, [], State, Acc) -> 121 + {ok, QID, lists:reverse(Acc), {Mod, State}}; 87 122 do_walk(Mod, QID0, [P | Rest], State0, Acc) -> 88 123 case Mod:walk(QID0, P, State0) of 89 124 {false, State} -> 90 - {lists:reverse(Acc), State}; 91 - {QID1, State} -> 92 - do_walk(Mod, QID1, Rest, State, [QID1 | Acc]) 125 + {ok, QID0, lists:reverse(Acc), {Mod, State}}; 126 + {QID, State} -> 127 + do_walk(Mod, QID, Rest, State, [QID | Acc]) 93 128 end. 129 + 130 + open({Mod, State}, QID) -> 131 + ?if_supported(Mod:open(QID, State)). 132 + 133 + create({Mod, State}, QID, Name, Perm, Mode) -> 134 + ?if_supported(Mod:create(QID, Name, Perm, Mode, State)). 135 + 136 + read({Mod, State}, QID, Offset, Length) -> 137 + ?if_supported(Mod:read(QID, Offset, Length, State)). 138 + 139 + write({Mod, State}, QID, Offset, Data) -> 140 + ?if_supported(Mod:write(QID, Offset, Data, State)). 141 + 142 + clunk({Mod, State}, QID) -> 143 + case erlang:function_exported(Mod, clunk, 3) of 144 + true -> 145 + {Resp, State} = Mod:clunk(QID, State), 146 + {Resp, {Mod, State}}; 147 + false -> {ok, {Mod, State}} 148 + end. 149 + 150 + remove({Mod, State}, QID) -> 151 + ?if_supported(Mod:remove(QID, State)). 152 + 153 + stat({Mod, State}, QID) -> 154 + ?if_supported(Mod:stat(QID, State)). 155 + 156 + wstat({Mod, State}, QID, Stat) -> 157 + ?if_supported(Mod:wstat(QID, Stat, State)).
+26 -16
src/e9p_internal.hrl
··· 2 2 % 3 3 % SPDX-License-Identifier: Apache-2.0 4 4 5 - -define(version, <<"9P2000">>). 5 + -define(version, ~"9P2000"). 6 6 7 7 -define(notag, 16#FFFF). 8 8 -define(nofid, 16#FFFFFFFF). ··· 12 12 -define(int, little-unsigned-unit:8). 13 13 -define(len, 2/?int). 14 14 15 - %% Requests 16 15 -define(Tversion, 100). 16 + -define(Rversion, 101). 17 + 17 18 -define(Tauth, 102). 19 + -define(Rauth, 103). 20 + 18 21 -define(Tattach, 104). 19 - -define(Tflush, 108). 20 - -define(Twalk, 110). 21 - -define(Topen, 112). 22 - -define(Tcreate, 114). 23 - -define(Tread, 116). 24 - -define(Twrite, 118). 25 - -define(Tclunk, 120). 26 - -define(Tremove, 122). 27 - -define(Tstat, 124). 28 - -define(Twstat, 126). 29 - 30 - %% Responses 31 - -define(Rversion, 101). 32 - -define(Rauth, 103). 33 22 -define(Rattach, 105). 23 + 34 24 -define(Rerror, 107). 25 + 26 + -define(Tflush, 108). 35 27 -define(Rflush, 109). 28 + 29 + -define(Twalk, 110). 36 30 -define(Rwalk, 111). 31 + 32 + -define(Topen, 112). 37 33 -define(Ropen, 113). 34 + 35 + -define(Tcreate, 114). 38 36 -define(Rcreate, 115). 37 + 38 + -define(Tread, 116). 39 39 -define(Rread, 117). 40 + 41 + -define(Twrite, 118). 40 42 -define(Rwrite, 119). 43 + 44 + -define(Tclunk, 120). 41 45 -define(Rclunk, 121). 46 + 47 + -define(Tremove, 122). 42 48 -define(Rremove, 123). 49 + 50 + -define(Tstat, 124). 43 51 -define(Rstat, 125). 52 + 53 + -define(Twstat, 126). 44 54 -define(Rwstat, 127).
-32
src/e9p_io_server.erl
··· 1 - % SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2 - % 3 - % SPDX-License-Identifier: Apache-2.0 4 - 5 - %% @hidden 6 - 7 - -module(e9p_io_server). 8 - 9 - -behaviour(gen_server). 10 - 11 - -include_lib("kernel/include/logger.hrl"). 12 - 13 - -export([start_link/2]). 14 - 15 - -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). 16 - 17 - start_link(QID, Client) -> 18 - gen_server:start_link(?MODULE, {QID, Client}, []). 19 - 20 - init({QID, Client}) -> 21 - {ok, {QID, Client}}. 22 - 23 - handle_call(_Msg, _From, State) -> 24 - {reply, notsupported, State}. 25 - 26 - handle_cast(_Msg, State) -> 27 - {noreply, State}. 28 - 29 - handle_info({io_request, From, ReplyAs, Request}, State) -> 30 - ?LOG_NOTICE("~p", [Request]), 31 - From ! {io_reply, ReplyAs, {error, notimplemented}}, 32 - {noreply, State}.
+59 -21
src/e9p_msg.erl
··· 6 6 %% @end 7 7 -module(e9p_msg). 8 8 9 - -export([parse/2, encode/2, encode/3]). 9 + -export([parse/1, encode/3]). 10 10 11 11 -export_type([tag/0, 12 12 message_type/0, 13 13 request_message_type/0, 14 - response_message_type/0, 15 - message/0]). 14 + response_message_type/0 15 + ]). 16 16 17 17 -include("e9p_internal.hrl"). 18 18 ··· 50 50 51 51 -type message_type() :: request_message_type() | response_message_type(). 52 52 53 - -type message() :: #{type => message_type(), 54 - data => map()}. 55 - 56 - -spec parse(Type :: byte(), Message :: binary()) -> {ok, message()} | {error, term()}. 57 - parse(Type, Data) -> 53 + parse(<<Type:1/?int, Tag:2/?int, Data/binary>>) -> 58 54 case do_parse(Type, Data) of 59 55 {ok, T, Parsed} -> 60 - {ok, #{type => T, data => Parsed}}; 61 - 56 + {ok, Tag, T, Parsed}; 62 57 {error, Reason} -> 63 58 {error, Reason} 64 59 end. ··· 161 156 end; 162 157 do_parse(?Rwalk, <<NWQLen:?len, QIDs:(NWQLen * 13)/binary>>) -> 163 158 {ok, rwalk, #{qids => [binary_to_qid(QID) || <<QID:13/binary>> <= QIDs]}}; 159 + 160 + do_parse(?Tread, <<FID:4/?int, Offset:8/?int, Count:4/?int>>) -> 161 + {ok, tread, #{fid => FID, offset => Offset, count => Count}}; 162 + do_parse(?Rread, <<Count:4/?int, Data:Count/?int>>) -> 163 + {ok, rread, #{data => Data}}; 164 164 165 165 do_parse(Type, Data) -> 166 166 {error, {invalid_message, Type, Data}}. ··· 193 193 }}; 194 194 parse_stat(_) -> {error, invalid_stat_data}. 195 195 196 - -spec encode(Tag :: tag() | notag, Message :: message()) -> iodata(). 197 - encode(Tag, #{type := Type, data := Data}) -> 198 - encode(Tag, Type, Data). 199 - 200 196 -spec encode(Tag :: tag() | notag, Type :: message_type(), Data :: map()) -> iodata(). 201 197 encode(Tag, Type, Data) -> 202 198 {MT, Encoded} = do_encode(Type, Data), ··· 204 200 notag -> ?notag; 205 201 V -> V 206 202 end, 207 - Size = iolist_size(Encoded) + 7, 208 - [<<Size:4/?int, MT:1/?int, Tag0:2/?int>> | Encoded]. 203 + [<<MT:1/?int, Tag0:2/?int>> | Encoded]. 209 204 210 205 do_encode(tversion, #{max_packet_size := MSize, version := Version}) -> 211 206 {?Tversion, [<<MSize:4/?int>> | encode_str(Version)]}; ··· 252 247 253 248 do_encode(tstat, #{fid := FID}) -> 254 249 {?Tstat, <<FID:4/?int>>}; 255 - do_encode(rstat, _Data) -> 256 - error(unimplemented); 250 + do_encode(rstat, #{stat := Stat}) -> 251 + {?Rstat, encode_stat(Stat)}; 257 252 258 - do_encode(twstat, #{fid := _FID, stat := _Stat}) -> 259 - error(unimplemented); 253 + do_encode(twstat, #{fid := FID, stat := Stat}) -> 254 + {?Twstat, [<<FID:4/?int>>, encode_stat(Stat)]}; 255 + do_encode(rwstat, _) -> 256 + {?Rwstat, []}; 260 257 261 258 do_encode(twalk, #{fid := FID, new_fid := NewFID, names := Names}) -> 262 259 ENames = [encode_str(Name) || Name <- Names], ··· 265 262 do_encode(rwalk, #{qids := QIDs}) -> 266 263 EQIDs = [qid_to_binary(QID) || QID <- QIDs], 267 264 Len = length(EQIDs), 268 - {?Rwalk, [<<Len:?len>> | EQIDs]}. 265 + {?Rwalk, [<<Len:?len>> | EQIDs]}; 266 + 267 + do_encode(tread, #{fid := FID, offset := Offset, count := Count}) -> 268 + {?Tread, <<FID:4/?int, Offset:8/?int, Count:4/?int>>}; 269 + do_encode(rread, #{data := Data}) -> 270 + {?Rread, encode_str(Data)}. 271 + 272 + encode_stat(#{ 273 + type := Type, 274 + dev := Dev, 275 + qid := QID, 276 + mode := Mode, 277 + atime := Atime, 278 + mtime := Mtime, 279 + length := Len, 280 + name := Name, 281 + uid := Uid, 282 + gid := Gid, 283 + muid := MUid 284 + }) -> 285 + Encoded = [<< 286 + Type:2/?int, 287 + Dev:2/?int 288 + >>, 289 + qid_to_binary(QID), 290 + <<Mode:4/?int>>, 291 + time_to_encoded_sec(Atime), 292 + time_to_encoded_sec(Mtime), 293 + <<Len:8/?int>>, 294 + encode_str(Name), 295 + encode_str(Uid), 296 + encode_str(Gid), 297 + encode_str(MUid) 298 + ], 299 + encode_str(Encoded). 300 + 301 + 302 + %% ========== Utilities ========== 269 303 270 304 encode_str(Data) -> 271 305 Len = iolist_size(Data), ··· 276 310 277 311 qid_to_binary(#{type := Type, version := Version, path := Path}) -> 278 312 <<Type:1/?int, Version:4/?int, Path:8/?int>>. 313 + 314 + time_to_encoded_sec(Time) -> 315 + Sec = calendar:universal_time_to_system_time(Time, [{unit, second}]), 316 + <<Sec:4/?int>>.
-47
src/e9p_proto.erl
··· 1 - % SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2 - % 3 - % SPDX-License-Identifier: Apache-2.0 4 - 5 - -module(e9p_proto). 6 - 7 - -behaviour(ranch_protocol). 8 - 9 - -export([start_link/3]). 10 - 11 - -export([init/1, loop/3]). 12 - 13 - -include("e9p_internal.hrl"). 14 - -include_lib("kernel/include/logger.hrl"). 15 - 16 - start_link(Ref, Transport, Opts) -> 17 - Pid = proc_lib:spawn_link(?MODULE, init, [{Ref, Transport, Opts}]), 18 - {ok, Pid}. 19 - 20 - init({Ref, Transport, Opts}) -> 21 - {ok, Socket} = ranch:handshake(Ref), 22 - ?MODULE:loop(Socket, Transport, Opts). 23 - 24 - loop(Socket, Transport, Opts) -> 25 - Timeout = 300000, 26 - case Transport:recv(Socket, 7, Timeout) of 27 - {ok, <<Size:4/?int, Type:1/?int, Tag:2/?int>>} -> 28 - {ok, Data} = Transport:recv(Socket, Size - 7, 0), 29 - {ok, #{type := TType, data := TMsg}} = e9p_msg:parse(Type, Data), 30 - ?LOG_DEBUG("-> ~4.16.0B: ~s ~p~n", [Tag, TType, TMsg]), 31 - {RType, RMsg} = handle_msg(TType, TMsg), 32 - ?LOG_DEBUG("<- ~4.16.0B: ~s ~p~n", [Tag, RType, RMsg]), 33 - Resp = e9p_msg:encode(Tag, RType, RMsg), 34 - Transport:send(Socket, Resp), 35 - ?MODULE:loop(Socket, Transport, Opts); 36 - _Other -> 37 - ok = Transport:close(Socket) 38 - end. 39 - 40 - handle_msg(tversion, #{version := <<"9P2000">>, max_packet_size := MP}) -> 41 - {rversion, #{version => <<"9P2000">>, max_packet_size => MP}}; 42 - handle_msg(tversion, #{version := Version}) -> 43 - {rerror, #{error => ["Unsupported version: ", Version]}}; 44 - handle_msg(tattach, _) -> 45 - {rattach, #{qid => #{type => 0, version => 0, path => 0}}}; 46 - handle_msg(Msg, _) -> 47 - {rerror, #{error => io_lib:format("Unsupported message ~s", [Msg])}}.
+87
src/e9p_server.erl
··· 1 + % SPDX-FileCopyrightText: 2026 Łukasz Niemier <~@hauleth.dev> 2 + % 3 + % SPDX-License-Identifier: Apache-2.0 4 + 5 + -module(e9p_server). 6 + 7 + -include_lib("kernel/include/logger.hrl"). 8 + 9 + -export([start_link/2, 10 + setup_acceptor/3, 11 + accept_loop/2, 12 + loop/3 13 + ]). 14 + 15 + start_link(Port, Handler) -> 16 + proc_lib:start_link(?MODULE, setup_acceptor, [self(), Port, Handler]). 17 + 18 + setup_acceptor(Parent, Port, Handler0) -> 19 + {ok, LSock} = gen_tcp:listen(Port, [binary, {active, false}]), 20 + {ok, Handler} = e9p_fs:init(Handler0), 21 + 22 + proc_lib:init_ack(Parent, {ok, self()}), 23 + 24 + ?MODULE:accept_loop(LSock, Handler). 25 + 26 + accept_loop(LSock, Handler) -> 27 + case gen_tcp:accept(LSock, 5000) of 28 + {ok, Sock} -> 29 + ok = ?MODULE:loop(Sock, #{}, Handler), 30 + ?MODULE:accept_loop(LSock, Handler); 31 + {error, timeout} -> 32 + ?MODULE:accept_loop(LSock, Handler); 33 + {error, closed} -> 34 + ok 35 + end. 36 + 37 + loop(Sock, FIDs, Handler) -> 38 + case e9p_transport:read(Sock) of 39 + {ok, Tag, Type, Data} -> 40 + case handle_message(Type, Data, FIDs, Handler) of 41 + {ok, {RType, RData}, RFIDs, RHandler} -> 42 + e9p_transport:send(Sock, Tag, RType, RData), 43 + ?MODULE:loop(Sock, RFIDs, RHandler) 44 + end; 45 + {error, closed} -> 46 + ?LOG_WARNING("Connection closed"), 47 + ok 48 + end. 49 + 50 + handle_message(tversion, #{version := ~"9P2000"} = Data, FIDs, Handler) -> 51 + {ok, {rversion, Data}, FIDs, Handler}; 52 + handle_message(tattach, Data, FIDs, Handler0) -> 53 + #{fid := FID, uname := _UName, aname := AName} = Data, 54 + {ok, QID, Handler} = e9p_fs:root(Handler0, AName), 55 + NFIDs = FIDs#{FID => QID}, 56 + {ok, {rattach, #{qid => QID}}, NFIDs, Handler}; 57 + handle_message(tclunk, #{fid := FID}, FIDs, Handler) -> 58 + NFIDs = maps:remove(FID, FIDs), 59 + {ok, {rflush, #{}}, NFIDs, Handler}; 60 + handle_message(twalk, Data, FIDs, Handler0) -> 61 + #{ 62 + fid := FID, 63 + new_fid := NewFID, 64 + names := Paths 65 + } = Data, 66 + #{FID := QID} = FIDs, 67 + {ok, NewQID, QIDs, Handler} = e9p_fs:walk(Handler0, QID, Paths), 68 + {ok, {rwalk, #{qids => QIDs}}, FIDs#{NewFID => NewQID}, Handler}; 69 + handle_message(topen, #{fid := FID}, FIDs, Handler0) -> 70 + #{FID := QID} = FIDs, 71 + {ok, IOUnit, Handler} = e9p_fs:open(Handler0, QID), 72 + {ok, {ropen, #{qid => QID, iounit => IOUnit}}, FIDs, Handler}; 73 + handle_message(tcreate, #{fid := FID}, FIDs, Handler0) -> 74 + #{FID := QID, name := Name, perm := Perm, mode := Mode} = FIDs, 75 + {ok, {NewQID, IOUnit}, Handler} = e9p_fs:create(Handler0, QID, Name, Perm, Mode), 76 + {ok, {rcreate, #{qid => NewQID, iounit => IOUnit}}, FIDs, Handler}; 77 + handle_message(tread, Data, FIDs, Handler0) -> 78 + #{ 79 + fid := FID, 80 + offset := Offset, 81 + count := Count 82 + } = Data, 83 + #{FID := QID} = FIDs, 84 + {ok, Data, Handler} = e9p_fs:read(Handler0, QID, Offset, Count), 85 + {ok, {rread, #{data => Data}}, FIDs, Handler}; 86 + handle_message(_Type, _Data, FIDs, Handler) -> 87 + {ok, {rerror, #{error => ~"Unknown request type"}}, FIDs, Handler}.
+16 -15
src/e9p_transport.erl
··· 6 6 7 7 -include("e9p_internal.hrl"). 8 8 9 - -export([send/3, read/1, read_stream/1]). 9 + -export([send/4, read/1, read_stream/1]). 10 10 11 - send(Socket, Tag, Message) -> 12 - Encoded = e9p_msg:encode(Tag, Message), 13 - gen_tcp:send(Socket, Encoded). 11 + send(Socket, Tag, Type, Message) -> 12 + Encoded = e9p_msg:encode(Tag, Type, Message), 13 + Size = iolist_size(Encoded) + 4, 14 + gen_tcp:send(Socket, [<<Size:4/?int>>, Encoded]). 14 15 15 16 read(Socket) -> 16 - case gen_tcp:recv(Socket, 7) of 17 - {ok, <<Size:4/?int, Type, Tag:2/?int>>} -> 18 - case gen_tcp:recv(Socket, Size - 7) of 19 - {ok, Data} -> 20 - case e9p_msg:parse(Type, Data) of 21 - {ok, Msg} -> 22 - {ok, Tag, Msg}; 17 + case gen_tcp:recv(Socket, 4) of 18 + {ok, <<Size:4/?int>>} -> 19 + case gen_tcp:recv(Socket, Size - 4) of 20 + {ok, Data} when is_binary(Data) -> 21 + case e9p_msg:parse(Data) of 22 + {ok, Tag, Type, Msg} -> 23 + {ok, Tag, Type, Msg}; 23 24 {error, _} = Error -> 24 25 Error 25 26 end; ··· 30 31 Error 31 32 end. 32 33 33 - read_stream(<<Size:4/?int, Type, Tag:2/?int, Data:(Size - 7)/binary, Rest/binary>> = Input) -> 34 - case e9p_msg:parse(Type, Data) of 35 - {ok, Msg} -> 36 - {ok, Tag, Msg, Rest}; 34 + read_stream(<<Size:4/?int, Data:(Size - 4)/binary, Rest/binary>> = Input) -> 35 + case e9p_msg:parse(Data) of 36 + {ok, Tag, Type, Msg} -> 37 + {ok, Tag, Type, Msg, Rest}; 37 38 {error, Error} -> 38 39 {error, Error, Input} 39 40 end;
+38
src/e9p_unfs.erl
··· 1 + % SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2 + % 3 + % SPDX-License-Identifier: Apache-2.0 4 + 5 + -module(e9p_unfs). 6 + 7 + -behaviour(e9p_fs). 8 + 9 + -include_lib("kernel/include/file.hrl"). 10 + 11 + -export([init/1, root/2, walk/3, stat/2, read/4, write/4]). 12 + 13 + init(#{path := Path}) -> 14 + {ok, #{root => Path}}. 15 + 16 + root(_AName, #{root := Root} = State) -> 17 + Qid = e9p:make_qid(dir, 0, 0, Root), 18 + {ok, Qid, State}. 19 + 20 + walk(#{state := Path}, File, State) -> 21 + Next = filename:join(Path, File), 22 + case file:read_file_info(Next, [{time, posix}]) of 23 + {ok, #file_info{type = Type, inode = Inode}} -> 24 + NQid = e9p:make_qid(Type, 0, Inode, Next), 25 + {NQid, State}; 26 + 27 + {error, _} -> 28 + {false, State} 29 + end. 30 + 31 + stat(_Qid, State) -> 32 + {error, unimplemented, State}. 33 + 34 + read(_Qid, _Offset, _Len, State) -> 35 + {error, unimplemented, State}. 36 + 37 + write(_Qid, _Offset, _Data, State) -> 38 + {error, unimplemented, State}.
+11 -2
src/e9p_utils.erl
··· 4 4 5 5 -module(e9p_utils). 6 6 7 - -export([normalize_path/1]). 7 + -export([normalize_path/1, qtype_from_atom/1]). 8 8 9 9 normalize_path(List) -> normalize_path(List, []). 10 10 11 11 normalize_path([], Acc) -> lists:reverse(Acc); 12 12 normalize_path([Dot | Rest], Acc) 13 - when Dot =:= "." orelse Dot =:= <<".">> 13 + when Dot =:= "." orelse Dot =:= ~"." 14 14 -> 15 15 normalize_path(Rest, Acc); 16 16 normalize_path([P | Rest], Acc) -> 17 17 normalize_path(Rest, [P | Acc]). 18 + 19 + qtype_from_atom(dir) -> 16#80; 20 + qtype_from_atom(append) -> 16#40; 21 + qtype_from_atom(excl) -> 16#20; 22 + qtype_from_atom(device) -> 16#10; 23 + qtype_from_atom(auth) -> 16#08; 24 + qtype_from_atom(tmp) -> 16#04; 25 + qtype_from_atom(symlink) -> 16#02; 26 + qtype_from_atom(regular) -> 16#00.
+20
test/prop_e9p_msg.erl
··· 1 + % SPDX-FileCopyrightText: 2026 Łukasz Niemier <~@hauleth.dev> 2 + % 3 + % SPDX-License-Identifier: Apache-2.0 4 + 5 + -module(prop_e9p_msg). 6 + 7 + -include_lib("proper/include/proper.hrl"). 8 + % -include_lib("stdlib/include/assert.hrl"). 9 + 10 + prop_can_decode_encoded_tauth() -> 11 + ?FORALL({Uname, Aname}, {binary(), binary()}, 12 + begin 13 + enc_dec(tauth, #{afid => 1, uname => Uname, aname => Aname}) 14 + end). 15 + 16 + enc_dec(Kind, Data) -> 17 + Tag = 1, 18 + Out = e9p_msg:encode(Tag, Kind, Data), 19 + Encoded = iolist_to_binary(Out), 20 + {ok, Tag, Kind, Data} =:= e9p_msg:parse(Encoded).