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-module(e9p_client).
6
7-include("e9p_internal.hrl").
8-include_lib("kernel/include/logger.hrl").
9
10-behaviour(gen_server).
11
12-export([attach/3]).
13-export([start_link/3]).
14-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
15
16attach(Client, Uname, Aname) ->
17 gen_server:call(Client, {attach, noauth, Uname, Aname}).
18
19start_link(Host, Port, Opts) ->
20 gen_server:start_link(?MODULE, {Host, Port, Opts}, []).
21
22init({Host, Port, _Opts}) ->
23 {ok, Socket} = gen_tcp:connect(Host, Port, [{active, false}, binary]),
24 case version_negotiation(Socket) of
25 {ok, #rversion{max_packet_size = MaxPacketSize, version = ?version}} ->
26 inet:setopts(Socket, [{active, once}]),
27 {ok,
28 #{socket => Socket,
29 buffer => <<>>,
30 tag => 0,
31 fid => 0,
32 msgs => #{},
33 max_packet_size => MaxPacketSize,
34 version => ?version}};
35 {ok, #rversion{version = OtherVersion}} ->
36 {error, {unsupported_version, OtherVersion}};
37 {error, _} = Error ->
38 Error
39 end.
40
41handle_call({attach, Auth, Uname, Aname}, From, State) ->
42 #{tag := Tag,
43 fid := Fid,
44 socket := Socket,
45 msgs := Msgs} =
46 State,
47 Afid =
48 case Auth of
49 noauth ->
50 ?nofid;
51 Id ->
52 Id
53 end,
54 Msg = #tattach{fid = Fid,
55 afid = Afid,
56 uname = Uname,
57 aname = Aname},
58 e9p_transport:send(Socket, Tag, Msg),
59 {noreply,
60 State#{tag := Tag + 1,
61 fid := Fid + 1,
62 msgs := Msgs#{Tag => {From, #{fid => Fid}}}}};
63handle_call(_Msg, _From, State) ->
64 {reply, {error, not_implemented}, State}.
65
66handle_cast(_Msg, State) ->
67 {noreply, State}.
68
69handle_info({tcp, Socket, Data}, #{socket := Socket} = State) ->
70 #{buffer := Buffer, msgs := Msgs0} = State,
71 inet:setopts(Socket, [{active, once}]),
72 case e9p_transport:read_stream(<<Buffer/binary, Data/binary>>) of
73 {ok, Tag, Msg, Rest} ->
74 Msgs =
75 case maps:take(Tag, Msgs0) of
76 {{From, _}, M} ->
77 gen_server:reply(From, {ok, Msg}),
78 M;
79 error ->
80 ?LOG_WARNING("Unknown tag ~p", [Tag]),
81 Msgs0
82 end,
83 {noreply, State#{buffer := Rest, msgs := Msgs}};
84 {more, Data} ->
85 {noreply, State#{buffer := Data}}
86 end.
87
88version_negotiation(Socket) ->
89 Msg = #tversion{max_packet_size = ?max_packet_size, version = ?version},
90 e9p_transport:send(Socket, notag, Msg),
91 case e9p_transport:read(Socket) of
92 {ok, _, Resp} ->
93 {ok, Resp};
94 {error, _} = Error ->
95 Error
96 end.