1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
mod id;
pub mod utils;
pub use id::ID;
use std::convert::TryInto;
/// Hotline defines 3 data types in the wire format:
/// 1) "integer" (16-bit or 32-bit, determined by magnitude)
/// 2) "string" (ASCII)
/// 3) "binary"
///
/// Internally, we can unify them into:
/// - Integer(u32)
/// - String(String)
/// - Binary(Vec<u8>)
#[derive(Debug, Clone)]
pub enum Value {
Integer(u32),
Text(String),
Binary(Vec<u8>),
}
/// A single Hotline field: an ID plus a typed value.
#[derive(Debug, Clone)]
pub struct Field {
pub id: ID,
pub value: Value,
}
/// Parse the field data into the correct `Value` type based on the `field_id`.
/// The spec says:
/// - Some field IDs are definitely “integer” (e.g. user ID).
/// - Some are definitely “string” (e.g. user name).
/// - Others are “binary” (which might contain sub-structures).
pub fn parse_field_value(id: ID, data: &[u8]) -> Value {
use Value::*;
use ID::*;
// By default, assume "binary"
// Then override to integer or string if the spec clearly says so.
match id {
// Integers
UserID
| UserIconID
| ReferenceNumber
| TransferSize
| ChatOptions
| UserFlags
| Options
| ChatID
| WaitingCount
| ServerBannerType
| NoServerAgreement
| Version
| CommunityBannerID
| FileSize
| NewsArticleID
| NewsArticlePrevArticle
| NewsArticleNextArticle
| NewsArticleFlags
| NewsArticleParentArticle
| NewsArticle1stChildArticle
| NewsArticleRecurseDel
| FolderItemCount => {
// Convert the data to an integer (u32).
// Hotline protocol can store 16-bit or 32-bit depending on magnitude,
// but on the wire we need to detect it. For simplicity here,
// we'll parse up to 4 bytes. If data is 2 bytes, parse as 16-bit.
// If 4 bytes, parse as 32-bit. If it's not 2 or 4, we do best-effort.
let len = data.len();
if len == 2 {
Integer(u16::from_be_bytes(data.try_into().unwrap()) as u32)
} else if len == 4 {
Integer(u32::from_be_bytes(data.try_into().unwrap()))
} else if len == 0 {
// no data => 0
Integer(0)
} else {
// fallback
// interpret first 4 bytes, or everything as 32-bit
let mut buf = [0u8; 4];
for (i, b) in data.iter().enumerate().take(4) {
buf[i] = *b;
}
Integer(u32::from_be_bytes(buf))
}
}
// Strings
UserName
| UserLogin
| UserPassword
| ChatSubject
| ServerAgreement
| FileName
| FileTypeString
| FileCreatorString
| FileComment
| FileNewName
| AutomaticResponse
| ServerName
| NewsCategoryName
| NewsArticleDataFlavor
| NewsArticleTitle
| NewsArticlePoster => {
let s = String::from_utf8_lossy(data).to_string();
Text(s)
}
// Some are definitely “binary,” but might happen to contain text:
ErrorText
| Data
| ServerBanner
| ServerBannerUrl
| FileNameWithInfo
| FilePath
| FileResumeData
| FileXferOptions
| FileCreateDate
| FileModifyDate
| FileNewPath
| FileType
| QuotingMessage
| UserAccess
| UserAlias
| UserNameWithInfo
| NewsCategoryGUID
| LegacyNewsCategoryListData
| NewsArticleListData
| NewsCategoryListData
| NewsPath
| NewsArticleDate
| NewsArticleData
| Unknown(_) => Binary(data.to_vec()),
}
}
|