]> git.r.bdr.sh - rbdr/mobius/blame - hotline/field.go
Update README.md
[rbdr/mobius] / hotline / field.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
d9bc63a1
JH
4 "bufio"
5 "bytes"
6988a057 6 "encoding/binary"
a2ef262a 7 "errors"
95159e55 8 "io"
9c44621e 9 "slices"
6988a057
JH
10)
11
d005ef04 12// List of Hotline protocol field types taken from the official 1.9 protocol document
a2ef262a
JH
13var (
14 FieldError = [2]byte{0x00, 0x64} // 100
15 FieldData = [2]byte{0x00, 0x65} // 101
16 FieldUserName = [2]byte{0x00, 0x66} // 102
17 FieldUserID = [2]byte{0x00, 0x67} // 103
18 FieldUserIconID = [2]byte{0x00, 0x68} // 104
19 FieldUserLogin = [2]byte{0x00, 0x69} // 105
20 FieldUserPassword = [2]byte{0x00, 0x6A} // 106
21 FieldRefNum = [2]byte{0x00, 0x6B} // 107
22 FieldTransferSize = [2]byte{0x00, 0x6C} // 108
23 FieldChatOptions = [2]byte{0x00, 0x6D} // 109
24 FieldUserAccess = [2]byte{0x00, 0x6E} // 110
25 FieldUserFlags = [2]byte{0x00, 0x70} // 112
26 FieldOptions = [2]byte{0x00, 0x71} // 113
27 FieldChatID = [2]byte{0x00, 0x72} // 114
28 FieldChatSubject = [2]byte{0x00, 0x73} // 115
29 FieldWaitingCount = [2]byte{0x00, 0x74} // 116
30 FieldBannerType = [2]byte{0x00, 0x98} // 152
31 FieldNoServerAgreement = [2]byte{0x00, 0x98} // 152
32 FieldVersion = [2]byte{0x00, 0xA0} // 160
33 FieldCommunityBannerID = [2]byte{0x00, 0xA1} // 161
34 FieldServerName = [2]byte{0x00, 0xA2} // 162
35 FieldFileNameWithInfo = [2]byte{0x00, 0xC8} // 200
36 FieldFileName = [2]byte{0x00, 0xC9} // 201
37 FieldFilePath = [2]byte{0x00, 0xCA} // 202
38 FieldFileResumeData = [2]byte{0x00, 0xCB} // 203
39 FieldFileTransferOptions = [2]byte{0x00, 0xCC} // 204
40 FieldFileTypeString = [2]byte{0x00, 0xCD} // 205
41 FieldFileCreatorString = [2]byte{0x00, 0xCE} // 206
42 FieldFileSize = [2]byte{0x00, 0xCF} // 207
43 FieldFileCreateDate = [2]byte{0x00, 0xD0} // 208
44 FieldFileModifyDate = [2]byte{0x00, 0xD1} // 209
45 FieldFileComment = [2]byte{0x00, 0xD2} // 210
46 FieldFileNewName = [2]byte{0x00, 0xD3} // 211
47 FieldFileNewPath = [2]byte{0x00, 0xD4} // 212
48 FieldFileType = [2]byte{0x00, 0xD5} // 213
49 FieldQuotingMsg = [2]byte{0x00, 0xD6} // 214
50 FieldAutomaticResponse = [2]byte{0x00, 0xD7} // 215
51 FieldFolderItemCount = [2]byte{0x00, 0xDC} // 220
52 FieldUsernameWithInfo = [2]byte{0x01, 0x2C} // 300
53 FieldNewsArtListData = [2]byte{0x01, 0x41} // 321
54 FieldNewsCatName = [2]byte{0x01, 0x42} // 322
55 FieldNewsCatListData15 = [2]byte{0x01, 0x43} // 323
56 FieldNewsPath = [2]byte{0x01, 0x45} // 325
57 FieldNewsArtID = [2]byte{0x01, 0x46} // 326
58 FieldNewsArtDataFlav = [2]byte{0x01, 0x47} // 327
59 FieldNewsArtTitle = [2]byte{0x01, 0x48} // 328
60 FieldNewsArtPoster = [2]byte{0x01, 0x49} // 329
61 FieldNewsArtDate = [2]byte{0x01, 0x4A} // 330
62 FieldNewsArtPrevArt = [2]byte{0x01, 0x4B} // 331
63 FieldNewsArtNextArt = [2]byte{0x01, 0x4C} // 332
64 FieldNewsArtData = [2]byte{0x01, 0x4D} // 333
65 FieldNewsArtParentArt = [2]byte{0x01, 0x4F} // 335
66 FieldNewsArt1stChildArt = [2]byte{0x01, 0x50} // 336
d9bc63a1 67 FieldNewsArtRecurseDel = [2]byte{0x01, 0x51} // 337
a2ef262a
JH
68
69 // These fields are documented, but seemingly unused.
70 // FieldUserAlias = [2]byte{0x00, 0x6F} // 111
71 // FieldNewsArtFlags = [2]byte{0x01, 0x4E} // 334
d005ef04 72)
6988a057
JH
73
74type Field struct {
d9bc63a1
JH
75 Type [2]byte // Type of field
76 FieldSize [2]byte // Size of the data field
77 Data []byte // Field data
95159e55
JH
78
79 readOffset int // Internal offset to track read progress
6988a057
JH
80}
81
d9bc63a1 82func NewField(fieldType [2]byte, data []byte) Field {
a2ef262a 83 f := Field{
d9bc63a1 84 Type: fieldType,
a2ef262a
JH
85 Data: make([]byte, len(data)),
86 }
6988a057 87
a2ef262a
JH
88 // Copy instead of assigning to avoid data race when the field is read in another go routine.
89 copy(f.Data, data)
6988a057 90
a2ef262a 91 binary.BigEndian.PutUint16(f.FieldSize[:], uint16(len(data)))
a55350da 92 return f
6988a057
JH
93}
94
fd740bc4
JH
95// FieldScanner implements bufio.SplitFunc for parsing byte slices into complete tokens
96func FieldScanner(data []byte, _ bool) (advance int, token []byte, err error) {
95159e55
JH
97 if len(data) < minFieldLen {
98 return 0, nil, nil
99 }
100
a2ef262a 101 // neededSize represents the length of bytes that are part of the field token.
95159e55
JH
102 neededSize := minFieldLen + int(binary.BigEndian.Uint16(data[2:4]))
103 if neededSize > len(data) {
104 return 0, nil, nil
105 }
106
107 return neededSize, data[0:neededSize], nil
108}
109
d9bc63a1
JH
110// DecodeInt decodes the field bytes to an int.
111// The official Hotline clients will send uint32s as 2 bytes if possible, but
112// some third party clients such as Frogblast and Heildrun will always send 4 bytes
113func (f *Field) DecodeInt() (int, error) {
114 switch len(f.Data) {
115 case 2:
116 return int(binary.BigEndian.Uint16(f.Data)), nil
117 case 4:
118 return int(binary.BigEndian.Uint32(f.Data)), nil
119 }
120
121 return 0, errors.New("unknown byte length")
122}
123
124func (f *Field) DecodeObfuscatedString() string {
fd740bc4 125 return string(EncodeString(f.Data))
d9bc63a1
JH
126}
127
128// DecodeNewsPath decodes the field data to a news path.
129// Example News Path data for a Category nested under two Bundles:
130// 00000000 00 03 00 00 10 54 6f 70 20 4c 65 76 65 6c 20 42 |.....Top Level B|
131// 00000010 75 6e 64 6c 65 00 00 13 53 65 63 6f 6e 64 20 4c |undle...Second L|
132// 00000020 65 76 65 6c 20 42 75 6e 64 6c 65 00 00 0f 4e 65 |evel Bundle...Ne|
133// 00000030 73 74 65 64 20 43 61 74 65 67 6f 72 79 |sted Category|
134func (f *Field) DecodeNewsPath() ([]string, error) {
135 if len(f.Data) == 0 {
136 return []string{}, nil
137 }
138
139 pathCount := binary.BigEndian.Uint16(f.Data[0:2])
140
141 scanner := bufio.NewScanner(bytes.NewReader(f.Data[2:]))
142 scanner.Split(newsPathScanner)
143
144 var paths []string
145
146 for i := uint16(0); i < pathCount; i++ {
147 scanner.Scan()
148 paths = append(paths, scanner.Text())
149 }
150
151 return paths, nil
152}
153
95159e55
JH
154// Read implements io.Reader for Field
155func (f *Field) Read(p []byte) (int, error) {
d9bc63a1 156 buf := slices.Concat(f.Type[:], f.FieldSize[:], f.Data)
95159e55
JH
157
158 if f.readOffset >= len(buf) {
159 return 0, io.EOF // All bytes have been read
160 }
161
162 n := copy(p, buf[f.readOffset:])
163 f.readOffset += n
164
165 return n, nil
166}
167
168// Write implements io.Writer for Field
169func (f *Field) Write(p []byte) (int, error) {
a2ef262a
JH
170 if len(p) < minFieldLen {
171 return 0, errors.New("input slice too short")
172 }
173
d9bc63a1 174 copy(f.Type[:], p[0:2])
a2ef262a
JH
175 copy(f.FieldSize[:], p[2:4])
176
177 dataSize := int(binary.BigEndian.Uint16(f.FieldSize[:]))
178 if len(p) < minFieldLen+dataSize {
179 return 0, errors.New("input slice too short for data size")
180 }
95159e55 181
a2ef262a
JH
182 f.Data = make([]byte, dataSize)
183 copy(f.Data, p[4:4+dataSize])
95159e55 184
a2ef262a 185 return minFieldLen + dataSize, nil
6988a057 186}
d2810ae9 187
fd740bc4 188func GetField(id [2]byte, fields *[]Field) *Field {
d2810ae9 189 for _, field := range *fields {
d9bc63a1 190 if id == field.Type {
d2810ae9
JH
191 return &field
192 }
193 }
194 return nil
195}