Pure Erlang implementation of 9p2000 protocol
filesystem
fs
9p2000
erlang
9p
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.