X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/958108952eec4cef92bcd26cd0c845aaed5a4982..5954ccad9c87063231f3a8bb3e5d01675a9865ca:/hotline/transaction.go diff --git a/hotline/transaction.go b/hotline/transaction.go index 9b0ac40..39dcd81 100644 --- a/hotline/transaction.go +++ b/hotline/transaction.go @@ -1,78 +1,78 @@ package hotline import ( + "bufio" "bytes" "encoding/binary" "errors" "fmt" - "github.com/jhalter/mobius/concat" + "io" "math/rand" + "slices" ) const ( - tranError = 0 - tranGetMsgs = 101 - tranNewMsg = 102 - tranOldPostNews = 103 - tranServerMsg = 104 - tranChatSend = 105 - tranChatMsg = 106 - tranLogin = 107 - tranSendInstantMsg = 108 - tranShowAgreement = 109 - tranDisconnectUser = 110 - // tranDisconnectMsg = 111 TODO: implement friendly disconnect - tranInviteNewChat = 112 - tranInviteToChat = 113 - tranRejectChatInvite = 114 - tranJoinChat = 115 - tranLeaveChat = 116 - tranNotifyChatChangeUser = 117 - tranNotifyChatDeleteUser = 118 - tranNotifyChatSubject = 119 - tranSetChatSubject = 120 - tranAgreed = 121 - tranServerBanner = 122 - tranGetFileNameList = 200 - tranDownloadFile = 202 - tranUploadFile = 203 - tranNewFolder = 205 - tranDeleteFile = 204 - tranGetFileInfo = 206 - tranSetFileInfo = 207 - tranMoveFile = 208 - tranMakeFileAlias = 209 - tranDownloadFldr = 210 - // tranDownloadInfo = 211 TODO: implement file transfer queue - tranDownloadBanner = 212 - tranUploadFldr = 213 - tranGetUserNameList = 300 - tranNotifyChangeUser = 301 - tranNotifyDeleteUser = 302 - tranGetClientInfoText = 303 - tranSetClientUserInfo = 304 - tranListUsers = 348 - tranUpdateUser = 349 - tranNewUser = 350 - tranDeleteUser = 351 - tranGetUser = 352 - tranSetUser = 353 - tranUserAccess = 354 - tranUserBroadcast = 355 - tranGetNewsCatNameList = 370 - tranGetNewsArtNameList = 371 - tranDelNewsItem = 380 - tranNewNewsFldr = 381 - tranNewNewsCat = 382 - tranGetNewsArtData = 400 - tranPostNewsArt = 410 - tranDelNewsArt = 411 - tranKeepAlive = 500 + TranError = 0 + TranGetMsgs = 101 + TranNewMsg = 102 + TranOldPostNews = 103 + TranServerMsg = 104 + TranChatSend = 105 + TranChatMsg = 106 + TranLogin = 107 + TranSendInstantMsg = 108 + TranShowAgreement = 109 + TranDisconnectUser = 110 + TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect + TranInviteNewChat = 112 + TranInviteToChat = 113 + TranRejectChatInvite = 114 + TranJoinChat = 115 + TranLeaveChat = 116 + TranNotifyChatChangeUser = 117 + TranNotifyChatDeleteUser = 118 + TranNotifyChatSubject = 119 + TranSetChatSubject = 120 + TranAgreed = 121 + TranServerBanner = 122 + TranGetFileNameList = 200 + TranDownloadFile = 202 + TranUploadFile = 203 + TranNewFolder = 205 + TranDeleteFile = 204 + TranGetFileInfo = 206 + TranSetFileInfo = 207 + TranMoveFile = 208 + TranMakeFileAlias = 209 + TranDownloadFldr = 210 + TranDownloadInfo = 211 // TODO: implement file transfer queue + TranDownloadBanner = 212 + TranUploadFldr = 213 + TranGetUserNameList = 300 + TranNotifyChangeUser = 301 + TranNotifyDeleteUser = 302 + TranGetClientInfoText = 303 + TranSetClientUserInfo = 304 + TranListUsers = 348 + TranUpdateUser = 349 + TranNewUser = 350 + TranDeleteUser = 351 + TranGetUser = 352 + TranSetUser = 353 + TranUserAccess = 354 + TranUserBroadcast = 355 + TranGetNewsCatNameList = 370 + TranGetNewsArtNameList = 371 + TranDelNewsItem = 380 + TranNewNewsFldr = 381 + TranNewNewsCat = 382 + TranGetNewsArtData = 400 + TranPostNewsArt = 410 + TranDelNewsArt = 411 + TranKeepAlive = 500 ) type Transaction struct { - clientID *[]byte - Flags byte // Reserved (should be 0) IsReply byte // Request (0) or reply (1) Type []byte // Requested operation (user defined) @@ -82,6 +82,9 @@ type Transaction struct { DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts. ParamCount []byte // Number of the parameters for this transaction Fields []Field + + clientID *[]byte // Internal identifier for target client + readOffset int // Internal offset to track read progress } func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction { @@ -102,34 +105,42 @@ func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction { } } -// ReadTransaction parses a byte slice into a struct. The input slice may be shorter or longer -// that the transaction size depending on what was read from the network connection. -func ReadTransaction(buf []byte) (*Transaction, int, error) { - totalSize := binary.BigEndian.Uint32(buf[12:16]) +// Write implements io.Writer interface for Transaction +func (t *Transaction) Write(p []byte) (n int, err error) { + totalSize := binary.BigEndian.Uint32(p[12:16]) // the buf may include extra bytes that are not part of the transaction // tranLen represents the length of bytes that are part of the transaction tranLen := int(20 + totalSize) - if tranLen > len(buf) { - return nil, 0, errors.New("buflen too small for tranLen") + if tranLen > len(p) { + return n, errors.New("buflen too small for tranLen") } - fields, err := ReadFields(buf[20:22], buf[22:tranLen]) - if err != nil { - return nil, 0, err + + // Create a new scanner for parsing incoming bytes into transaction tokens + scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen])) + scanner.Split(fieldScanner) + + for i := 0; i < int(binary.BigEndian.Uint16(p[20:22])); i++ { + scanner.Scan() + + var field Field + if _, err := field.Write(scanner.Bytes()); err != nil { + return 0, fmt.Errorf("error reading field: %w", err) + } + t.Fields = append(t.Fields, field) } - return &Transaction{ - Flags: buf[0], - IsReply: buf[1], - Type: buf[2:4], - ID: buf[4:8], - ErrorCode: buf[8:12], - TotalSize: buf[12:16], - DataSize: buf[16:20], - ParamCount: buf[20:22], - Fields: fields, - }, tranLen, nil + t.Flags = p[0] + t.IsReply = p[1] + t.Type = p[2:4] + t.ID = p[4:8] + t.ErrorCode = p[8:12] + t.TotalSize = p[12:16] + t.DataSize = p[16:20] + t.ParamCount = p[20:22] + + return len(p), err } const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields @@ -178,8 +189,8 @@ func ReadFields(paramCount []byte, buf []byte) ([]Field, error) { } fields = append(fields, Field{ - ID: fieldID, - FieldSize: fieldSize, + ID: [2]byte(fieldID), + FieldSize: [2]byte(fieldSize), Data: buf[4 : 4+fieldSizeInt], }) @@ -193,18 +204,24 @@ func ReadFields(paramCount []byte, buf []byte) ([]Field, error) { return fields, nil } -func (t *Transaction) MarshalBinary() (data []byte, err error) { +// Read implements the io.Reader interface for Transaction +func (t *Transaction) Read(p []byte) (int, error) { payloadSize := t.Size() fieldCount := make([]byte, 2) binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields))) - var fieldPayload []byte + bbuf := new(bytes.Buffer) + for _, field := range t.Fields { - fieldPayload = append(fieldPayload, field.Payload()...) + f := field + _, err := bbuf.ReadFrom(&f) + if err != nil { + return 0, fmt.Errorf("error reading field: %w", err) + } } - return concat.Slices( + buf := slices.Concat( []byte{t.Flags, t.IsReply}, t.Type, t.ID, @@ -212,8 +229,17 @@ func (t *Transaction) MarshalBinary() (data []byte, err error) { payloadSize, payloadSize, // this is the dataSize field, but seeming the same as totalSize fieldCount, - fieldPayload, - ), err + bbuf.Bytes(), + ) + + if t.readOffset >= len(buf) { + return 0, io.EOF // All bytes have been read + } + + n := copy(p, buf[t.readOffset:]) + t.readOffset += n + + return n, nil } // Size returns the total size of the transaction payload @@ -232,7 +258,7 @@ func (t *Transaction) Size() []byte { func (t *Transaction) GetField(id int) Field { for _, field := range t.Fields { - if id == int(binary.BigEndian.Uint16(field.ID)) { + if id == int(binary.BigEndian.Uint16(field.ID[:])) { return field } } @@ -241,5 +267,5 @@ func (t *Transaction) GetField(id int) Field { } func (t *Transaction) IsError() bool { - return bytes.Compare(t.ErrorCode, []byte{0, 0, 0, 1}) == 0 + return bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 1}) }