aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-02-09 22:44:21 +0100
committerRuben Beltran del Rio <git@r.bdr.sh>2025-02-09 22:44:21 +0100
commitc402a8923d2e89607a007f46ee4cde15714d746d (patch)
tree597f0e436cac0ec2dda2eb8437cfb57de6c95a82 /src
parent48408b9e42eaeeaf0944f9a37f7882ba9ddc7f19 (diff)
Restart with fields module
Diffstat (limited to 'src')
-rw-r--r--src/field/data.rs24
-rw-r--r--src/field/id.rs149
-rw-r--r--src/field/mod.rs145
-rw-r--r--src/field/utils/access_privileges.rs47
-rw-r--r--src/field/utils/banner_type.rs9
-rw-r--r--src/field/utils/mod.rs6
-rw-r--r--src/field/utils/user_flags.rs22
-rw-r--r--src/lib.rs4
-rw-r--r--src/transaction/mod.rs19
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
+}
diff --git a/src/lib.rs b/src/lib.rs
index 397cbd2..187380a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
- }
- }
-}