]> git.r.bdr.sh - rbdr/mobius/blame - hotline/transaction.go
Fix corrupt file info forks
[rbdr/mobius] / hotline / transaction.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
95159e55 4 "bufio"
9174dbe8 5 "bytes"
6988a057
JH
6 "encoding/binary"
7 "errors"
8 "fmt"
95159e55 9 "io"
6988a057 10 "math/rand"
9c44621e 11 "slices"
6988a057
JH
12)
13
14const (
d005ef04
JH
15 TranError = 0
16 TranGetMsgs = 101
17 TranNewMsg = 102
18 TranOldPostNews = 103
19 TranServerMsg = 104
20 TranChatSend = 105
21 TranChatMsg = 106
22 TranLogin = 107
23 TranSendInstantMsg = 108
24 TranShowAgreement = 109
25 TranDisconnectUser = 110
26 TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect
27 TranInviteNewChat = 112
28 TranInviteToChat = 113
29 TranRejectChatInvite = 114
30 TranJoinChat = 115
31 TranLeaveChat = 116
32 TranNotifyChatChangeUser = 117
33 TranNotifyChatDeleteUser = 118
34 TranNotifyChatSubject = 119
35 TranSetChatSubject = 120
36 TranAgreed = 121
37 TranServerBanner = 122
38 TranGetFileNameList = 200
39 TranDownloadFile = 202
40 TranUploadFile = 203
41 TranNewFolder = 205
42 TranDeleteFile = 204
43 TranGetFileInfo = 206
44 TranSetFileInfo = 207
45 TranMoveFile = 208
46 TranMakeFileAlias = 209
47 TranDownloadFldr = 210
48 TranDownloadInfo = 211 // TODO: implement file transfer queue
49 TranDownloadBanner = 212
50 TranUploadFldr = 213
51 TranGetUserNameList = 300
52 TranNotifyChangeUser = 301
53 TranNotifyDeleteUser = 302
54 TranGetClientInfoText = 303
55 TranSetClientUserInfo = 304
56 TranListUsers = 348
57 TranUpdateUser = 349
58 TranNewUser = 350
59 TranDeleteUser = 351
60 TranGetUser = 352
61 TranSetUser = 353
62 TranUserAccess = 354
63 TranUserBroadcast = 355
64 TranGetNewsCatNameList = 370
65 TranGetNewsArtNameList = 371
66 TranDelNewsItem = 380
67 TranNewNewsFldr = 381
68 TranNewNewsCat = 382
69 TranGetNewsArtData = 400
70 TranPostNewsArt = 410
71 TranDelNewsArt = 411
72 TranKeepAlive = 500
6988a057
JH
73)
74
75type Transaction struct {
6988a057
JH
76 Flags byte // Reserved (should be 0)
77 IsReply byte // Request (0) or reply (1)
78 Type []byte // Requested operation (user defined)
79 ID []byte // Unique transaction ID (must be != 0)
80 ErrorCode []byte // Used in the reply (user defined, 0 = no error)
81 TotalSize []byte // Total data size for the transaction (all parts)
82 DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
83 ParamCount []byte // Number of the parameters for this transaction
84 Fields []Field
95159e55
JH
85
86 clientID *[]byte // Internal identifier for target client
87 readOffset int // Internal offset to track read progress
6988a057
JH
88}
89
90func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction {
91 typeSlice := make([]byte, 2)
92 binary.BigEndian.PutUint16(typeSlice, uint16(t))
93
94 idSlice := make([]byte, 4)
95 binary.BigEndian.PutUint32(idSlice, rand.Uint32())
96
97 return &Transaction{
98 clientID: clientID,
99 Flags: 0x00,
100 IsReply: 0x00,
101 Type: typeSlice,
102 ID: idSlice,
103 ErrorCode: []byte{0, 0, 0, 0},
104 Fields: fields,
105 }
106}
107
854a92fc
JH
108// Write implements io.Writer interface for Transaction
109func (t *Transaction) Write(p []byte) (n int, err error) {
110 totalSize := binary.BigEndian.Uint32(p[12:16])
6988a057
JH
111
112 // the buf may include extra bytes that are not part of the transaction
113 // tranLen represents the length of bytes that are part of the transaction
114 tranLen := int(20 + totalSize)
115
854a92fc
JH
116 if tranLen > len(p) {
117 return n, errors.New("buflen too small for tranLen")
6988a057 118 }
95159e55
JH
119
120 // Create a new scanner for parsing incoming bytes into transaction tokens
121 scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
122 scanner.Split(fieldScanner)
123
124 for i := 0; i < int(binary.BigEndian.Uint16(p[20:22])); i++ {
125 scanner.Scan()
126
127 var field Field
128 if _, err := field.Write(scanner.Bytes()); err != nil {
129 return 0, fmt.Errorf("error reading field: %w", err)
130 }
131 t.Fields = append(t.Fields, field)
6988a057
JH
132 }
133
854a92fc
JH
134 t.Flags = p[0]
135 t.IsReply = p[1]
136 t.Type = p[2:4]
137 t.ID = p[4:8]
138 t.ErrorCode = p[8:12]
139 t.TotalSize = p[12:16]
140 t.DataSize = p[16:20]
141 t.ParamCount = p[20:22]
854a92fc
JH
142
143 return len(p), err
6988a057
JH
144}
145
3178ae58 146const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
6988a057 147
3178ae58
JH
148// transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
149func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
150 // The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
151 if len(data) < 16 {
152 return 0, nil, nil
153 }
6988a057 154
3178ae58 155 totalSize := binary.BigEndian.Uint32(data[12:16])
6988a057 156
3178ae58
JH
157 // tranLen represents the length of bytes that are part of the transaction
158 tranLen := int(tranHeaderLen + totalSize)
159 if tranLen > len(data) {
160 return 0, nil, nil
6988a057
JH
161 }
162
3178ae58 163 return tranLen, data[0:tranLen], nil
6988a057
JH
164}
165
166const minFieldLen = 4
167
168func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
169 paramCountInt := int(binary.BigEndian.Uint16(paramCount))
170 if paramCountInt > 0 && len(buf) < minFieldLen {
171 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
172 }
173
174 // A Field consists of:
175 // ID: 2 bytes
176 // Size: 2 bytes
177 // Data: FieldSize number of bytes
178 var fields []Field
179 for i := 0; i < paramCountInt; i++ {
180 if len(buf) < minFieldLen {
181 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
182 }
183 fieldID := buf[0:2]
184 fieldSize := buf[2:4]
185 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
186 expectedLen := minFieldLen + fieldSizeInt
187 if len(buf) < expectedLen {
188 return []Field{}, fmt.Errorf("field length too short")
189 }
190
191 fields = append(fields, Field{
95159e55
JH
192 ID: [2]byte(fieldID),
193 FieldSize: [2]byte(fieldSize),
6988a057
JH
194 Data: buf[4 : 4+fieldSizeInt],
195 })
196
197 buf = buf[fieldSizeInt+4:]
198 }
199
200 if len(buf) != 0 {
201 return []Field{}, fmt.Errorf("extra field bytes")
202 }
203
204 return fields, nil
205}
206
95159e55
JH
207// Read implements the io.Reader interface for Transaction
208func (t *Transaction) Read(p []byte) (int, error) {
6988a057
JH
209 payloadSize := t.Size()
210
211 fieldCount := make([]byte, 2)
212 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
213
95159e55
JH
214 bbuf := new(bytes.Buffer)
215
6988a057 216 for _, field := range t.Fields {
0ed51327
JH
217 f := field
218 _, err := bbuf.ReadFrom(&f)
95159e55
JH
219 if err != nil {
220 return 0, fmt.Errorf("error reading field: %w", err)
221 }
6988a057
JH
222 }
223
95159e55 224 buf := slices.Concat(
6988a057
JH
225 []byte{t.Flags, t.IsReply},
226 t.Type,
227 t.ID,
228 t.ErrorCode,
229 payloadSize,
230 payloadSize, // this is the dataSize field, but seeming the same as totalSize
231 fieldCount,
95159e55
JH
232 bbuf.Bytes(),
233 )
234
235 if t.readOffset >= len(buf) {
236 return 0, io.EOF // All bytes have been read
237 }
238
239 n := copy(p, buf[t.readOffset:])
240 t.readOffset += n
241
242 return n, nil
6988a057
JH
243}
244
245// Size returns the total size of the transaction payload
0a92e50b 246func (t *Transaction) Size() []byte {
6988a057
JH
247 bs := make([]byte, 4)
248
249 fieldSize := 0
250 for _, field := range t.Fields {
251 fieldSize += len(field.Data) + 4
252 }
253
254 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
255
256 return bs
257}
258
0a92e50b 259func (t *Transaction) GetField(id int) Field {
6988a057 260 for _, field := range t.Fields {
95159e55 261 if id == int(binary.BigEndian.Uint16(field.ID[:])) {
6988a057
JH
262 return field
263 }
264 }
265
266 return Field{}
267}
9174dbe8
JH
268
269func (t *Transaction) IsError() bool {
043c00da 270 return bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 1})
9174dbe8 271}