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

MVP implementation of `e9p_unfs`

`e9p_unfs` is simple implementation for providing access layer to Unix
Filesystem through 9p2000.

hauleth.dev eaff6b7b afcee832

verified
+143 -68
+5 -3
README.md
··· 20 20 + [ ] File stats 21 21 - [ ] Server implementation 22 22 + [x] Establishing connection 23 - + [ ] Tree walking 23 + + [x] Tree walking 24 24 + [ ] File/directory creation 25 25 + [ ] File/directory deletion 26 - + [ ] File stats 26 + + [x] File stats 27 27 + [ ] Customisable FS implementations 28 28 29 29 ### Example FS 30 30 31 - - [ ] "Passthrough" - which will simply allow accessing some "real" directory in 31 + - [-] UnixFs - which will simply allow accessing some "real" directory in 32 32 system FS 33 + 34 + **WIP**: Implemented directory reading and file reading. 33 35 - [ ] ErlProcFS - which will expose Erlang process tree and other internal data 34 36 via API similar to `procfs` from Linux 35 37
+21 -2
src/e9p.erl
··· 4 4 5 5 -module(e9p). 6 6 7 - -export([make_qid/4]). 7 + -export([make_qid/4, is_type/2]). 8 8 9 9 -export_type([qid/0, fid/0]). 10 10 ··· 27 27 %-spec make_qid() 28 28 make_qid(Type, Version, Path, State) -> 29 29 #{ 30 - type => e9p_utils:to_qtype(Type), 30 + type => to_qtype(Type), 31 31 version => Version, 32 32 path => Path, 33 33 state => State 34 34 }. 35 + 36 + is_type(#{type := QType}, Type) -> 37 + (to_qtype(Type) band QType) =/= 0. 38 + 39 + to_qtype(List) when is_list(List) -> 40 + lists:foldl( 41 + fun(El, Acc) when is_integer(Acc) -> to_qtype(El) bor Acc end, 42 + 0, 43 + List 44 + ); 45 + 46 + to_qtype(directory) -> 16#80; 47 + to_qtype(append) -> 16#40; 48 + to_qtype(excl) -> 16#20; 49 + to_qtype(device) -> 16#10; 50 + to_qtype(auth) -> 16#08; 51 + to_qtype(tmp) -> 16#04; 52 + to_qtype(symlink) -> 16#02; 53 + to_qtype(regular) -> 16#00.
+20 -15
src/e9p_fs.erl
··· 10 10 init/1, 11 11 root/2, 12 12 walk/3, 13 - open/2, 13 + open/3, 14 14 create/5, 15 15 read/4, 16 16 write/4, ··· 30 30 case erlang:function_exported(Mod, ?FUNCTION_NAME, ?FUNCTION_ARITY) of 31 31 true -> 32 32 case (fun() -> Code end)() of 33 - {ok, Ret, {Mod, NewState}} -> 33 + {ok, Ret, NewState} -> 34 34 {ok, Ret, {Mod, NewState}}; 35 35 {error, Error, NewState} -> 36 36 {error, Error, {Mod, NewState}} ··· 56 56 -callback walk(QID :: e9p:qid(), unicode:chardata(), state()) -> 57 57 {e9p:qid() | false, state()}. 58 58 59 - -callback open(QID :: e9p:qid(), state()) -> result({e9p:qid(), e9p:u32()}). 59 + -callback open(QID :: e9p:qid(), Mode :: integer(), state()) -> result({e9p:qid(), e9p:u32()}). 60 60 61 61 -callback create(QID :: e9p:qid(), 62 62 Name :: unicode:chardata(), ··· 68 68 -callback read(QID :: e9p:qid(), 69 69 Offset :: non_neg_integer(), 70 70 Length :: non_neg_integer(), 71 - state()) -> result(iodata()). 71 + state()) -> result({e9p:qid(), iodata()}). 72 72 73 73 %% Write data to file indicated by `QID' 74 74 -callback write(QID :: e9p:qid(), 75 75 Offset :: non_neg_integer(), 76 76 Data :: iodata(), 77 - state()) -> result(non_neg_integer()). 77 + state()) -> result({e9p:qid(), non_neg_integer()}). 78 78 79 79 -callback clunk(QID :: e9p:qid(), state()) -> result(). 80 80 ··· 89 89 -optional_callbacks([ 90 90 flush/1, 91 91 walk/3, 92 - open/2, 92 + open/3, 93 93 create/5, 94 94 read/4, 95 95 write/4, ··· 117 117 walk({Mod, State}, QID, Paths) when is_atom(Mod) -> 118 118 ?if_supported(do_walk(Mod, QID, Paths, State, [])). 119 119 120 - do_walk(Mod, QID, [], State, Acc) -> 121 - {ok, QID, lists:reverse(Acc), {Mod, State}}; 120 + do_walk(_Mod, QID, [], State, Acc) -> 121 + {ok, {QID, lists:reverse(Acc)}, State}; 122 122 do_walk(Mod, QID0, [P | Rest], State0, Acc) -> 123 123 case Mod:walk(QID0, P, State0) of 124 124 {false, State} -> 125 - {ok, QID0, lists:reverse(Acc), {Mod, State}}; 125 + {ok, {QID0, lists:reverse(Acc)}, State}; 126 126 {QID, State} -> 127 127 do_walk(Mod, QID, Rest, State, [QID | Acc]) 128 128 end. 129 129 130 - open({Mod, State}, QID) -> 131 - ?if_supported(Mod:open(QID, State)). 130 + open({Mod, State}, QID, Mode) -> 131 + ?if_supported(Mod:open(QID, Mode, State)). 132 132 133 133 create({Mod, State}, QID, Name, Perm, Mode) -> 134 134 ?if_supported(Mod:create(QID, Name, Perm, Mode, State)). ··· 139 139 write({Mod, State}, QID, Offset, Data) -> 140 140 ?if_supported(Mod:write(QID, Offset, Data, State)). 141 141 142 - clunk({Mod, State}, QID) -> 142 + clunk({Mod, State0}, QID) -> 143 143 case erlang:function_exported(Mod, clunk, 3) of 144 144 true -> 145 - {Resp, State} = Mod:clunk(QID, State), 146 - {Resp, {Mod, State}}; 147 - false -> {ok, {Mod, State}} 145 + maybe 146 + {ok, State} ?= Mod:clunk(QID, State0), 147 + {ok, {Mod, State}} 148 + else 149 + {error, Reason, StateE} -> 150 + {error, Reason, {Mod, StateE}} 151 + end; 152 + false -> {ok, {Mod, State0}} 148 153 end. 149 154 150 155 remove({Mod, State}, QID) ->
+12 -5
src/e9p_msg.erl
··· 248 248 do_encode(#tstat{fid = FID}) -> 249 249 {?Tstat, <<FID:4/?int>>}; 250 250 do_encode(#rstat{stat = Stat}) -> 251 - {?Rstat, encode_stat(Stat)}; 251 + Encoded = encode_stat(Stat), 252 + Len = iolist_size(Encoded), 253 + {?Rstat, [<<Len:?len>> | encode_stat(Stat)]}; 252 254 253 255 do_encode(#twstat{fid = FID, stat = Stat}) -> 254 - {?Twstat, [<<FID:4/?int>>, encode_stat(Stat)]}; 256 + Encoded = encode_stat(Stat), 257 + Len = iolist_size(Encoded), 258 + {?Twstat, [<<FID:4/?int, Len:?len>> | encode_stat(Stat)]}; 255 259 do_encode(#rwstat{}) -> 256 260 {?Rwstat, []}; 257 261 ··· 267 271 do_encode(#tread{fid = FID, offset = Offset, len = Len}) -> 268 272 {?Tread, <<FID:4/?int, Offset:8/?int, Len:4/?int>>}; 269 273 do_encode(#rread{data = Data}) -> 270 - {?Rread, encode_str(Data)}. 274 + Len = iolist_size(Data), 275 + {?Rread, [<<Len:4/?int>>, Data]}. 271 276 272 277 encode_stat(#{ 273 278 type := Type, ··· 296 301 encode_str(Gid), 297 302 encode_str(MUid) 298 303 ], 299 - encode_str(Encoded). 304 + ELen = iolist_size(Encoded), 305 + [<<ELen:?len>> | Encoded]. 300 306 301 307 302 308 %% ========== Utilities ========== 303 309 304 - encode_str(Data) -> 310 + encode_str(Data0) -> 311 + Data = unicode:characters_to_binary(Data0), 305 312 Len = iolist_size(Data), 306 313 [<<Len:?len>> | Data]. 307 314
+9 -7
src/e9p_server.erl
··· 40 40 loop(Sock, FIDs, Handler) -> 41 41 case e9p_transport:read(Sock) of 42 42 {ok, Tag, Data} -> 43 + ?LOG_WARNING(#{msg => Data}), 43 44 case handle_message(Data, FIDs, Handler) of 44 45 {ok, Reply, RFIDs, RHandler} -> 45 46 e9p_transport:send(Sock, Tag, Reply), ··· 73 74 handle_message(#twalk{fid = FID, new_fid = NewFID, names = Paths}, FIDs, Handler0) -> 74 75 maybe 75 76 {ok, QID} ?= get_qid(FIDs, FID), 76 - {ok, NewQID, QIDs, Handler} ?= e9p_fs:walk(Handler0, QID, Paths), 77 + {ok, {NewQID, QIDs}, Handler} ?= e9p_fs:walk(Handler0, QID, Paths), 77 78 {ok, #rwalk{qids = QIDs}, FIDs#{NewFID => NewQID}, Handler} 78 79 end; 79 80 80 - handle_message(#topen{fid = FID}, FIDs, Handler0) -> 81 + handle_message(#topen{fid = FID, mode = Mode}, FIDs, Handler0) -> 81 82 maybe 83 + ?LOG_WARNING(#{fids => FIDs}), 82 84 {ok, QID} ?= get_qid(FIDs, FID), 83 - {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:open(Handler0, QID), 85 + {ok, {NewQID, IOUnit}, Handler} ?= e9p_fs:open(Handler0, QID, Mode), 84 86 {ok, #ropen{qid = QID, io_unit = IOUnit}, FIDs#{FID => NewQID}, Handler} 85 87 end; 86 88 handle_message(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}, FIDs, Handler0) -> ··· 93 95 handle_message(#tread{fid = FID, offset = Offset, len = Len}, FIDs, Handler0) -> 94 96 maybe 95 97 {ok, QID} ?= get_qid(FIDs, FID), 96 - {ok, Data, Handler} ?= e9p_fs:read(Handler0, QID, Offset, Len), 97 - {ok, #rread{data = Data}, FIDs, Handler} 98 + {ok, {NQID, Data}, Handler} ?= e9p_fs:read(Handler0, QID, Offset, Len), 99 + {ok, #rread{data = Data}, FIDs#{FID => NQID}, Handler} 98 100 end; 99 101 handle_message(#twrite{fid = FID, offset = Offset, data = Data}, FIDs, Handler0) -> 100 102 maybe 101 103 {ok, QID} ?= get_qid(FIDs, FID), 102 - {ok, Data, Handler} ?= e9p_fs:write(Handler0, QID, Offset, Data), 103 - {ok, #rread{data = Data}, FIDs, Handler} 104 + {ok, {NQID, Data}, Handler} ?= e9p_fs:write(Handler0, QID, Offset, Data), 105 + {ok, #rread{data = Data}, FIDs#{FID => NQID}, Handler} 104 106 end; 105 107 106 108 handle_message(#tclunk{fid = FID}, FIDs, Handler0) ->
+2
src/e9p_transport.erl
··· 6 6 7 7 -include("e9p_internal.hrl"). 8 8 9 + % -include_lib("kernel/include/logger.hrl"). 10 + 9 11 -export([send/3, read/1, read_stream/1]). 10 12 11 13 send(Socket, Tag, Message) ->
+73 -19
src/e9p_unfs.erl
··· 6 6 7 7 -behaviour(e9p_fs). 8 8 9 + % -include_lib("kernel/include/logger.hrl"). 9 10 -include_lib("kernel/include/file.hrl"). 10 11 11 - -export([init/1, root/2, walk/3, stat/2, open/2, read/4]). 12 + -export([init/1, root/2, walk/3, stat/2, open/3, read/4, clunk/2]). 13 + 14 + qid(Path) -> 15 + case file:read_file_info(Path, [{time, posix}]) of 16 + {ok, #file_info{type = Type, inode = Inode}} -> 17 + NQid = e9p:make_qid(Type, 0, Inode, {Path, []}), 18 + {ok, NQid}; 19 + {error, _} = Error -> Error 20 + end. 21 + 22 + qid_stat(Root, Path) -> 23 + case file:read_file_info(Path, [{time, posix}]) of 24 + {ok, #file_info{type = Type, inode = Inode} = FI} -> 25 + NQid = e9p:make_qid(Type, 0, Inode, {Path, []}), 26 + Stat = file_info_to_stat(Root, NQid, FI), 27 + {ok, NQid, Stat}; 28 + {error, _} = Error -> Error 29 + end. 12 30 13 31 init(#{path := Path}) -> 14 32 {ok, #{root => Path}}. 15 33 16 34 root(_AName, #{root := Root} = State) -> 17 - Qid = e9p:make_qid(dir, 0, 0, Root), 18 - {ok, Qid, State}. 35 + maybe 36 + {ok, Qid} ?= qid(Root), 37 + {ok, Qid, State} 38 + end. 19 39 20 - walk(#{state := Path}, File, State) -> 40 + walk(#{state := {Path, _}}, File, State) -> 21 41 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} 42 + case qid(Next) of 43 + {ok, NQid} -> {NQid, State}; 44 + {error, _} -> {false, State} 29 45 end. 30 46 31 - stat(#{state := {Path, []}} = QID, State) -> 47 + stat(#{state := {Path, _}} = QID, #{root := Root} = State) -> 32 48 case file:read_file_info(Path, [{time, posix}]) of 33 49 {ok, FileInfo} -> 34 - Stat = file_info_to_stat(QID, FileInfo), 50 + Stat = file_info_to_stat(Root, QID, FileInfo), 35 51 {ok, Stat, State}; 36 52 {error, Error} -> 37 53 {error, Error, State} 38 54 end. 39 55 40 - open(_Qid, State) -> 41 - {error, unimplemented, State}. 56 + open(#{state := {Path, []}} = QID, _Mode, State) -> 57 + QS = case e9p:is_type(QID, directory) of 58 + true -> 59 + {ok, List} = file:list_dir(Path), 60 + {dir, List}; 61 + false -> 62 + {ok, FD} = file:open(Path, [raw, read, binary]), 63 + {regular, FD} 64 + end, 65 + NQID = QID#{state => {Path, QS}}, 66 + {ok, {NQID, 0}, State}. 42 67 43 - read(_Qid, _Offset, _Len, State) -> 44 - {error, unimplemented, State}. 68 + read(#{state := {_, {regular, FD}}} = QID, Offset, Len, State) -> 69 + case file:pread(FD, Offset, Len) of 70 + {ok, Data} -> {ok, {QID, Data}, State}; 71 + eof -> {ok, {QID, []}, State}; 72 + {error, Err} -> {error, Err, State} 73 + end; 74 + read(#{state := {Path, {dir, List}}} = QID, _Offset, Len, #{root := Root} = State) -> 75 + {Remaining, Data} = readdir(Root, Path, List, Len, []), 76 + {ok, {QID#{state => {Path, {dir, Remaining}}}, Data}, State}. 77 + 78 + readdir(_Root, _Path, List, 0, Acc) -> {List, Acc}; 79 + readdir(_Root, _Path, [], _Len, Acc) -> {[], Acc}; 80 + readdir(Root, Path, [Next | Rest], Len, Acc) -> 81 + {ok, _QID, Stat} = qid_stat(Root, filename:join(Path, Next)), 82 + Encoded = e9p_msg:encode_stat(Stat), 83 + Size = iolist_size(Encoded), 84 + if 85 + Size > Len -> []; 86 + true -> readdir(Root, Path, Rest, Len - Size, [Encoded | Acc]) 87 + end. 88 + 89 + clunk(#{state := {_Path, {refular, FD}}}, State) -> 90 + ok = file:close(FD), 91 + {ok, State}; 92 + clunk(_QID, State) -> 93 + {ok, State}. 45 94 46 95 file_info_to_stat( 47 - #{state := Path} = QID, 96 + Root, 97 + #{state := {Path, _}} = QID, 48 98 #file_info{ 49 99 size = Len, 50 100 atime = Atime, 51 101 mtime = Mtime, 52 102 mode = Mode 53 103 }) -> 104 + Name = if 105 + Root == Path -> ~"/"; 106 + true -> filename:basename(Path) 107 + end, 54 108 #{ 55 109 type => 0, 56 110 dev => 0, ··· 59 113 atime => Atime, 60 114 mtime => Mtime, 61 115 length => Len, 62 - name => filename:basename(Path), 116 + name => Name, 63 117 uid => ~"", 64 118 gid => ~"", 65 119 muid => ~""
+1 -17
src/e9p_utils.erl
··· 4 4 5 5 -module(e9p_utils). 6 6 7 - -export([normalize_path/1, to_qtype/1]). 7 + -export([normalize_path/1]). 8 8 9 9 normalize_path(List) -> normalize_path(List, []). 10 10 ··· 15 15 normalize_path(Rest, Acc); 16 16 normalize_path([P | Rest], Acc) -> 17 17 normalize_path(Rest, [P | Acc]). 18 - 19 - to_qtype(List) when is_list(List) -> 20 - lists:foldl( 21 - fun(El, Acc) when is_integer(Acc) -> to_qtype(El) bor Acc end, 22 - 0, 23 - List 24 - ); 25 - 26 - to_qtype(dir) -> 16#80; 27 - to_qtype(append) -> 16#40; 28 - to_qtype(excl) -> 16#20; 29 - to_qtype(device) -> 16#10; 30 - to_qtype(auth) -> 16#08; 31 - to_qtype(tmp) -> 16#04; 32 - to_qtype(symlink) -> 16#02; 33 - to_qtype(regular) -> 16#00.