diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-02-09 22:44:21 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-02-09 22:44:21 +0100 |
| commit | c402a8923d2e89607a007f46ee4cde15714d746d (patch) | |
| tree | 597f0e436cac0ec2dda2eb8437cfb57de6c95a82 /src | |
| parent | 48408b9e42eaeeaf0944f9a37f7882ba9ddc7f19 (diff) | |
Restart with fields module
Diffstat (limited to 'src')
| -rw-r--r-- | src/field/data.rs | 24 | ||||
| -rw-r--r-- | src/field/id.rs | 149 | ||||
| -rw-r--r-- | src/field/mod.rs | 145 | ||||
| -rw-r--r-- | src/field/utils/access_privileges.rs | 47 | ||||
| -rw-r--r-- | src/field/utils/banner_type.rs | 9 | ||||
| -rw-r--r-- | src/field/utils/mod.rs | 6 | ||||
| -rw-r--r-- | src/field/utils/user_flags.rs | 22 | ||||
| -rw-r--r-- | src/lib.rs | 4 | ||||
| -rw-r--r-- | src/transaction/mod.rs | 19 |
9 files changed, 365 insertions, 60 deletions
diff --git a/src/field/data.rs b/src/field/data.rs deleted file mode 100644 index cd1341c..0000000 --- a/src/field/data.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::Field; -use std::io::Result; - -pub struct Data { - value: Vec<u8>, -} - -impl Field for Data { - const ID: u16 = 101; - - fn size(&self) -> [u8; 2] { - (self.value.len() as u16).to_be_bytes() - } - - fn serialize_data(&self) -> &Vec<u8> { - &self.value - } - - fn deserialize(data: &[u8]) -> Result<Self> { - Ok(Data { - value: data.to_owned(), - }) - } -} diff --git a/src/field/id.rs b/src/field/id.rs new file mode 100644 index 0000000..07e02b2 --- /dev/null +++ b/src/field/id.rs @@ -0,0 +1,149 @@ +#[repr(u16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ID { + ErrorText = 100, + Data = 101, + UserName = 102, + UserID = 103, + UserIconID = 104, + UserLogin = 105, + UserPassword = 106, + ReferenceNumber = 107, + TransferSize = 108, + ChatOptions = 109, + UserAccess = 110, + UserAlias = 111, + UserFlags = 112, + Options = 113, + ChatID = 114, + ChatSubject = 115, + WaitingCount = 116, + + ServerAgreement = 150, + ServerBanner = 151, + ServerBannerType = 152, + ServerBannerUrl = 153, + NoServerAgreement = 154, + + Version = 160, + CommunityBannerID = 161, + ServerName = 162, + + // 200..215 + FileNameWithInfo = 200, // TODO: Parse into a struct! (FileInfo) + FileName = 201, + FilePath = 202, + FileResumeData = 203, // TODO: Parse into a struct! (ResumeData) + FileXferOptions = 204, + FileTypeString = 205, + FileCreatorString = 206, + FileSize = 207, + FileCreateDate = 208, // TODO: Parse into a struct! (Date) + FileModifyDate = 209, // TODO: Parse into a struct! (Date) + FileComment = 210, + FileNewName = 211, + FileNewPath = 212, + FileType = 213, + QuotingMessage = 214, // TODO: This is marked as binary, but is basically a string. + AutomaticResponse = 215, + + FolderItemCount = 220, + + UserNameWithInfo = 300, // TODO: Parse into a struct! (UserInfo) + + NewsCategoryGUID = 319, + LegacyNewsCategoryListData = 320, // TODO: Parse into a struct! (LegacyNewsCategory) + NewsArticleListData = 321, // TODO: Parse into a struct! (NewsArticleList) + NewsCategoryName = 322, + NewsCategoryListData = 323, // TODO: Parse into a struct (NewsCategory) + + NewsPath = 325, + NewsArticleID = 326, + NewsArticleDataFlavor = 327, + NewsArticleTitle = 328, + NewsArticlePoster = 329, + NewsArticleDate = 330, // TODO: Parse into a struct! (Date) + NewsArticlePrevArticle = 331, + NewsArticleNextArticle = 332, + NewsArticleData = 333, + NewsArticleFlags = 334, + NewsArticleParentArticle = 335, + NewsArticle1stChildArticle = 336, + NewsArticleRecurseDel = 337, + + // If we receive an unrecognized field ID, we can store it as a raw integer. + Unknown(u16), +} + +impl ID { + pub fn from_u16(id: u16) -> Self { + use ID::*; + match id { + 100 => ErrorText, + 101 => Data, + 102 => UserName, + 103 => UserID, + 104 => UserIconID, + 105 => UserLogin, + 106 => UserPassword, + 107 => ReferenceNumber, + 108 => TransferSize, + 109 => ChatOptions, + 110 => UserAccess, + 111 => UserAlias, + 112 => UserFlags, + 113 => Options, + 114 => ChatID, + 115 => ChatSubject, + 116 => WaitingCount, + + 150 => ServerAgreement, + 151 => ServerBanner, + 152 => ServerBannerType, + 153 => ServerBannerUrl, + 154 => NoServerAgreement, + 160 => Version, + 161 => CommunityBannerID, + 162 => ServerName, + + 200 => FileNameWithInfo, + 201 => FileName, + 202 => FilePath, + 203 => FileResumeData, + 204 => FileXferOptions, + 205 => FileTypeString, + 206 => FileCreatorString, + 207 => FileSize, + 208 => FileCreateDate, + 209 => FileModifyDate, + 210 => FileComment, + 211 => FileNewName, + 212 => FileNewPath, + 213 => FileType, + 214 => QuotingMessage, + 215 => AutomaticResponse, + 220 => FolderItemCount, + + 300 => UserNameWithInfo, + 319 => NewsCategoryGUID, + 320 => LegacyNewsCategoryListData, + 321 => NewsArticleListData, + 322 => NewsCategoryName, + 323 => NewsCategoryListData, + 325 => NewsPath, + 326 => NewsArticleID, + 327 => NewsArticleDataFlavor, + 328 => NewsArticleTitle, + 329 => NewsArticlePoster, + 330 => NewsArticleDate, + 331 => NewsArticlePrevArticle, + 332 => NewsArticleNextArticle, + 333 => NewsArticleData, + 334 => NewsArticleFlags, + 335 => NewsArticleParentArticle, + 336 => NewsArticle1stChildArticle, + 337 => NewsArticleRecurseDel, + other => Unknown(other), + } + } +} diff --git a/src/field/mod.rs b/src/field/mod.rs index 82e8abd..277e7c4 100644 --- a/src/field/mod.rs +++ b/src/field/mod.rs @@ -1,21 +1,138 @@ -use std::io::Result; -mod data; +mod id; +pub mod utils; -pub use data::Data; +pub use id::ID; -pub trait Field: Sized { - const ID: u16; +use std::convert::TryInto; - fn size(&self) -> [u8; 2]; - fn serialize_data(&self) -> &Vec<u8>; - fn deserialize(data: &[u8]) -> Result<Self>; - fn serialize(&self) -> Vec<u8> { - let mut result = Vec::new(); +/// 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)) + } + } - result.extend_from_slice(&Self::ID.to_be_bytes()); - result.extend_from_slice(&self.size()); - result.extend_from_slice(self.serialize_data()); + // 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) + } - result + // 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()), } } diff --git a/src/field/utils/access_privileges.rs b/src/field/utils/access_privileges.rs new file mode 100644 index 0000000..eea9557 --- /dev/null +++ b/src/field/utils/access_privileges.rs @@ -0,0 +1,47 @@ +//! This module contains utilities to deal with the user access bitmap + +pub const CAN_DELETE_FILES: u64 = 2_u64.pow(0); +pub const CAN_UPLOAD_FILES: u64 = 2_u64.pow(1); +pub const CAN_DOWNLOAD_FILES: u64 = 2_u64.pow(2); +pub const CAN_RENAME_FILES: u64 = 2_u64.pow(3); +pub const CAN_MOVE_FILES: u64 = 2_u64.pow(4); +pub const CAN_CREATE_FOLDERS: u64 = 2_u64.pow(5); +pub const CAN_DELETE_FOLDERS: u64 = 2_u64.pow(6); +pub const CAN_RENAME_FOLDERS: u64 = 2_u64.pow(7); +pub const CAN_MOVE_FOLDERS: u64 = 2_u64.pow(8); +pub const CAN_READ_CHATS: u64 = 2_u64.pow(9); +pub const CAN_SEND_CHATS: u64 = 2_u64.pow(10); +pub const CAN_OPEN_CHATS: u64 = 2_u64.pow(11); +pub const CAN_CLOSE_CHATS: u64 = 2_u64.pow(12); +pub const CAN_BE_SHOWN_IN_LIST: u64 = 2_u64.pow(13); +pub const CAN_CREATE_USERS: u64 = 2_u64.pow(14); +pub const CAN_DELETE_USERS: u64 = 2_u64.pow(15); +pub const CAN_OPEN_USERS: u64 = 2_u64.pow(16); +pub const CAN_MODIFY_USERS: u64 = 2_u64.pow(17); +pub const CAN_CHANGE_OWN_PASSWORD: u64 = 2_u64.pow(18); +pub const CAN_SEND_PRIVATE_MESSAGES: u64 = 2_u64.pow(19); +pub const CAN_READ_NEWS_ARTICLES: u64 = 2_u64.pow(20); +pub const CAN_POST_NEWS_ARTICLES: u64 = 2_u64.pow(21); +pub const CAN_DISCONNECT_USERS: u64 = 2_u64.pow(22); +pub const CANNOT_BE_DISCONNECTED: u64 = 2_u64.pow(23); +pub const CAN_GET_CLIENT_INFO: u64 = 2_u64.pow(24); +pub const CAN_UPLOAD_ANYWHERE: u64 = 2_u64.pow(25); +pub const CAN_USE_ANY_NAME: u64 = 2_u64.pow(26); +pub const CAN_SKIP_AGREEMENT: u64 = 2_u64.pow(27); +pub const CAN_SET_FILE_COMMENT: u64 = 2_u64.pow(28); +pub const CAN_SET_FOLDER_COMMENT: u64 = 2_u64.pow(29); +pub const CAN_VIEW_DROP_BOXES: u64 = 2_u64.pow(30); +pub const CAN_MAKE_ALIASES: u64 = 2_u64.pow(31); +pub const CAN_BROADCAST: u64 = 2_u64.pow(32); +pub const CAN_DELETE_NEWS_ARTICLES: u64 = 2_u64.pow(33); +pub const CAN_CREATE_NEWS_CATEGORIES: u64 = 2_u64.pow(34); +pub const CAN_DELETE_NEWS_CATEGORIES: u64 = 2_u64.pow(35); +pub const CAN_CREATE_NEWS_FOLDERS: u64 = 2_u64.pow(36); +pub const CAN_DELETE_NEWS_FOLDERS: u64 = 2_u64.pow(37); +pub const CAN_UPLOAD_FOLDERS: u64 = 2_u64.pow(38); +pub const CAN_DOWNLOAD_FOLDERS: u64 = 2_u64.pow(39); +pub const CAN_SEND_MESSAGES: u64 = 2_u64.pow(40); + +pub fn can(permission: u64, bits: u64) -> bool { + bits & permission != 0 +} diff --git a/src/field/utils/banner_type.rs b/src/field/utils/banner_type.rs new file mode 100644 index 0000000..eb4e10c --- /dev/null +++ b/src/field/utils/banner_type.rs @@ -0,0 +1,9 @@ +#[repr(u16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BannerType { + URL = 1, + JPEG = 3, + GIF = 4, + BMP = 5, + PICT = 6, +} diff --git a/src/field/utils/mod.rs b/src/field/utils/mod.rs new file mode 100644 index 0000000..83b3239 --- /dev/null +++ b/src/field/utils/mod.rs @@ -0,0 +1,6 @@ +//! This module contains utilities to interact with specialized fields (eg. +//! bitmaps) + +pub mod access_privileges; +pub mod banner_type; +pub mod user_flags; diff --git a/src/field/utils/user_flags.rs b/src/field/utils/user_flags.rs new file mode 100644 index 0000000..6c79e8b --- /dev/null +++ b/src/field/utils/user_flags.rs @@ -0,0 +1,22 @@ +//! This module contains utilities to read the data in a UserFlags bitmap. + +const AWAY: u32 = 0b0001; +const ADMIN_OR_DISCONNECTED: u32 = 0b0010; +const REFUSES_PRIVATE_MESSAGES: u32 = 0b0100; +const REFUSES_PRIVATE_CHAT: u32 = 0b1000; + +pub fn is_away(bits: u32) -> bool { + bits & AWAY != 0 +} + +pub fn is_admin_or_disconnected(bits: u32) -> bool { + bits & ADMIN_OR_DISCONNECTED != 0 +} + +pub fn refuses_private_messages(bits: u32) -> bool { + bits & REFUSES_PRIVATE_MESSAGES != 0 +} + +pub fn refuses_private_chat(bits: u32) -> bool { + bits & REFUSES_PRIVATE_CHAT != 0 +} @@ -1,4 +1,2 @@ -//! `linea_caliente` implements the hotline protocol to help build servers and -//! clients around it. pub mod field; -pub mod transaction; +// pub mod transactions; diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs deleted file mode 100644 index b1cc12a..0000000 --- a/src/transaction/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::field::Data; - -pub enum Request { - GetMessages, - NewMessage { data: Data }, - OldPostNews { data: Data }, - ServerMessage, -} - -impl Request { - pub fn get_type(&self) -> u8 { - match self { - Request::GetMessages => 101, - Request::NewMessage { data: _ } => 102, - Request::OldPostNews { data: _ } => 103, - Request::ServerMessage => 104, - } - } -} |