Pure Erlang implementation of 9p2000 protocol
filesystem fs 9p2000 erlang 9p
at master 204 lines 7.5 kB view raw
1% SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2% 3% SPDX-License-Identifier: Apache-2.0 4 5%% @doc Definition of 9p filesystem 6%% @end 7-module(e9p_fs). 8 9-export([ 10 init/1, 11 root/3, 12 walk/3, 13 open/3, 14 create/5, 15 read/4, 16 write/4, 17 clunk/2, 18 remove/2, 19 stat/2, 20 wstat/3 21 ]). 22 23-include("e9p_internal.hrl"). 24-include_lib("kernel/include/logger.hrl"). 25 26-export_type([state/0, fid/0, path/0, result/0, result/1]). 27 28-type state() :: term(). 29-type fid() :: {QID :: e9p:qid(), State :: fid_state()}. 30-type path() :: [unicode:chardata()]. 31-type fid_state() :: term(). 32-type result() :: {ok, state()} | {error, term(), state()}. 33-type result(T) :: {ok, T, state()} | {error, term(), state()}. 34 35-define(if_supported(Code), 36 case erlang:function_exported(Mod, ?FUNCTION_NAME, ?FUNCTION_ARITY) of 37 true -> 38 case (fun() -> Code end)() of 39 {ok, Ret, NewState} -> 40 {ok, Ret, {Mod, NewState}}; 41 {error, Error, NewState} -> 42 {error, Error, {Mod, NewState}} 43 end; 44 false -> {error, nosupport, {Mod, State}} 45 end). 46 47%% Setup state for given filesystem. 48-callback init(term()) -> 49 {ok, state()} | 50 {error, Reason :: term()}. 51 52%% Returns `QID' for root node. 53%% 54%% If implementation provides multiple trees then the `AName' will be set to the 55%% tree defined by the client. It is left to the implementation to ensure the 56%% constraints of the file root (aka `walk(Root, "..", State0) =:= {Root, State1}'. 57-callback root(UName :: unicode:chardata(), AName :: unicode:chardata(), state()) -> 58 {ok, fid_state(), state()}. 59 60-callback flush(state()) -> result(). 61 62%% Walk through the given path starting at the `QID' 63-callback walk(fid(), File :: unicode:chardata(), unicode:chardata(), state()) -> 64 {fid() | false, state()}. 65 66-callback open(fid(), path(), Mode :: integer(), state()) -> result({fid_state(), e9p:u32()}). 67 68-callback create(fid(), 69 path(), 70 Name :: unicode:chardata(), 71 Perm :: e9p:u32(), 72 Mode :: e9p:u8(), 73 state()) -> result({fid(), IOUnit :: e9p:u32()}). 74 75%% Read data from file indicated by `QID' 76-callback read(fid(), 77 path(), 78 Offset :: non_neg_integer(), 79 Length :: non_neg_integer(), 80 state()) -> result({fid_state(), iodata()}). 81 82%% Write data to file indicated by `QID' 83-callback write(fid(), 84 path(), 85 Offset :: non_neg_integer(), 86 Data :: iodata(), 87 state()) -> result({fid_state(), non_neg_integer()}). 88 89-callback clunk(fid(), path(), state()) -> result(). 90 91-callback remove(fid(), path(), state()) -> result(). 92 93%% Return stat data for file indicated by `QID' 94-callback stat(fid(), path(), state()) -> result(map()). 95 96%% Write stat data for file indicated by `QID' 97-callback wstat(fid(), path(), map(), state()) -> result(). 98 99-optional_callbacks([ 100 flush/1, 101 clunk/3 102 ]). 103 104init({Mod, State}) -> 105 case Mod:init(State) of 106 {ok, NewState} -> {ok, {Mod, NewState}}; 107 Error -> Error 108 end. 109 110root({Mod, State}, UName, AName) -> 111 case Mod:root(UName, AName, State) of 112 {ok, {QID, FState}, NewState} -> 113 {ok, #fid{qid = QID, path = [], state = FState}, {Mod, NewState}} 114 end. 115 116-doc """ 117Walk through paths starting at QID. 118""". 119walk({Mod, State0}, FID0, Paths) when is_atom(Mod) -> 120 case do_walk(Mod, FID0, Paths, State0, []) of 121 {ok, {FID, QIDs}, State} -> {ok, {FID, QIDs}, {Mod, State}}; 122 {error, Reason, State} -> {error, Reason, {Mod, State}} 123 end. 124 125do_walk(_Mod, FID, [], State, Acc) -> 126 {ok, {FID, lists:reverse(Acc)}, State}; 127do_walk(_Mod, #fid{path = []}, [~".." | _Names], State, _Acc) -> 128 {error, "Cannot walt to root parent of root directory", State}; 129do_walk(Mod, #fid{qid = QID0, path = Path, state = FState0} = FID0, [P | Rest], State0, Acc) -> 130 case e9p:is_type(QID0, directory) of 131 true -> 132 case Mod:walk({QID0, FState0}, Path, P, State0) of 133 {false, State} when Acc =:= [] -> 134 % Per specification walk to first entry in name list must succeed 135 % (if any) otherwise return error. In subsequent steps we return 136 % successful list and last succeeded QID 137 {error, io_lib:format("Failed walk to ~p", [P]), State}; 138 {false, State} -> 139 {ok, {FID0, lists:reverse(Acc)}, State}; 140 {{QID, FState}, State} -> 141 FID = #fid{qid = QID, state = FState, path = Path ++ [P]}, 142 do_walk(Mod, FID, Rest, State, [QID | Acc]) 143 end; 144 false -> 145 {error, io_lib:format("Not directory ~p", [Path]), State0} 146 end. 147 148open({Mod, State0}, #fid{qid = QID, path = Path, state = FState0} = FID, Mode) -> 149 EMode = translate_mode(Mode), 150 case Mod:open({QID, FState0}, Path, EMode, State0) of 151 {ok, {FState, IOUnit}, State} -> 152 {ok, {FID#fid{state = FState}, IOUnit}, {Mod, State}}; 153 {error, Reason, StateE} -> {error, Reason, {Mod, StateE}} 154 end. 155 156translate_mode(Mode) when Mode >= 16#10 -> 157 [trunc | translate_mode(Mode band 16#EF)]; 158translate_mode(0) -> [read]; 159translate_mode(1) -> [write]; 160translate_mode(2) -> [append]; 161translate_mode(3) -> [exec]. 162 163create({Mod, State}, #fid{qid = QID, path = Path, state = FState}, Name, Perm, Mode) -> 164 ?if_supported(Mod:create({QID, FState}, Path, Name, Perm, Mode, State)). 165 166read({Mod, State0}, #fid{qid = QID, path = Path, state = FState0} = FID, Offset, Length) -> 167 case Mod:read({QID, FState0}, Path, Offset, Length, State0) of 168 {ok, {FState, Data}, State} -> {ok, {FID#fid{state = FState}, Data}, {Mod, State}}; 169 {error, Reason, StateE} -> {error, Reason, {Mod, StateE}} 170 end. 171 172write({Mod, State0}, #fid{qid = QID, path = Path, state = FState0} = FID, Offset, Data) -> 173 case Mod:write({QID, FState0}, Path, Offset, Data, State0) of 174 {ok, {FState, Len}, State} -> {ok, {FID#fid{state = FState}, Len}, {Mod, State}}; 175 {error, Reason, StateE} -> {error, Reason, {Mod, StateE}} 176 end. 177 178clunk({Mod, State0}, #fid{qid = QID, path = Path, state = FState}) -> 179 case erlang:function_exported(Mod, clunk, 3) of 180 true -> 181 case Mod:clunk({QID, FState}, Path, State0) of 182 {ok, State} -> {ok, {Mod, State}}; 183 {error, Reason, StateE} -> {error, Reason, {Mod, StateE}} 184 end; 185 false -> {ok, {Mod, State0}} 186 end. 187 188remove({Mod, State0}, #fid{qid = QID, path = Path, state = FState}) -> 189 case Mod:remove({QID, FState}, Path, State0) of 190 {ok, State} -> {ok, {Mod, State}}; 191 {error, Reason, State} -> {error, Reason, {Mod, State}} 192 end. 193 194stat({Mod, State0}, #fid{qid = QID, path = Path, state = FState0}) -> 195 case Mod:stat({QID, FState0}, Path, State0) of 196 {ok, Stat, State} -> {ok, Stat, {Mod, State}}; 197 {error, Reason, StateE} -> {error, Reason, {Mod, StateE}} 198 end. 199 200wstat({Mod, State0}, #fid{qid = QID, path = Path, state = FState}, Stat) -> 201 case Mod:wstat({QID, FState}, Path, Stat, State0) of 202 {ok, State} -> {ok, {Mod, State}}; 203 {error, Reason, State} -> {error, Reason, {Mod, State}} 204 end.