Pure Erlang implementation of 9p2000 protocol
filesystem fs 9p2000 erlang 9p
at master 351 lines 11 kB view raw
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>>.