From 564d16957b88ca034a985a1a6a69f70bee55780b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Niemier?= <~@hauleth.dev> Date: Mon, 26 Jan 2026 18:47:30 +0100 Subject: [PATCH] Add Unix extension parsing Per Postel's law, current implementation will accept extended messages irrelevant to selected version, but produced messages will always conform to specified version. --- src/e9p_internal.hrl | 8 +- src/e9p_msg.erl | 238 +++++++++++++++++++++++++++++++++--------- src/e9p_server.erl | 10 +- src/e9p_unfs.erl | 2 +- test/prop_e9p_msg.erl | 63 ++++++++++- 5 files changed, 254 insertions(+), 67 deletions(-) diff --git a/src/e9p_internal.hrl b/src/e9p_internal.hrl index e30c8e5..1c71007 100644 --- a/src/e9p_internal.hrl +++ b/src/e9p_internal.hrl @@ -59,15 +59,15 @@ -record(tversion, {max_packet_size, version}). -record(rversion, {max_packet_size, version}). --record(tauth, {afid, uname, aname}). +-record(tauth, {afid, uname, aname, uid}). -record(rauth, {aqid}). --record(rerror, {msg}). +-record(rerror, {msg, errno}). -record(tflush, {tag}). -record(rflush, {}). --record(tattach, {fid, afid, uname, aname}). +-record(tattach, {fid, afid, uname, aname, uid}). -record(rattach, {qid}). -record(twalk, {fid, new_fid, names}). @@ -76,7 +76,7 @@ -record(topen, {fid, mode}). -record(ropen, {qid, io_unit}). --record(tcreate, {fid, name, perm, mode}). +-record(tcreate, {fid, name, perm, mode, ext}). -record(rcreate, {qid, io_unit}). -record(tread, {fid, offset, len}). diff --git a/src/e9p_msg.erl b/src/e9p_msg.erl index c576df9..573b7d7 100644 --- a/src/e9p_msg.erl +++ b/src/e9p_msg.erl @@ -6,7 +6,7 @@ %% @end -module(e9p_msg). --export([parse/1, encode/2, encode_stat/1, parse_stat/1]). +-export([parse/1, encode/2, encode/3, encode_stat/2, parse_stat/1]). -export_type([ tag/0, @@ -75,6 +75,18 @@ do_parse( uname = Uname, aname = Aname }}; +% .u extension +do_parse( + ?Tauth, + <> +) -> + {ok, #tauth{ + afid = AFID, + uname = Uname, + aname = Aname, + uid = UID + }}; do_parse(?Rauth, <>) -> {ok, #rauth{aqid = binary_to_qid(AQID)}}; do_parse( @@ -87,6 +99,19 @@ do_parse( uname = Uname, aname = Aname }}; +% .u extension +do_parse( + ?Tattach, + <> +) -> + {ok, #tattach{ + fid = FID, + afid = AFID, + uname = Uname, + aname = Aname, + uid = UID + }}; do_parse(?Rattach, <>) -> {ok, #rattach{qid = binary_to_qid(QID)}}; %% clunk - forget about a fid @@ -97,6 +122,9 @@ do_parse(?Rclunk, <<>>) -> %% error - return an error do_parse(?Rerror, <>) -> {ok, #rerror{msg = Error}}; +% .u extension +do_parse(?Rerror, <>) -> + {ok, #rerror{msg = Error, errno = Errno}}; %% flush - abort a message do_parse(?Tflush, <>) -> {ok, #tflush{tag = Tag}}; @@ -109,6 +137,19 @@ 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}}; +% .u extension +do_parse( + ?Tcreate, + <> +) -> + {ok, #tcreate{ + fid = FID, + name = Name, + perm = Perm, + mode = Mode, + ext = Ext + }}; do_parse(?Rcreate, <>) -> {ok, #rcreate{qid = binary_to_qid(QID), io_unit = IOUnit}}; %% remove - remove a file from a server @@ -159,10 +200,12 @@ 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, + <> -) -> +) when + Size =:= 47 + NLen + ULen + GLen + MULen +-> QID = binary_to_qid(RawQID), Flags = qid_to_mode_flags(QID), {ok, #{ @@ -178,12 +221,42 @@ parse_stat( gid => Gid, muid => MUid }}; +% .u extension +parse_stat( + <> +) when + Size =:= 61 + NLen + ULen + GLen + MULen + ExtLen +-> + 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, + ext => Ext, + n_uid => NUid, + n_gid => NGid, + n_muid => NMUid + }}; parse_stat(_) -> {error, invalid_stat_data}. --spec encode(Tag :: tag() | notag, Data :: message()) -> iodata(). -encode(Tag, Data) -> - {MT, Encoded} = do_encode(Data), +encode(Tag, Data) -> encode(Tag, Data, legacy). + +-spec encode(Tag :: tag() | notag, Data :: message(), Version :: legacy | unix) -> iodata(). +encode(Tag, Data, Ver) -> + {MT, Encoded} = do_encode(Data, Ver), Tag0 = case Tag of notag -> ?notag; @@ -191,72 +264,107 @@ encode(Tag, Data) -> end, [<> | Encoded]. -do_encode(#tversion{max_packet_size = MSize, version = Version}) -> - {?Tversion, [<> | encode_str(Version)]}; -do_encode(#rversion{max_packet_size = MSize, version = Version}) -> +do_encode(#tversion{max_packet_size = MSize, version = Version}, _Ver) -> + {?Tversion, [<>, encode_str(Version)]}; +do_encode(#rversion{max_packet_size = MSize, version = Version}, _Ver) -> {?Rversion, [<> | encode_str(Version)]}; -do_encode(#tauth{afid = AFID, uname = Uname, aname = Aname}) -> +do_encode(#tauth{afid = AFID, uname = Uname, aname = Aname}, legacy) -> {?Tauth, [<>, encode_str(Uname), encode_str(Aname)]}; -do_encode(#rauth{aqid = AQID}) -> +do_encode(#tauth{afid = AFID, uname = Uname, aname = Aname, uid = UID}, unix) when + is_integer(UID) +-> + {?Tauth, [<>, encode_str(Uname), encode_str(Aname), <>]}; +do_encode(#rauth{aqid = AQID}, _Ver) -> {?Rauth, qid_to_binary(AQID)}; -do_encode(#tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname}) -> +do_encode(#tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname}, legacy) -> {?Tattach, [<>, encode_str(Uname), encode_str(Aname)]}; -do_encode(#rattach{qid = QID}) -> +do_encode(#tattach{fid = FID, afid = AFID, uname = Uname, aname = Aname, uid = UID}, unix) when + is_integer(UID) +-> + {?Tattach, [<>, encode_str(Uname), encode_str(Aname), <>]}; +do_encode(#rattach{qid = QID}, _Ver) -> {?Rattach, qid_to_binary(QID)}; -do_encode(#tclunk{fid = FID}) -> +do_encode(#tclunk{fid = FID}, _Ver) -> {?Tclunk, <>}; -do_encode(#rclunk{}) -> +do_encode(#rclunk{}, _Ver) -> {?Rclunk, []}; -do_encode(#rerror{msg = Error}) -> +do_encode(#rerror{msg = Error}, legacy) -> {?Rerror, encode_str(Error)}; -do_encode(#tflush{tag = Tag}) -> +do_encode(#rerror{msg = Error, errno = Errno}, _Ver) -> + {?Rerror, [encode_str(Error), <>]}; +do_encode(#tflush{tag = Tag}, _Ver) -> {?Tflush, <>}; -do_encode(#rflush{}) -> +do_encode(#rflush{}, _Ver) -> {?Rflush, []}; -do_encode(#topen{fid = FID, mode = Mode}) -> +do_encode(#topen{fid = FID, mode = Mode}, _Ver) -> {?Topen, <>}; -do_encode(#ropen{qid = QID, io_unit = IOUnit}) -> +do_encode(#ropen{qid = QID, io_unit = IOUnit}, _Ver) -> {?Ropen, [qid_to_binary(QID), <>]}; -do_encode(#tcreate{fid = FID, name = Name, perm = Perm, mode = Mode}) -> +do_encode( + #tcreate{ + fid = FID, + name = Name, + perm = Perm, + mode = Mode + }, + legacy +) -> {?Tcreate, [<>, encode_str(Name), <>]}; -do_encode(#rcreate{qid = QID, io_unit = IOUnit}) -> +do_encode( + #tcreate{ + fid = FID, + name = Name, + perm = Perm, + mode = Mode, + ext = Ext + }, + unix +) -> + {?Tcreate, [ + <>, + encode_str(Name), + <>, + encode_str(Ext) + ]}; +do_encode(#rcreate{qid = QID, io_unit = IOUnit}, _Ver) -> {?Rcreate, [qid_to_binary(QID), <>]}; -do_encode(#tremove{fid = FID}) -> +do_encode(#tremove{fid = FID}, _Ver) -> {?Tremove, <>}; -do_encode(#rremove{}) -> +do_encode(#rremove{}, _Ver) -> {?Rremove, []}; -do_encode(#tstat{fid = FID}) -> +do_encode(#tstat{fid = FID}, _Ver) -> {?Tstat, <>}; -do_encode(#rstat{stat = Stat}) -> - Encoded = encode_stat(Stat), +do_encode(#rstat{stat = Stat}, Ver) -> + Encoded = encode_stat(Stat, Ver), Len = iolist_size(Encoded), - {?Rstat, [<> | encode_stat(Stat)]}; -do_encode(#twstat{fid = FID, stat = Stat}) -> - Encoded = encode_stat(Stat), + {?Rstat, [<>, Encoded]}; +do_encode(#twstat{fid = FID, stat = Stat}, Ver) -> + Encoded = encode_stat(Stat, Ver), Len = iolist_size(Encoded), - {?Twstat, [<> | encode_stat(Stat)]}; -do_encode(#rwstat{}) -> + {?Twstat, [<>, Encoded]}; +do_encode(#rwstat{}, _Ver) -> {?Rwstat, []}; -do_encode(#twalk{fid = FID, new_fid = NewFID, names = Names}) -> +do_encode(#twalk{fid = FID, new_fid = NewFID, names = Names}, _Ver) -> ENames = [encode_str(Name) || Name <- Names], Len = length(ENames), {?Twalk, [<> | ENames]}; -do_encode(#rwalk{qids = QIDs}) -> +do_encode(#rwalk{qids = QIDs}, _Ver) -> EQIDs = [qid_to_binary(QID) || QID <- QIDs], Len = length(EQIDs), {?Rwalk, [<> | EQIDs]}; -do_encode(#tread{fid = FID, offset = Offset, len = Len}) -> +do_encode(#tread{fid = FID, offset = Offset, len = Len}, _Ver) -> {?Tread, <>}; -do_encode(#rread{data = Data}) -> +do_encode(#rread{data = Data}, _Ver) -> Len = iolist_size(Data), {?Rread, [<> | Data]}; -do_encode(#twrite{fid = FID, offset = Offset, data = Data}) -> +do_encode(#twrite{fid = FID, offset = Offset, data = Data}, _Ver) -> Len = iolist_size(Data), {?Twrite, [<>, Data]}; -do_encode(#rwrite{len = Len}) -> +do_encode(#rwrite{len = Len}, _Ver) -> {?Rwrite, [<>]}. -encode_stat(Stat) -> +-spec encode_stat(e9p:stat(), legacy | unix) -> iodata(). +encode_stat(Stat, Ver) -> #{ type := Type, dev := Dev, @@ -269,19 +377,24 @@ encode_stat(Stat) -> uid := Uid, gid := Gid, muid := MUid - } = maps:merge( - #{ - type => 0, - dev => 0, - mode => 0, - atime => 0, - mtime => 0, - uid => ~"", - gid => ~"", - muid => ~"" - }, - Stat - ), + } = + Data = maps:merge( + #{ + type => 0, + dev => 0, + mode => 0, + atime => 0, + mtime => 0, + uid => ~"", + gid => ~"", + muid => ~"", + extra => ~"", + n_uid => 0, + n_gid => 0, + n_muid => 0 + }, + Stat + ), FullMode = qid_to_mode_flags(QID) bor Mode, Encoded = [ << @@ -297,10 +410,31 @@ encode_stat(Stat) -> encode_str(Uid), encode_str(Gid), encode_str(MUid) + | encode_unix(Data, Ver) ], ELen = iolist_size(Encoded), [<> | Encoded]. +encode_unix(_, legacy) -> + []; +encode_unix( + #{ + extra := Ext, + n_uid := UID, + n_gid := GID, + n_muid := MUID + }, + unix +) -> + [ + encode_str(Ext), + << + UID:4/?int, + GID:4/?int, + MUID:4/?int + >> + ]. + qid_to_mode_flags(#qid{type = Type}) -> (Type band 2#11100100) bsl 24. diff --git a/src/e9p_server.erl b/src/e9p_server.erl index 9ce707c..649c3f3 100644 --- a/src/e9p_server.erl +++ b/src/e9p_server.erl @@ -22,7 +22,6 @@ -record(state, { socket, % trans_mod = gen_tcp, - % ver, fids = #{}, handler }). @@ -35,7 +34,7 @@ start_link(Port, Handler) -> setup_acceptor(Parent, Port, Handler0) -> maybe - {ok, LSock} ?= gen_tcp:listen(Port, [binary, {active, false}]), + {ok, LSock} ?= gen_tcp:listen(Port, [{ip, {0, 0, 0, 0}}, binary, {active, false}]), {ok, Handler} ?= e9p_fs:init(Handler0), proc_lib:init_ack(Parent, {ok, self()}), ?MODULE:accept_loop(LSock, Handler) @@ -64,7 +63,6 @@ accept_loop(LSock, Handler) -> loop(#state{socket = Sock} = State) -> case e9p_transport:read(Sock) of {ok, Tag, Data} -> - ?LOG_DEBUG(#{message => Data, tag => Tag}), try handle_message(Data, State#state.fids, State#state.handler) of {ok, Reply, FIDs, Handler} -> e9p_transport:send(Sock, Tag, Reply), @@ -85,10 +83,11 @@ loop(#state{socket = Sock} = State) -> ?MODULE:loop(State) end; {error, closed} -> - ?LOG_INFO("Connection closed"), ok end. +handle_message(#tversion{version = ~"9P2000.u", max_packet_size = MPS}, FIDs, Handler) -> + {ok, #rversion{version = ~"9P2000.u", max_packet_size = MPS}, FIDs, Handler}; handle_message(#tversion{version = <<"9P2000", _/binary>>, max_packet_size = MPS}, FIDs, Handler) -> % Currently only "basic" 9p2000 version is supported, without any extensions % like `.u` or `.L` @@ -97,7 +96,8 @@ handle_message(#tflush{}, FIDs, Handler) -> % Currently there is no support for parallel messages, so this does simply % nothing {ok, #rflush{}, FIDs, Handler}; -handle_message(#tattach{fid = FID, uname = UName, aname = AName}, FIDs, Handler0) -> +handle_message(#tattach{fid = FID, uname = UName, aname = AName, uid = UID}, FIDs, Handler0) -> + ?LOG_NOTICE(#{uname => UName, aname => AName, uid => UID}), maybe {ok, QID, Handler} ?= e9p_fs:root(Handler0, UName, AName), NFIDs = FIDs#{FID => QID}, diff --git a/src/e9p_unfs.erl b/src/e9p_unfs.erl index f83c192..870af76 100644 --- a/src/e9p_unfs.erl +++ b/src/e9p_unfs.erl @@ -207,7 +207,7 @@ readdir(_Root, _Path, [], _Len, Acc) -> {[], Acc}; readdir(Root, Path, [Next | Rest], Len, Acc) -> {ok, _QID, Stat} = qid(Root, Path ++ [Next]), - Encoded = e9p_msg:encode_stat(Stat), + Encoded = e9p_msg:encode_stat(Stat, legacy), Size = iolist_size(Encoded), if Size > Len -> []; diff --git a/test/prop_e9p_msg.erl b/test/prop_e9p_msg.erl index 89eb222..2dd6116 100644 --- a/test/prop_e9p_msg.erl +++ b/test/prop_e9p_msg.erl @@ -64,6 +64,21 @@ prop_tauth() -> aname = Aname }) ). +prop_tauth_unix() -> + ?FORALL( + {Afid, Uname, Aname, UID}, + {fid(), bin_str(), bin_str(), int(4)}, + enc_dec( + #tauth{ + afid = Afid, + uname = Uname, + aname = Aname, + uid = UID + }, + unix + ) + ). + prop_rauth() -> ?FORALL( {AQID}, @@ -75,9 +90,13 @@ prop_rerror() -> ?FORALL( {Msg}, {bin_str()}, - begin - enc_dec(#rerror{msg = Msg}) - end + enc_dec(#rerror{msg = Msg}) + ). +prop_rerror_unix() -> + ?FORALL( + {Msg, Errno}, + {bin_str(), int(4)}, + enc_dec(#rerror{msg = Msg, errno = Errno}, unix) ). prop_tattach() -> @@ -91,6 +110,22 @@ prop_tattach() -> aname = Aname }) ). +prop_tattach_unix() -> + ?FORALL( + {FID, AFID, Uname, Aname, UID}, + {fid(), fid(), bin_str(), bin_str(), int(4)}, + enc_dec( + #tattach{ + fid = FID, + afid = AFID, + uname = Uname, + aname = Aname, + uid = UID + }, + unix + ) + ). + prop_rattach() -> ?FORALL( {QID}, @@ -135,6 +170,22 @@ prop_tcreate() -> mode = Mode }) ). +prop_tcreate_unix() -> + ?FORALL( + {FID, Name, Perm, Mode, Ext}, + {fid(), bin_str(), int(4), int(1), bin_str()}, + enc_dec( + #tcreate{ + fid = FID, + name = Name, + perm = Perm, + mode = Mode, + ext = Ext + }, + unix + ) + ). + prop_rcreate() -> ?FORALL( {QID, IOUnit}, @@ -226,8 +277,10 @@ prop_rstat() -> prop_tflush() -> ?FORALL({Tag}, {int(2)}, enc_dec(#tflush{tag = Tag})). -enc_dec(Data) -> +enc_dec(Data) -> enc_dec(Data, legacy). + +enc_dec(Data, Ver) -> Tag = 1, - Out = e9p_msg:encode(Tag, Data), + Out = e9p_msg:encode(Tag, Data, Ver), Encoded = iolist_to_binary(Out), {ok, Tag, Data} =:= e9p_msg:parse(Encoded). -- 2.51.2