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