diff --git a/rebar.config b/rebar.config index 0721539..af84908 100644 --- a/rebar.config +++ b/rebar.config @@ -3,6 +3,14 @@ % SPDX-License-Identifier: Apache-2.0 {erl_opts, [debug_info]}. -{deps, [ranch]}. +{deps, [telemetry]}. -{project_plugins, [rebar3_ex_doc]}. +{project_plugins, [ + rebar3_ex_doc, + rebar3_proper + ]}. + +{profiles, [{test, [ + {deps, [proper]}, + {erlc_flags, [nowarn_export_all]} + ]}]}. diff --git a/rebar.lock b/rebar.lock index 66b1272..fd6ebf1 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,8 +1,8 @@ {"1.2.0", -[{<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}]}. +[{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.3.0">>},0}]}. [ {pkg_hash,[ - {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}]}, + {<<"telemetry">>, <<"FEDEBBAE410D715CF8E7062C96A1EF32EC22E764197F70CDA73D82778D61E7A2">>}]}, {pkg_hash_ext,[ - {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}]} + {<<"telemetry">>, <<"7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6">>}]} ]. diff --git a/src/e9p.erl b/src/e9p.erl index 939371d..e16421e 100644 --- a/src/e9p.erl +++ b/src/e9p.erl @@ -4,14 +4,31 @@ -module(e9p). --export([]). +-export([make_qid/4]). -export_type([qid/0, fid/0]). +-export_type([u8/0, u16/0, u32/0, u64/0]). + +-type u8() :: 16#00..16#FF. +-type u16() :: 16#0000..16#FFFF. +-type u32() :: 16#00000000..16#FFFFFFFF. +-type u64() :: 16#0000000000000000..16#FFFFFFFFFFFFFFFF. + -type qid() :: #{ - type => integer(), - version => integer(), - path => integer() + type => u8(), + version => u16(), + path => u64(), + state => term() }. -type fid() :: 16#00000000..16#FFFFFFFF. + +%-spec make_qid() +make_qid(Type, Version, Path, State) -> + #{ + type => e9p_utils:qtype_from_atom(Type), + version => Version, + path => Path, + state => State + }. diff --git a/src/e9p_client.erl b/src/e9p_client.erl index 0000dc2..2157aaf 100644 --- a/src/e9p_client.erl +++ b/src/e9p_client.erl @@ -51,13 +51,11 @@ handle_call({attach, Auth, Uname, Aname}, From, State) -> Id -> Id end, - Msg = #{type => tattach, - data => - #{fid => Fid, + Msg = #{fid => Fid, afid => Afid, uname => Uname, - aname => Aname}}, - e9p_transport:send(Socket, Tag, Msg), + aname => Aname}, + e9p_transport:send(Socket, Tag, tattach, Msg), {noreply, State#{tag := Tag + 1, fid := Fid + 1, @@ -71,7 +69,7 @@ handle_cast(_Msg, State) -> handle_info({tcp, Socket, Data}, #{socket := Socket} = State) -> #{buffer := Buffer, msgs := Msgs0} = State, case e9p_transport:read_stream(<>) of - {ok, Tag, Msg, Rest} -> + {ok, Tag, _Type, Msg, Rest} -> Msgs = case maps:take(Tag, Msgs0) of {{From, _}, M} -> @@ -87,11 +85,10 @@ handle_info({tcp, Socket, Data}, #{socket := Socket} = State) -> end. version_negotiation(Socket) -> - Msg = #{type => tversion, - data => #{max_packet_size => ?max_packet_size, version => ?version}}, - e9p_transport:send(Socket, notag, Msg), + Msg = #{max_packet_size => ?max_packet_size, version => ?version}, + e9p_transport:send(Socket, notag, tversion, Msg), case e9p_transport:read(Socket) of - {ok, _, #{type := rversion, data := Resp}} -> + {ok, _, rversion, Resp} -> {ok, Resp}; {error, _} = Error -> Error diff --git a/src/e9p_fs.erl b/src/e9p_fs.erl index 88a4a77..810b42b 100644 --- a/src/e9p_fs.erl +++ b/src/e9p_fs.erl @@ -6,88 +6,152 @@ %% @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(). +-export([ + init/1, + root/2, + walk/3, + open/2, + create/5, + read/4, + write/4, + clunk/2, + remove/2, + stat/2, + wstat/3 + ]). + +-export_type([state/0]). -type state() :: term(). +-type result() :: {ok, state()} | {error, term(), state()}. +-type result(T) :: {ok, T, state()} | {error, term(), state()}. + +-define(if_supported(Code), + case erlang:function_exported(Mod, ?FUNCTION_NAME, ?FUNCTION_ARITY) of + true -> + case (fun() -> Code end)() of + {ok, Ret, {Mod, NewState}} -> + {ok, Ret, {Mod, NewState}}; + {error, Error, NewState} -> + {error, Error, {Mod, NewState}} + end; + false -> {error, nosupport, {Mod, State}} + end). %% Setup state for given filesystem. --callback init(term()) -> state(). +-callback init(term()) -> + {ok, state()} | + {error, Reason :: term()}. %% 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()}. +-callback root(AName :: unicode:chardata(), state()) -> {ok, e9p:qid(), state()}. + +-callback flush(state()) -> {ok, state()} | {error, term(), 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()}. +-callback open(QID :: e9p:qid(), state()) -> result(e9p:u32()). + +-callback create(QID :: e9p:qid(), + Name :: unicode:chardata(), + Perm :: e9p:u32(), + Mode :: e9p:u8(), + state()) -> result({e9p:qid(), e9p:u32()}). %% 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()}. + state()) -> result(iodata()). %% 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()}. + state()) -> result(non_neg_integer()). -%% @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. +-callback clunk(QID :: e9p:qid(), state()) -> result(). -%% @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 +-callback remove(QID :: e9p:qid(), state()) -> result(). + +%% Return stat data for file indicated by `QID' +-callback stat(QID :: e9p:qid(), state()) -> result(map()). + +%% Write stat data for file indicated by `QID' +-callback wstat(QID :: e9p:qid(), map(), state()) -> result(). + +-optional_callbacks([ + flush/1, + walk/3, + open/2, + create/5, + read/4, + write/4, + clunk/2, + remove/2, + stat/2, + wstat/3 + ]). + +init({Mod, State}) -> + case Mod:init(State) of + {ok, NewState} -> {ok, {Mod, NewState}}; + 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}. +root({Mod, State}, AName) -> + case Mod:root(AName, State) of + {ok, QID, NewState} -> + {ok, QID, {Mod, NewState}} + end. -%% Walk through the FS tree. -do_walk(Mod, QID, List, State) -> - do_walk(Mod, QID, List, State, []). +-doc """ +Walk through paths starting at QID. +""". +walk({Mod, State}, QID, Paths) when is_atom(Mod) -> + ?if_supported(do_walk(Mod, QID, Paths, State, [])). -do_walk(_Mod, _QID, [], State, Acc) -> - {lists:reverse(Acc), State}; +do_walk(Mod, QID, [], State, Acc) -> + {ok, QID, lists:reverse(Acc), {Mod, 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]) + {ok, QID0, lists:reverse(Acc), {Mod, State}}; + {QID, State} -> + do_walk(Mod, QID, Rest, State, [QID | Acc]) + end. + +open({Mod, State}, QID) -> + ?if_supported(Mod:open(QID, State)). + +create({Mod, State}, QID, Name, Perm, Mode) -> + ?if_supported(Mod:create(QID, Name, Perm, Mode, State)). + +read({Mod, State}, QID, Offset, Length) -> + ?if_supported(Mod:read(QID, Offset, Length, State)). + +write({Mod, State}, QID, Offset, Data) -> + ?if_supported(Mod:write(QID, Offset, Data, State)). + +clunk({Mod, State}, QID) -> + case erlang:function_exported(Mod, clunk, 3) of + true -> + {Resp, State} = Mod:clunk(QID, State), + {Resp, {Mod, State}}; + false -> {ok, {Mod, State}} end. + +remove({Mod, State}, QID) -> + ?if_supported(Mod:remove(QID, State)). + +stat({Mod, State}, QID) -> + ?if_supported(Mod:stat(QID, State)). + +wstat({Mod, State}, QID, Stat) -> + ?if_supported(Mod:wstat(QID, Stat, State)). diff --git a/src/e9p_internal.hrl b/src/e9p_internal.hrl index 78c280a..4ff71e3 100644 --- a/src/e9p_internal.hrl +++ b/src/e9p_internal.hrl @@ -2,7 +2,7 @@ % % SPDX-License-Identifier: Apache-2.0 --define(version, <<"9P2000">>). +-define(version, ~"9P2000"). -define(notag, 16#FFFF). -define(nofid, 16#FFFFFFFF). @@ -12,33 +12,43 @@ -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(Tauth, 102). -define(Rauth, 103). + +-define(Tattach, 104). -define(Rattach, 105). + -define(Rerror, 107). + +-define(Tflush, 108). -define(Rflush, 109). + +-define(Twalk, 110). -define(Rwalk, 111). + +-define(Topen, 112). -define(Ropen, 113). + +-define(Tcreate, 114). -define(Rcreate, 115). + +-define(Tread, 116). -define(Rread, 117). + +-define(Twrite, 118). -define(Rwrite, 119). + +-define(Tclunk, 120). -define(Rclunk, 121). + +-define(Tremove, 122). -define(Rremove, 123). + +-define(Tstat, 124). -define(Rstat, 125). + +-define(Twstat, 126). -define(Rwstat, 127). diff --git a/src/e9p_io_server.erl b/src/e9p_io_server.erl deleted file mode 100644 index 22ed5fe..0000000 --- a/src/e9p_io_server.erl +++ /dev/null @@ -1,32 +0,0 @@ -% SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> -% -% SPDX-License-Identifier: Apache-2.0 - -%% @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 index 388d2ae..cf9dd6a 100644 --- a/src/e9p_msg.erl +++ b/src/e9p_msg.erl @@ -6,13 +6,13 @@ %% @end -module(e9p_msg). --export([parse/2, encode/2, encode/3]). +-export([parse/1, encode/3]). -export_type([tag/0, message_type/0, request_message_type/0, - response_message_type/0, - message/0]). + response_message_type/0 + ]). -include("e9p_internal.hrl"). @@ -50,15 +50,10 @@ -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) -> +parse(<>) -> case do_parse(Type, Data) of {ok, T, Parsed} -> - {ok, #{type => T, data => Parsed}}; - + {ok, Tag, T, Parsed}; {error, Reason} -> {error, Reason} end. @@ -162,6 +157,11 @@ do_parse(?Twalk, <>) -> do_parse(?Rwalk, <>) -> {ok, rwalk, #{qids => [binary_to_qid(QID) || <> <= QIDs]}}; +do_parse(?Tread, <>) -> + {ok, tread, #{fid => FID, offset => Offset, count => Count}}; +do_parse(?Rread, <>) -> + {ok, rread, #{data => Data}}; + do_parse(Type, Data) -> {error, {invalid_message, Type, Data}}. @@ -193,10 +193,6 @@ parse_stat(<<_Size:2/?int, }}; 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), @@ -204,8 +200,7 @@ encode(Tag, Type, Data) -> notag -> ?notag; V -> V end, - Size = iolist_size(Encoded) + 7, - [<> | Encoded]. + [<> | Encoded]. do_encode(tversion, #{max_packet_size := MSize, version := Version}) -> {?Tversion, [<> | encode_str(Version)]}; @@ -252,11 +247,13 @@ do_encode(rremove, _) -> do_encode(tstat, #{fid := FID}) -> {?Tstat, <>}; -do_encode(rstat, _Data) -> - error(unimplemented); +do_encode(rstat, #{stat := Stat}) -> + {?Rstat, encode_stat(Stat)}; -do_encode(twstat, #{fid := _FID, stat := _Stat}) -> - error(unimplemented); +do_encode(twstat, #{fid := FID, stat := Stat}) -> + {?Twstat, [<>, encode_stat(Stat)]}; +do_encode(rwstat, _) -> + {?Rwstat, []}; do_encode(twalk, #{fid := FID, new_fid := NewFID, names := Names}) -> ENames = [encode_str(Name) || Name <- Names], @@ -265,7 +262,44 @@ do_encode(twalk, #{fid := FID, new_fid := NewFID, names := Names}) -> do_encode(rwalk, #{qids := QIDs}) -> EQIDs = [qid_to_binary(QID) || QID <- QIDs], Len = length(EQIDs), - {?Rwalk, [<> | EQIDs]}. + {?Rwalk, [<> | EQIDs]}; + +do_encode(tread, #{fid := FID, offset := Offset, count := Count}) -> + {?Tread, <>}; +do_encode(rread, #{data := Data}) -> + {?Rread, encode_str(Data)}. + +encode_stat(#{ + type := Type, + dev := Dev, + qid := QID, + mode := Mode, + atime := Atime, + mtime := Mtime, + length := Len, + name := Name, + uid := Uid, + gid := Gid, + muid := MUid + }) -> + Encoded = [<< + Type:2/?int, + Dev:2/?int + >>, + qid_to_binary(QID), + <>, + time_to_encoded_sec(Atime), + time_to_encoded_sec(Mtime), + <>, + encode_str(Name), + encode_str(Uid), + encode_str(Gid), + encode_str(MUid) + ], + encode_str(Encoded). + + +%% ========== Utilities ========== encode_str(Data) -> Len = iolist_size(Data), @@ -276,3 +310,7 @@ binary_to_qid(<>) -> qid_to_binary(#{type := Type, version := Version, path := Path}) -> <>. + +time_to_encoded_sec(Time) -> + Sec = calendar:universal_time_to_system_time(Time, [{unit, second}]), + <>. diff --git a/src/e9p_proto.erl b/src/e9p_proto.erl deleted file mode 100644 index 3545b16..0000000 --- a/src/e9p_proto.erl +++ /dev/null @@ -1,47 +0,0 @@ -% SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> -% -% SPDX-License-Identifier: Apache-2.0 - --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..997c0a5 --- /dev/null +++ b/src/e9p_server.erl @@ -0,0 +1,87 @@ +% SPDX-FileCopyrightText: 2026 Łukasz Niemier <~@hauleth.dev> +% +% SPDX-License-Identifier: Apache-2.0 + +-module(e9p_server). + +-include_lib("kernel/include/logger.hrl"). + +-export([start_link/2, + setup_acceptor/3, + accept_loop/2, + loop/3 + ]). + +start_link(Port, Handler) -> + proc_lib:start_link(?MODULE, setup_acceptor, [self(), Port, Handler]). + +setup_acceptor(Parent, Port, Handler0) -> + {ok, LSock} = gen_tcp:listen(Port, [binary, {active, false}]), + {ok, Handler} = e9p_fs:init(Handler0), + + proc_lib:init_ack(Parent, {ok, self()}), + + ?MODULE:accept_loop(LSock, Handler). + +accept_loop(LSock, Handler) -> + case gen_tcp:accept(LSock, 5000) of + {ok, Sock} -> + ok = ?MODULE:loop(Sock, #{}, Handler), + ?MODULE:accept_loop(LSock, Handler); + {error, timeout} -> + ?MODULE:accept_loop(LSock, Handler); + {error, closed} -> + ok + end. + +loop(Sock, FIDs, Handler) -> + case e9p_transport:read(Sock) of + {ok, Tag, Type, Data} -> + case handle_message(Type, Data, FIDs, Handler) of + {ok, {RType, RData}, RFIDs, RHandler} -> + e9p_transport:send(Sock, Tag, RType, RData), + ?MODULE:loop(Sock, RFIDs, RHandler) + end; + {error, closed} -> + ?LOG_WARNING("Connection closed"), + ok + end. + +handle_message(tversion, #{version := ~"9P2000"} = Data, FIDs, Handler) -> + {ok, {rversion, Data}, FIDs, Handler}; +handle_message(tattach, Data, FIDs, Handler0) -> + #{fid := FID, uname := _UName, aname := AName} = Data, + {ok, QID, Handler} = e9p_fs:root(Handler0, AName), + NFIDs = FIDs#{FID => QID}, + {ok, {rattach, #{qid => QID}}, NFIDs, Handler}; +handle_message(tclunk, #{fid := FID}, FIDs, Handler) -> + NFIDs = maps:remove(FID, FIDs), + {ok, {rflush, #{}}, NFIDs, Handler}; +handle_message(twalk, Data, FIDs, Handler0) -> + #{ + fid := FID, + new_fid := NewFID, + names := Paths + } = Data, + #{FID := QID} = FIDs, + {ok, NewQID, QIDs, Handler} = e9p_fs:walk(Handler0, QID, Paths), + {ok, {rwalk, #{qids => QIDs}}, FIDs#{NewFID => NewQID}, Handler}; +handle_message(topen, #{fid := FID}, FIDs, Handler0) -> + #{FID := QID} = FIDs, + {ok, IOUnit, Handler} = e9p_fs:open(Handler0, QID), + {ok, {ropen, #{qid => QID, iounit => IOUnit}}, FIDs, Handler}; +handle_message(tcreate, #{fid := FID}, FIDs, Handler0) -> + #{FID := QID, name := Name, perm := Perm, mode := Mode} = FIDs, + {ok, {NewQID, IOUnit}, Handler} = e9p_fs:create(Handler0, QID, Name, Perm, Mode), + {ok, {rcreate, #{qid => NewQID, iounit => IOUnit}}, FIDs, Handler}; +handle_message(tread, Data, FIDs, Handler0) -> + #{ + fid := FID, + offset := Offset, + count := Count + } = Data, + #{FID := QID} = FIDs, + {ok, Data, Handler} = e9p_fs:read(Handler0, QID, Offset, Count), + {ok, {rread, #{data => Data}}, FIDs, Handler}; +handle_message(_Type, _Data, FIDs, Handler) -> + {ok, {rerror, #{error => ~"Unknown request type"}}, FIDs, Handler}. diff --git a/src/e9p_transport.erl b/src/e9p_transport.erl index 110bcb2..b4056b8 100644 --- a/src/e9p_transport.erl +++ b/src/e9p_transport.erl @@ -6,20 +6,21 @@ -include("e9p_internal.hrl"). --export([send/3, read/1, read_stream/1]). +-export([send/4, read/1, read_stream/1]). -send(Socket, Tag, Message) -> - Encoded = e9p_msg:encode(Tag, Message), - gen_tcp:send(Socket, Encoded). +send(Socket, Tag, Type, Message) -> + Encoded = e9p_msg:encode(Tag, Type, Message), + Size = iolist_size(Encoded) + 4, + 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}; + case gen_tcp:recv(Socket, 4) of + {ok, <>} -> + case gen_tcp:recv(Socket, Size - 4) of + {ok, Data} when is_binary(Data) -> + case e9p_msg:parse(Data) of + {ok, Tag, Type, Msg} -> + {ok, Tag, Type, Msg}; {error, _} = Error -> Error end; @@ -30,10 +31,10 @@ read(Socket) -> Error end. -read_stream(<> = Input) -> - case e9p_msg:parse(Type, Data) of - {ok, Msg} -> - {ok, Tag, Msg, Rest}; +read_stream(<> = Input) -> + case e9p_msg:parse(Data) of + {ok, Tag, Type, Msg} -> + {ok, Tag, Type, Msg, Rest}; {error, Error} -> {error, Error, Input} end; diff --git a/src/e9p_unfs.erl b/src/e9p_unfs.erl new file mode 100644 index 0000000..c2dc9e2 --- /dev/null +++ b/src/e9p_unfs.erl @@ -0,0 +1,38 @@ +% SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> +% +% SPDX-License-Identifier: Apache-2.0 + +-module(e9p_unfs). + +-behaviour(e9p_fs). + +-include_lib("kernel/include/file.hrl"). + +-export([init/1, root/2, walk/3, stat/2, read/4, write/4]). + +init(#{path := Path}) -> + {ok, #{root => Path}}. + +root(_AName, #{root := Root} = State) -> + Qid = e9p:make_qid(dir, 0, 0, Root), + {ok, Qid, State}. + +walk(#{state := Path}, File, State) -> + Next = filename:join(Path, File), + case file:read_file_info(Next, [{time, posix}]) of + {ok, #file_info{type = Type, inode = Inode}} -> + NQid = e9p:make_qid(Type, 0, Inode, Next), + {NQid, State}; + + {error, _} -> + {false, State} + end. + +stat(_Qid, State) -> + {error, unimplemented, State}. + +read(_Qid, _Offset, _Len, State) -> + {error, unimplemented, State}. + +write(_Qid, _Offset, _Data, State) -> + {error, unimplemented, State}. diff --git a/src/e9p_utils.erl b/src/e9p_utils.erl index 3900875..7c06017 100644 --- a/src/e9p_utils.erl +++ b/src/e9p_utils.erl @@ -4,14 +4,23 @@ -module(e9p_utils). --export([normalize_path/1]). +-export([normalize_path/1, qtype_from_atom/1]). normalize_path(List) -> normalize_path(List, []). normalize_path([], Acc) -> lists:reverse(Acc); normalize_path([Dot | Rest], Acc) - when Dot =:= "." orelse Dot =:= <<".">> + when Dot =:= "." orelse Dot =:= ~"." -> normalize_path(Rest, Acc); normalize_path([P | Rest], Acc) -> normalize_path(Rest, [P | Acc]). + +qtype_from_atom(dir) -> 16#80; +qtype_from_atom(append) -> 16#40; +qtype_from_atom(excl) -> 16#20; +qtype_from_atom(device) -> 16#10; +qtype_from_atom(auth) -> 16#08; +qtype_from_atom(tmp) -> 16#04; +qtype_from_atom(symlink) -> 16#02; +qtype_from_atom(regular) -> 16#00. diff --git a/test/prop_e9p_msg.erl b/test/prop_e9p_msg.erl new file mode 100644 index 0000000..97e1b0c --- /dev/null +++ b/test/prop_e9p_msg.erl @@ -0,0 +1,20 @@ +% SPDX-FileCopyrightText: 2026 Łukasz Niemier <~@hauleth.dev> +% +% SPDX-License-Identifier: Apache-2.0 + +-module(prop_e9p_msg). + +-include_lib("proper/include/proper.hrl"). +% -include_lib("stdlib/include/assert.hrl"). + +prop_can_decode_encoded_tauth() -> + ?FORALL({Uname, Aname}, {binary(), binary()}, + begin + enc_dec(tauth, #{afid => 1, uname => Uname, aname => Aname}) + end). + +enc_dec(Kind, Data) -> + Tag = 1, + Out = e9p_msg:encode(Tag, Kind, Data), + Encoded = iolist_to_binary(Out), + {ok, Tag, Kind, Data} =:= e9p_msg:parse(Encoded).