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) #[derive(Debug, Clone)] pub enum Value { Integer(u32), Text(String), Binary(Vec), } /// 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()), } }