]>
Commit | Line | Data |
---|---|---|
6988a057 JH |
1 | package hotline |
2 | ||
3 | import ( | |
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 |
13 | var ( |
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 | |
74 | type 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 | 82 | func 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 |
96 | func 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 | |
113 | func (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 | ||
124 | func (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| | |
134 | func (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 |
155 | func (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 | |
169 | func (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 | 188 | func 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 | } |