mod id; pub mod utils; pub use id::ID; use std::convert::TryInto; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("Data could not be parsed as an integer.")] InvalidInteger, } /// Hotline defines 3 data types in the wire format: /// 1) "integer" (16-bit or 32-bit, determined by magnitude) /// 2) "string" (ASCII) /// 3) "binary" #[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 `id`. /// /// # Errors /// If an Int can't be parsed, it'll return an `InvalidInteger` error. pub fn parse_field_value(id: ID, data: &[u8]) -> Result { use Value::{Binary, Integer, Text}; use ID::{ AutomaticResponse, ChatID, ChatOptions, ChatSubject, CommunityBannerID, Data, ErrorText, FileComment, FileCreateDate, FileCreatorString, FileModifyDate, FileName, FileNameWithInfo, FileNewName, FileNewPath, FilePath, FileResumeData, FileSize, FileType, FileTypeString, FileXferOptions, FolderItemCount, LegacyNewsCategoryListData, NewsArticle1stChildArticle, NewsArticleData, NewsArticleDataFlavor, NewsArticleDate, NewsArticleFlags, NewsArticleID, NewsArticleListData, NewsArticleNextArticle, NewsArticleParentArticle, NewsArticlePoster, NewsArticlePrevArticle, NewsArticleRecurseDel, NewsArticleTitle, NewsCategoryGUID, NewsCategoryListData, NewsCategoryName, NewsPath, NoServerAgreement, Options, QuotingMessage, ReferenceNumber, ServerAgreement, ServerBanner, ServerBannerType, ServerBannerUrl, ServerName, TransferSize, Unknown, UserAccess, UserAlias, UserFlags, UserID, UserIconID, UserLogin, UserName, UserNameWithInfo, UserPassword, Version, WaitingCount, }; 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 { data.try_into() .map(u16::from_be_bytes) .map(u32::from) .map(Integer) .map_err(|_| Error::InvalidInteger) } else if len == 4 { data.try_into() .map(u32::from_be_bytes) .map(Integer) .map_err(|_| Error::InvalidInteger) } else { Err(Error::InvalidInteger) } } // 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(); Ok(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(_) => Ok(Binary(data.to_vec())), } }