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 Protocol messages parsing and encoding.
6%% @end
7-module(e9p_msg).
8
9-export([parse/1, encode/2, encode_stat/1, parse_stat/1]).
10
11-export_type([tag/0,
12 message/0,
13 request_message/0,
14 response_message/0
15 ]).
16
17-include("e9p_internal.hrl").
18
19-type tag() :: 16#0000..16#FFFF.
20
21-type request_message() ::
22 #tversion{} |
23 #tauth{} |
24 #tattach{} |
25 #tflush{} |
26 #twalk{} |
27 #topen{} |
28 #tcreate{} |
29 #tread{} |
30 #twrite{} |
31 #tclunk{} |
32 #tremove{} |
33 #tstat{} |
34 #twstat{}.
35
36-type response_message() ::
37 #rversion{} |
38 #rauth{} |
39 #rattach{} |
40 #rerror{} |
41 #rflush{} |
42 #rwalk{} |
43 #ropen{} |
44 #rcreate{} |
45 #rread{} |
46 #rwrite{} |
47 #rclunk{} |
48 #rstat{} |
49 #rwstat{}.
50
51-type message() :: request_message() | response_message().
52
53-spec parse(binary()) -> {ok, tag(), message()} | {error, term()}.
54parse(<<Type:1/?int, Tag:2/?int, Data/binary>>) ->
55 case do_parse(Type, Data) of
56 {ok, Parsed} ->
57 {ok, Tag, Parsed};
58 {error, Reason} ->
59 {error, Reason}
60 end.
61
62%% version - negotiate protocol version
63do_parse(?Tversion, <<MSize:4/?int, VSize:?len, Version:VSize/binary>>) ->
64 {ok, #tversion{max_packet_size = MSize, version = Version}};
65do_parse(?Rversion, <<MSize:4/?int, VSize:?len, Version:VSize/binary>>) ->
66 {ok, #rversion{max_packet_size = MSize, version = Version}};
67
68%% attach, auth - messages to establish a connection
69do_parse(?Tauth, <<AFID:4/?int,
70 UnameLen:?len, Uname:UnameLen/binary,
71 AnameLen:?len, Aname:AnameLen/binary>>) ->
72 {ok, #tauth{afid = AFID,
73 uname = Uname,
74 aname = Aname}};
75do_parse(?Rauth, <<AQID:13/binary>>) ->
76 {ok, #rauth{aqid = binary_to_qid(AQID)}};
77
78do_parse(?Tattach, <<FID:4/?int,
79 AFID:4/?int,
80 ULen:?len, Uname:ULen/binary,
81 ALen:?len, Aname:ALen/binary>>) ->
82 {ok, #tattach{fid = FID,
83 afid = AFID,
84 uname = Uname,
85 aname = Aname}};
86do_parse(?Rattach, <<QID:13/binary>>) ->
87 {ok, #rattach{qid = binary_to_qid(QID)}};
88
89%% clunk - forget about a fid
90do_parse(?Tclunk, <<FID:4/?int>>) ->
91 {ok, #tclunk{fid = FID}};
92do_parse(?Rclunk, <<>>) ->
93 {ok, #rclunk{}};
94
95%% error - return an error
96do_parse(?Rerror, <<ELen:?len, Error:ELen/binary>>) ->
97 {ok, #rerror{msg = Error}};
98
99%% flush - abort a message
100do_parse(?Tflush, <<Tag:2/?int>>) ->
101 {ok, #tflush{tag = Tag}};
102do_parse(?Rflush, <<>>) ->
103 {ok, #rflush{}};
104
105%% open, create - prepare a fid for I/O on an existing or new file
106do_parse(?Topen, <<FID:4/?int, Mode:1/?int>>) ->
107 {ok, #topen{fid = FID, mode = Mode}};
108do_parse(?Ropen, <<QID:13/binary, IOUnit:4/?int>>) ->
109 {ok, #ropen{qid = binary_to_qid(QID), io_unit = IOUnit}};
110
111do_parse(?Tcreate, <<FID:4/?int,
112 NLen:?len, Name:NLen/binary,
113 Perm:4/?int,
114 Mode:1/?int>>) ->
115 {ok, #tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}};
116do_parse(?Rcreate, <<QID:13/binary, IOUnit:4/?int>>) ->
117 {ok, #rcreate{qid = binary_to_qid(QID), io_unit = IOUnit}};
118
119%% remove - remove a file from a server
120do_parse(?Tremove, <<FID:4/?int>>) ->
121 {ok, #tremove{fid = FID}};
122do_parse(?Rremove, <<>>) ->
123 {ok, #rremove{}};
124
125%% stat, wstat - inquire or change file attributes
126do_parse(?Tstat, <<FID:4/?int>>) ->
127 {ok, #tstat{fid = FID}};
128do_parse(?Rstat, <<DLen:?len, Data:DLen/binary>>) ->
129 case parse_stat(Data) of
130 {ok, Stat} ->
131 {ok, #rstat{stat = Stat}};
132
133 {error, _} = Error ->
134 Error
135 end;
136
137do_parse(?Twstat, <<FID:4/?int, DLen:?len, Data:DLen/binary>>) ->
138 case parse_stat(Data) of
139 {ok, Stat} ->
140 {ok, #twstat{fid = FID, stat = Stat}};
141
142 {error, _} = Error ->
143 Error
144 end;
145do_parse(?Rwstat, <<>>) ->
146 {ok, #rwstat{}};
147
148%% walk - descend a directory hierarchy
149do_parse(?Twalk, <<FID:4/?int, NewFID:4/?int, NWNLen:?len, Rest/binary>>) ->
150 NWNames = [Name || <<NLen:?len, Name:NLen/binary>> <= Rest],
151 Len = length(NWNames),
152 if
153 Len == NWNLen ->
154 {ok, #twalk{fid = FID, new_fid = NewFID, names = NWNames}};
155 true ->
156 {error, {invalid_walk_length, NWNLen, Len}}
157 end;
158do_parse(?Rwalk, <<NWQLen:?len, QIDs:(NWQLen * 13)/binary>>) ->
159 {ok, #rwalk{qids = [binary_to_qid(QID) || <<QID:13/binary>> <= QIDs]}};
160
161do_parse(?Tread, <<FID:4/?int, Offset:8/?int, Len:4/?int>>) ->
162 {ok, #tread{fid = FID, offset = Offset, len = Len}};
163do_parse(?Rread, <<Count:4/?int, Data:Count/binary>>) ->
164 {ok, #rread{data = Data}};
165do_parse(?Twrite, <<FID:4/?int, Offset:8/?int, Len:4/?int, Data:Len/binary>>) ->
166 {ok, #twrite{fid = FID, offset = Offset, data = Data}};
167do_parse(?Rwrite, <<Len:4/?int>>) ->
168 {ok, #rwrite{len = Len}};
169
170do_parse(Type, Data) ->
171 {error, {invalid_message, Type, Data}}.
172
173parse_stat(<<_Size:2/?int,
174 Type:2/?int,
175 Dev:4/?int,
176 RawQID:13/binary,
177 Mode:4/?int,
178 Atime:4/?int,
179 Mtime:4/?int,
180 Len:8/?int,
181 NLen:?len, Name:NLen/binary,
182 ULen:?len, Uid:ULen/binary,
183 GLen:?len, Gid:GLen/binary,
184 MULen:?len, MUid:MULen/binary>>)
185->
186 QID = binary_to_qid(RawQID),
187 Flags = qid_to_mode_flags(QID),
188 {ok, #{
189 type => Type,
190 dev => Dev,
191 qid => QID,
192 mode => Mode band (bnot Flags),
193 atime => Atime,
194 mtime => Mtime,
195 length => Len,
196 name => Name,
197 uid => Uid,
198 gid => Gid,
199 muid => MUid
200 }};
201parse_stat(_) -> {error, invalid_stat_data}.
202
203-spec encode(Tag :: tag() | notag, Data :: message()) -> iodata().
204encode(Tag, Data) ->
205 {MT, Encoded} = do_encode(Data),
206 Tag0 = case Tag of
207 notag -> ?notag;
208 V -> V
209 end,
210 [<<MT:1/?int, Tag0:2/?int>> | Encoded].
211
212do_encode(#tversion{max_packet_size = MSize, version = Version}) ->
213 {?Tversion, [<<MSize:4/?int>> | encode_str(Version)]};
214do_encode(#rversion{max_packet_size = MSize, version = Version}) ->
215 {?Rversion, [<<MSize:4/?int>> | encode_str(Version)]};
216
217do_encode(#tauth{afid = AFID, uname = Uname, aname = Aname}) ->
218 {?Tauth, [<<AFID:4/?int>>, encode_str(Uname), encode_str(Aname)]};
219do_encode(#rauth{aqid = AQID}) ->
220 {?Rauth, qid_to_binary(AQID)};
221
222do_encode(#tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname}) ->
223 {?Tattach, [<<FID:4/?int, AFID:4/?int>>, encode_str(Uname), encode_str(Aname)]};
224do_encode(#rattach{qid = QID}) ->
225 {?Rattach, qid_to_binary(QID)};
226
227do_encode(#tclunk{fid = FID}) ->
228 {?Tclunk, <<FID:4/?int>>};
229do_encode(#rclunk{}) ->
230 {?Rclunk, []};
231
232do_encode(#rerror{msg = Error}) ->
233 {?Rerror, encode_str(Error)};
234
235do_encode(#tflush{tag = Tag}) ->
236 {?Tflush, <<Tag:2/?int>>};
237do_encode(#rflush{}) ->
238 {?Rflush, []};
239
240do_encode(#topen{fid = FID, mode = Mode}) ->
241 {?Topen, <<FID:4/?int, Mode:1/?int>>};
242do_encode(#ropen{qid = QID, io_unit = IOUnit}) ->
243 {?Ropen, [qid_to_binary(QID), <<IOUnit:4/?int>>]};
244
245do_encode(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}) ->
246 {?Tcreate, [<<FID:4/?int>>, encode_str(Name), <<Perm:4/?int, Mode:1/?int>>]};
247do_encode(#rcreate{qid = QID, io_unit = IOUnit}) ->
248 {?Rcreate, [qid_to_binary(QID), <<IOUnit:4/?int>>]};
249
250do_encode(#tremove{fid = FID}) ->
251 {?Tremove, <<FID:4/?int>>};
252do_encode(#rremove{}) ->
253 {?Rremove, []};
254
255do_encode(#tstat{fid = FID}) ->
256 {?Tstat, <<FID:4/?int>>};
257do_encode(#rstat{stat = Stat}) ->
258 Encoded = encode_stat(Stat),
259 Len = iolist_size(Encoded),
260 {?Rstat, [<<Len:?len>> | encode_stat(Stat)]};
261
262do_encode(#twstat{fid = FID, stat = Stat}) ->
263 Encoded = encode_stat(Stat),
264 Len = iolist_size(Encoded),
265 {?Twstat, [<<FID:4/?int, Len:?len>> | encode_stat(Stat)]};
266do_encode(#rwstat{}) ->
267 {?Rwstat, []};
268
269do_encode(#twalk{fid = FID, new_fid = NewFID, names = Names}) ->
270 ENames = [encode_str(Name) || Name <- Names],
271 Len = length(ENames),
272 {?Twalk, [<<FID:4/?int, NewFID:4/?int, Len:?len>> | ENames]};
273do_encode(#rwalk{qids = QIDs}) ->
274 EQIDs = [qid_to_binary(QID) || QID <- QIDs],
275 Len = length(EQIDs),
276 {?Rwalk, [<<Len:?len>> | EQIDs]};
277
278do_encode(#tread{fid = FID, offset = Offset, len = Len}) ->
279 {?Tread, <<FID:4/?int, Offset:8/?int, Len:4/?int>>};
280do_encode(#rread{data = Data}) ->
281 Len = iolist_size(Data),
282 {?Rread, [<<Len:4/?int>> | Data]};
283do_encode(#twrite{fid = FID, offset = Offset, data = Data}) ->
284 Len = iolist_size(Data),
285 {?Twrite, [<<FID:4/?int, Offset:8/?int, Len:4/?int>>, Data]};
286do_encode(#rwrite{len = Len}) ->
287 {?Rwrite, [<<Len:4/?int>>]}.
288
289encode_stat(Stat) ->
290 #{
291 type := Type,
292 dev := Dev,
293 qid := QID,
294 mode := Mode,
295 atime := Atime,
296 mtime := Mtime,
297 length := Len,
298 name := Name,
299 uid := Uid,
300 gid := Gid,
301 muid := MUid
302 } = maps:merge(
303 #{
304 type => 0,
305 dev => 0,
306 mode => 0,
307 atime => 0,
308 mtime => 0,
309 uid => ~"",
310 gid => ~"",
311 muid => ~""
312 }, Stat),
313 FullMode = qid_to_mode_flags(QID) bor Mode,
314 Encoded = [<<
315 Type:2/?int,
316 Dev:4/?int
317 >>,
318 qid_to_binary(QID),
319 <<FullMode:4/?int>>,
320 time_to_encoded_sec(Atime),
321 time_to_encoded_sec(Mtime),
322 <<Len:8/?int>>,
323 encode_str(Name),
324 encode_str(Uid),
325 encode_str(Gid),
326 encode_str(MUid)
327 ],
328 ELen = iolist_size(Encoded),
329 [<<ELen:?len>> | Encoded].
330
331qid_to_mode_flags(#qid{type = Type}) ->
332 (Type band 2#11100100) bsl 24.
333
334%% ========== Utilities ==========
335
336encode_str(Data0) ->
337 Data = unicode:characters_to_binary(Data0),
338 true = is_binary(Data),
339 Len = iolist_size(Data),
340 [<<Len:?len>>, Data].
341
342binary_to_qid(<<Type:1/?int, Version:4/?int, Path:8/?int>>) ->
343 #qid{type = Type, version = Version, path = Path}.
344
345qid_to_binary(#qid{type = Type, version = Version, path = Path}) ->
346 <<Type:1/?int, Version:4/?int, Path:8/?int>>.
347
348time_to_encoded_sec(Sec) when is_integer(Sec) -> <<Sec:4/?int>>;
349time_to_encoded_sec(Time) ->
350 Sec = calendar:universal_time_to_system_time(Time, [{unit, second}]),
351 <<Sec:4/?int>>.