Pure Erlang implementation of 9p2000 protocol
filesystem fs 9p2000 erlang 9p
at master 199 lines 6.2 kB view raw
1% SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2% 3% SPDX-License-Identifier: Apache-2.0 4 5-module(e9p_unfs). 6 7-moduledoc """ 8Expose Unix Filesystem as 9p2000 mount 9""". 10 11-behaviour(e9p_fs). 12 13-include_lib("kernel/include/logger.hrl"). 14-include_lib("kernel/include/file.hrl"). 15 16-export([init/1, root/3, walk/4, stat/3, open/4, read/5, clunk/2, create/6, 17 write/5, remove/3, wstat/4]). 18 19% Create QID and Stat data for given path. 20qid(Root, Path) -> 21 FullPath = filename:join([Root] ++ Path), 22 case file:read_file_info(FullPath, [{time, posix}, raw]) of 23 {ok, #file_info{type = Type, inode = Inode} = FI} -> 24 QID = e9p:make_qid(Type, 0, Inode), 25 Stat = file_info_to_stat(Path, QID, FI), 26 {ok, QID, Stat}; 27 {error, _} = Error -> Error 28 end. 29 30file_info_to_stat( 31 Path, 32 QID, 33 #file_info{ 34 size = Len, 35 atime = Atime, 36 mtime = Mtime, 37 mode = Mode 38 }) -> 39 Name = if 40 Path == [] -> ~"/"; 41 true -> lists:last(Path) 42 end, 43 #{ 44 qid => QID, 45 mode => Mode, 46 atime => Atime, 47 mtime => Mtime, 48 length => Len, 49 name => Name 50 }. 51 52stat_to_file_info(Stat) -> 53 #{ 54 mode := Mode, 55 atime := Atime, 56 mtime := Mtime 57 } = Stat, 58 #file_info{ 59 mode = Mode, 60 atime = Atime, 61 mtime = Mtime 62 }. 63 64%% ====== Filesystem handlers ====== 65 66-doc false. 67init(#{path := Path}) -> 68 {ok, #{root => unicode:characters_to_binary(Path)}}. 69 70-doc false. 71root(UName, AName, #{root := Root} = State) -> 72 ?LOG_INFO(#{uname => UName, aname => AName}), 73 maybe 74 {ok, Qid, _Stat} ?= qid(Root, []), 75 {ok, {Qid, []}, State} 76 end. 77 78-doc false. 79walk(_QID, Path, ~"..", #{root := Root} = State) -> 80 case qid(Root, lists:droplast(Path)) of 81 {ok, NQid, _Stat} -> {{NQid, []}, State}; 82 {error, _} -> {false, State} 83 end; 84walk(_QID, Path, File, #{root := Root} = State) -> 85 case qid(Root, Path ++ [File]) of 86 {ok, NQid, _Stat} -> {{NQid, []}, State}; 87 {error, _} -> {false, State} 88 end. 89 90-doc false. 91stat({QID, _}, Path, #{root := Root} = State) -> 92 FullPath = filename:join([Root] ++ Path), 93 case file:read_file_info(FullPath, [{time, posix}, raw]) of 94 {ok, FileInfo} -> 95 Stat = file_info_to_stat(Path, QID, FileInfo), 96 {ok, Stat, State}; 97 {error, Error} -> 98 {error, Error, State} 99 end. 100 101-doc false. 102wstat(_QID, Path, Stat, #{root := Root} = State) -> 103 FileInfo = stat_to_file_info(Stat), 104 FullPath = filename:join([Root] ++ Path), 105 106 case file:write_file_info(FullPath, FileInfo, [{time, posix}, raw]) of 107 ok -> {ok, State}; 108 {error, Reason} -> 109 {error, io_lib:format("Couldn't write file stat: ~p", [Reason]), 110 State} 111 end. 112 113-doc false. 114open({QID, []}, Path, Mode, #{root := Root} = State) -> 115 FullPath = filename:join([Root] ++ Path), 116 QS = case e9p:is_type(QID, directory) of 117 true -> 118 % Currently `file` module do not expose raw mode for listing 119 % file directory, so we need to call private `prim_file` module 120 % to access such functionality. Otherwise we can encounter 121 % deadlock. 122 % 123 % See: https://github.com/erlang/otp/issues/10593 124 {ok, List} = prim_file:list_dir(FullPath), 125 {dir, List}; 126 false -> 127 {Trunc, Opts} = translate_mode(Mode), 128 {ok, FD} = file:open(FullPath, [raw, binary | Opts]), 129 if Trunc -> file:truncate(FD); true -> ok end, 130 {regular, FD} 131 end, 132 {ok, {QS, 0}, State}. 133 134-doc false. 135clunk({_, {regular, FD}}, State) -> 136 ok = file:close(FD), 137 {ok, State}; 138clunk(_QID, State) -> 139 {ok, State}. 140 141-doc false. 142create(_QID, _Path, _Name, _Perm, _Mode, State) -> 143 {error, "Unsupported", State}. 144 145-doc false. 146remove({QID, _} = FID, Path, #{root := Root} = State0) -> 147 FullPath = filename:join([Root] ++ Path), 148 {ok, State} = clunk(FID, State0), 149 case case e9p:is_type(QID, directory) of 150 % Currently `file` module do not expose raw mode for listing 151 % file directory, so we need to call private `prim_file` module 152 % to access such functionality. Otherwise we can encounter 153 % deadlock. 154 % 155 % See: https://github.com/erlang/otp/issues/10593 156 true -> prim_file:del_dir(FullPath); 157 false -> file:delete(FullPath, [raw]) 158 end of 159 ok -> {ok, State}; 160 {error, Reason} -> 161 {error, io_lib:format("Failed to remove path: ~p", [Reason]), State} 162 end. 163 164translate_mode([trunc | Rest]) -> 165 {_, Mode} = translate_mode(Rest), 166 {true, Mode}; 167translate_mode([read]) -> {false, [read]}; 168translate_mode([write]) -> {false, [read, write]}; 169translate_mode([append]) -> {false, [read, write]}; 170translate_mode([exec]) -> {false, [read]}. 171 172-doc false. 173read({_QID, {regular, FD}}, _Path, Offset, Len, State) -> 174 case file:pread(FD, Offset, Len) of 175 {ok, Data} -> {ok, {{regular, FD}, Data}, State}; 176 eof -> {ok, {{regular, FD}, []}, State}; 177 {error, Err} -> {error, Err, State} 178 end; 179read({_QID, {dir, List}}, Path, _Offset, Len, #{root := Root} = State) -> 180 {Remaining, Data} = readdir(Root, Path, List, Len, []), 181 {ok, {{dir, Remaining}, Data}, State}. 182 183readdir(_Root, _Path, List, 0, Acc) -> {List, Acc}; 184readdir(_Root, _Path, [], _Len, Acc) -> {[], Acc}; 185readdir(Root, Path, [Next | Rest], Len, Acc) -> 186 {ok, _QID, Stat} = qid(Root, Path ++ [Next]), 187 Encoded = e9p_msg:encode_stat(Stat), 188 Size = iolist_size(Encoded), 189 if 190 Size > Len -> []; 191 true -> readdir(Root, Path, Rest, Len - Size, [Encoded | Acc]) 192 end. 193 194-doc false. 195write({_QID, {regular, FD}}, _Path, Offset, Data, State) -> 196 case file:pwrite(FD, Offset, Data) of 197 ok -> {ok, {{regular, FD}, iolist_size(Data)}, State}; 198 {error, Err} -> {error, io_lib:format("Write error ~p", [Err]), State} 199 end.