% SPDX-FileCopyrightText: 2025 Ɓukasz Niemier <~@hauleth.dev> % % SPDX-License-Identifier: Apache-2.0 %% @doc Protocol messages parsing and encoding. %% @end -module(e9p_msg). -export([parse/1, encode/2, encode_stat/1, parse_stat/1]). -export_type([tag/0, message/0, request_message/0, response_message/0 ]). -include("e9p_internal.hrl"). -type tag() :: 16#0000..16#FFFF. -type request_message() :: #tversion{} | #tauth{} | #tattach{} | #tflush{} | #twalk{} | #topen{} | #tcreate{} | #tread{} | #twrite{} | #tclunk{} | #tremove{} | #tstat{} | #twstat{}. -type response_message() :: #rversion{} | #rauth{} | #rattach{} | #rerror{} | #rflush{} | #rwalk{} | #ropen{} | #rcreate{} | #rread{} | #rwrite{} | #rclunk{} | #rstat{} | #rwstat{}. -type message() :: request_message() | response_message(). -spec parse(binary()) -> {ok, tag(), message()} | {error, term()}. parse(<>) -> case do_parse(Type, Data) of {ok, Parsed} -> {ok, Tag, Parsed}; {error, Reason} -> {error, Reason} end. %% version - negotiate protocol version do_parse(?Tversion, <>) -> {ok, #tversion{max_packet_size = MSize, version = Version}}; do_parse(?Rversion, <>) -> {ok, #rversion{max_packet_size = MSize, version = Version}}; %% attach, auth - messages to establish a connection do_parse(?Tauth, <>) -> {ok, #tauth{afid = AFID, uname = Uname, aname = Aname}}; do_parse(?Rauth, <>) -> {ok, #rauth{aqid = binary_to_qid(AQID)}}; do_parse(?Tattach, <>) -> {ok, #tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname}}; do_parse(?Rattach, <>) -> {ok, #rattach{qid = binary_to_qid(QID)}}; %% clunk - forget about a fid do_parse(?Tclunk, <>) -> {ok, #tclunk{fid = FID}}; do_parse(?Rclunk, <<>>) -> {ok, #rclunk{}}; %% error - return an error do_parse(?Rerror, <>) -> {ok, #rerror{msg = Error}}; %% flush - abort a message do_parse(?Tflush, <>) -> {ok, #tflush{tag = Tag}}; do_parse(?Rflush, <<>>) -> {ok, #rflush{}}; %% open, create - prepare a fid for I/O on an existing or new file do_parse(?Topen, <>) -> {ok, #topen{fid = FID, mode = Mode}}; do_parse(?Ropen, <>) -> {ok, #ropen{qid = binary_to_qid(QID), io_unit = IOUnit}}; do_parse(?Tcreate, <>) -> {ok, #tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}}; do_parse(?Rcreate, <>) -> {ok, #rcreate{qid = binary_to_qid(QID), io_unit = IOUnit}}; %% remove - remove a file from a server do_parse(?Tremove, <>) -> {ok, #tremove{fid = FID}}; do_parse(?Rremove, <<>>) -> {ok, #rremove{}}; %% stat, wstat - inquire or change file attributes do_parse(?Tstat, <>) -> {ok, #tstat{fid = FID}}; do_parse(?Rstat, <>) -> case parse_stat(Data) of {ok, Stat} -> {ok, #rstat{stat = Stat}}; {error, _} = Error -> Error end; do_parse(?Twstat, <>) -> case parse_stat(Data) of {ok, Stat} -> {ok, #twstat{fid = FID, stat = Stat}}; {error, _} = Error -> Error end; do_parse(?Rwstat, <<>>) -> {ok, #rwstat{}}; %% walk - descend a directory hierarchy do_parse(?Twalk, <>) -> NWNames = [Name || <> <= Rest], Len = length(NWNames), if Len == NWNLen -> {ok, #twalk{fid = FID, new_fid = NewFID, names = NWNames}}; true -> {error, {invalid_walk_length, NWNLen, Len}} end; do_parse(?Rwalk, <>) -> {ok, #rwalk{qids = [binary_to_qid(QID) || <> <= QIDs]}}; do_parse(?Tread, <>) -> {ok, #tread{fid = FID, offset = Offset, len = Len}}; do_parse(?Rread, <>) -> {ok, #rread{data = Data}}; do_parse(?Twrite, <>) -> {ok, #twrite{fid = FID, offset = Offset, data = Data}}; do_parse(?Rwrite, <>) -> {ok, #rwrite{len = Len}}; do_parse(Type, Data) -> {error, {invalid_message, Type, Data}}. parse_stat(<<_Size:2/?int, Type:2/?int, Dev:4/?int, RawQID:13/binary, Mode:4/?int, Atime:4/?int, Mtime:4/?int, Len:8/?int, NLen:?len, Name:NLen/binary, ULen:?len, Uid:ULen/binary, GLen:?len, Gid:GLen/binary, MULen:?len, MUid:MULen/binary>>) -> QID = binary_to_qid(RawQID), Flags = qid_to_mode_flags(QID), {ok, #{ type => Type, dev => Dev, qid => QID, mode => Mode band (bnot Flags), atime => Atime, mtime => Mtime, length => Len, name => Name, uid => Uid, gid => Gid, muid => MUid }}; parse_stat(_) -> {error, invalid_stat_data}. -spec encode(Tag :: tag() | notag, Data :: message()) -> iodata(). encode(Tag, Data) -> {MT, Encoded} = do_encode(Data), Tag0 = case Tag of notag -> ?notag; V -> V end, [<> | Encoded]. do_encode(#tversion{max_packet_size = MSize, version = Version}) -> {?Tversion, [<> | encode_str(Version)]}; do_encode(#rversion{max_packet_size = MSize, version = Version}) -> {?Rversion, [<> | encode_str(Version)]}; do_encode(#tauth{afid = AFID, uname = Uname, aname = Aname}) -> {?Tauth, [<>, encode_str(Uname), encode_str(Aname)]}; do_encode(#rauth{aqid = AQID}) -> {?Rauth, qid_to_binary(AQID)}; do_encode(#tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname}) -> {?Tattach, [<>, encode_str(Uname), encode_str(Aname)]}; do_encode(#rattach{qid = QID}) -> {?Rattach, qid_to_binary(QID)}; do_encode(#tclunk{fid = FID}) -> {?Tclunk, <>}; do_encode(#rclunk{}) -> {?Rclunk, []}; do_encode(#rerror{msg = Error}) -> {?Rerror, encode_str(Error)}; do_encode(#tflush{tag = Tag}) -> {?Tflush, <>}; do_encode(#rflush{}) -> {?Rflush, []}; do_encode(#topen{fid = FID, mode = Mode}) -> {?Topen, <>}; do_encode(#ropen{qid = QID, io_unit = IOUnit}) -> {?Ropen, [qid_to_binary(QID), <>]}; do_encode(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}) -> {?Tcreate, [<>, encode_str(Name), <>]}; do_encode(#rcreate{qid = QID, io_unit = IOUnit}) -> {?Rcreate, [qid_to_binary(QID), <>]}; do_encode(#tremove{fid = FID}) -> {?Tremove, <>}; do_encode(#rremove{}) -> {?Rremove, []}; do_encode(#tstat{fid = FID}) -> {?Tstat, <>}; do_encode(#rstat{stat = Stat}) -> Encoded = encode_stat(Stat), Len = iolist_size(Encoded), {?Rstat, [<> | encode_stat(Stat)]}; do_encode(#twstat{fid = FID, stat = Stat}) -> Encoded = encode_stat(Stat), Len = iolist_size(Encoded), {?Twstat, [<> | encode_stat(Stat)]}; do_encode(#rwstat{}) -> {?Rwstat, []}; do_encode(#twalk{fid = FID, new_fid = NewFID, names = Names}) -> ENames = [encode_str(Name) || Name <- Names], Len = length(ENames), {?Twalk, [<> | ENames]}; do_encode(#rwalk{qids = QIDs}) -> EQIDs = [qid_to_binary(QID) || QID <- QIDs], Len = length(EQIDs), {?Rwalk, [<> | EQIDs]}; do_encode(#tread{fid = FID, offset = Offset, len = Len}) -> {?Tread, <>}; do_encode(#rread{data = Data}) -> Len = iolist_size(Data), {?Rread, [<> | Data]}; do_encode(#twrite{fid = FID, offset = Offset, data = Data}) -> Len = iolist_size(Data), {?Twrite, [<>, Data]}; do_encode(#rwrite{len = Len}) -> {?Rwrite, [<>]}. encode_stat(Stat) -> #{ type := Type, dev := Dev, qid := QID, mode := Mode, atime := Atime, mtime := Mtime, length := Len, name := Name, uid := Uid, gid := Gid, muid := MUid } = maps:merge( #{ type => 0, dev => 0, mode => 0, atime => 0, mtime => 0, uid => ~"", gid => ~"", muid => ~"" }, Stat), FullMode = qid_to_mode_flags(QID) bor Mode, Encoded = [<< Type:2/?int, Dev:4/?int >>, qid_to_binary(QID), <>, time_to_encoded_sec(Atime), time_to_encoded_sec(Mtime), <>, encode_str(Name), encode_str(Uid), encode_str(Gid), encode_str(MUid) ], ELen = iolist_size(Encoded), [<> | Encoded]. qid_to_mode_flags(#qid{type = Type}) -> (Type band 2#11100100) bsl 24. %% ========== Utilities ========== encode_str(Data0) -> Data = unicode:characters_to_binary(Data0), true = is_binary(Data), Len = iolist_size(Data), [<>, Data]. binary_to_qid(<>) -> #qid{type = Type, version = Version, path = Path}. qid_to_binary(#qid{type = Type, version = Version, path = Path}) -> <>. time_to_encoded_sec(Sec) when is_integer(Sec) -> <>; time_to_encoded_sec(Time) -> Sec = calendar:universal_time_to_system_time(Time, [{unit, second}]), <>.