diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-03-18 11:19:51 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-03-18 11:19:51 +0800 |
| commit | 2372495e1a0acb9ffead7651d8ed36a3bb98a15b (patch) | |
| tree | 5f330cdf1616b1e56a7b85b2b2530cdf1422ed54 /protocol/src | |
| parent | 6f8906f06f3efd009275dc23f861f5aaba76ce72 (diff) | |
Add new protocol crate with basic types and operations
Diffstat (limited to 'protocol/src')
| -rw-r--r-- | protocol/src/address.rs | 56 | ||||
| -rw-r--r-- | protocol/src/context.rs | 71 | ||||
| -rw-r--r-- | protocol/src/impls.rs | 1 | ||||
| -rw-r--r-- | protocol/src/lib.rs | 4 | ||||
| -rw-r--r-- | protocol/src/member.rs | 29 | ||||
| -rw-r--r-- | protocol/src/member/email.rs | 74 | ||||
| -rw-r--r-- | protocol/src/member/error.rs | 8 | ||||
| -rw-r--r-- | protocol/src/protocol.rs | 118 | ||||
| -rw-r--r-- | protocol/src/protocol/error.rs | 39 | ||||
| -rw-r--r-- | protocol/src/protocol/fetched_info.rs | 4 | ||||
| -rw-r--r-- | protocol/src/protocol/index_transfer.rs | 26 | ||||
| -rw-r--r-- | protocol/src/protocol/operations.rs | 42 | ||||
| -rw-r--r-- | protocol/src/server_space.rs | 1 | ||||
| -rw-r--r-- | protocol/src/user_space.rs | 1 |
14 files changed, 474 insertions, 0 deletions
diff --git a/protocol/src/address.rs b/protocol/src/address.rs new file mode 100644 index 0000000..ab49df4 --- /dev/null +++ b/protocol/src/address.rs @@ -0,0 +1,56 @@ +use crate::protocol::BasicProtocol; +use std::marker::PhantomData; + +/// Upstream, used by Workspace to describe the protocol of UpstreamVault +pub struct Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + /// Protocol of the target upstream machine + _p: PhantomData<Protocol>, + + /// Address of the target upstream machine + target_address: String, +} + +impl<Protocol> Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn new(addr: &str) -> Self { + Upstream { + _p: PhantomData, + target_address: addr.to_string(), + } + } +} + +/// Host, used by Vault to describe its own protocol +pub struct Host<Protocol> +where + Protocol: BasicProtocol, +{ + /// Protocol of the target upstream machine + _p: PhantomData<Protocol>, +} + +impl<Protocol> Upstream<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn address(addr: &str) -> Self { + Upstream { + _p: PhantomData, + target_address: addr.to_string(), + } + } +} + +impl<Protocol> Host<Protocol> +where + Protocol: BasicProtocol, +{ + pub fn new() -> Self { + Host { _p: PhantomData } + } +} diff --git a/protocol/src/context.rs b/protocol/src/context.rs new file mode 100644 index 0000000..4fdddb9 --- /dev/null +++ b/protocol/src/context.rs @@ -0,0 +1,71 @@ +use framework::space::{Space, SpaceRoot}; + +pub struct ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Current context's Vault + pub remote: Option<Space<Server>>, + + /// Current context's Workspace + pub user: Option<Space<User>>, +} + +impl<User, Server> ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Create a new local context with a user + pub fn new_local(user: Space<User>) -> Self { + Self { + remote: None, + user: Some(user), + } + } + + /// Create a new remote context with a server + pub fn new_remote(server: Space<Server>) -> Self { + Self { + remote: Some(server), + user: None, + } + } +} + +impl<User, Server> ProtocolContext<User, Server> +where + User: SpaceRoot, + Server: SpaceRoot, +{ + /// Check if the context is remote (has a server) + pub fn is_remote(&self) -> bool { + self.remote.is_some() + } + + /// Check if the context is local (has a user) + pub fn is_local(&self) -> bool { + self.user.is_some() + } + + /// Get the remote vault if it exists + pub fn remote(&self) -> Option<&Space<Server>> { + self.remote.as_ref() + } + + /// Get the local workspace if it exists + pub fn local(&self) -> Option<&Space<User>> { + self.user.as_ref() + } + + /// Unwrap the local workspace, panics if not local + pub fn unwrap_local(&self) -> &Space<User> { + self.user.as_ref().expect("Not a local context") + } + + /// Unwrap the remote vault, panics if not remote + pub fn unwrap_remote(&self) -> &Space<Server> { + self.remote.as_ref().expect("Not a remote context") + } +} diff --git a/protocol/src/impls.rs b/protocol/src/impls.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/impls.rs @@ -0,0 +1 @@ +pub mod jvcs; diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs new file mode 100644 index 0000000..6b54baa --- /dev/null +++ b/protocol/src/lib.rs @@ -0,0 +1,4 @@ +pub mod address; +pub mod context; +pub mod member; +pub mod protocol; diff --git a/protocol/src/member.rs b/protocol/src/member.rs new file mode 100644 index 0000000..a892118 --- /dev/null +++ b/protocol/src/member.rs @@ -0,0 +1,29 @@ +use crate::member::email::EMailAddress; +use serde::{Deserialize, Serialize}; + +pub mod email; +pub mod error; + +#[derive(Debug, Clone, Eq, Serialize, Deserialize)] +pub struct Member { + name: String, + mail: EMailAddress, +} + +impl PartialEq for Member { + fn eq(&self, other: &Self) -> bool { + self.mail == other.mail + } +} + +impl std::hash::Hash for Member { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.mail.hash(state); + } +} + +impl std::fmt::Display for Member { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/protocol/src/member/email.rs b/protocol/src/member/email.rs new file mode 100644 index 0000000..a3b829f --- /dev/null +++ b/protocol/src/member/email.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::member::error::EMailAddressParseError; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Debug, Eq)] +pub struct EMailAddress { + account: String, + domain: String, +} + +impl std::hash::Hash for EMailAddress { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.account.hash(state); + self.domain.hash(state); + } +} + +impl PartialEq for EMailAddress { + fn eq(&self, other: &Self) -> bool { + self.account == other.account && self.domain == other.domain + } +} + +impl Display for EMailAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.account, self.domain) + } +} + +impl TryFrom<&str> for EMailAddress { + type Error = EMailAddressParseError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + let trimmed_value = value.trim(); + let parts: Vec<&str> = trimmed_value.split('@').collect(); + if parts.len() != 2 { + return Err(EMailAddressParseError::InvalidFormat); + } + let account = parts[0].trim().to_string(); + let domain = parts[1].trim().to_string(); + if account.is_empty() || domain.is_empty() { + return Err(EMailAddressParseError::EmptyPart); + } + Ok(Self { account, domain }) + } +} + +impl TryFrom<String> for EMailAddress { + type Error = EMailAddressParseError; + + fn try_from(value: String) -> Result<Self, Self::Error> { + Self::try_from(value.as_str()) + } +} + +impl Serialize for EMailAddress { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EMailAddress { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + EMailAddress::try_from(s).map_err(serde::de::Error::custom) + } +} diff --git a/protocol/src/member/error.rs b/protocol/src/member/error.rs new file mode 100644 index 0000000..143329c --- /dev/null +++ b/protocol/src/member/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error)] +pub enum EMailAddressParseError { + #[error("Invalid email format")] + InvalidFormat, + + #[error("Account or domain cannot be empty")] + EmptyPart, +} diff --git a/protocol/src/protocol.rs b/protocol/src/protocol.rs new file mode 100644 index 0000000..a097989 --- /dev/null +++ b/protocol/src/protocol.rs @@ -0,0 +1,118 @@ +use std::path::PathBuf; + +use crate::{ + context::ProtocolContext, + member::Member, + protocol::{ + error::{FetchLatestInfoFailed, ProtocolAuthorizeFailed, VaultOperationFailed}, + fetched_info::FetchedInfo, + index_transfer::IndexesTransfer, + operations::{VaultHostOperations, VaultOperations}, + }, +}; +use framework::space::{Space, SpaceRoot}; +use vault_system::vault::Vault; +use workspace_system::workspace::Workspace; + +pub mod error; +pub mod fetched_info; +pub mod index_transfer; +pub mod operations; + +pub trait BasicProtocol { + type User: SpaceRoot; + type Server: SpaceRoot; + + /// Protocol name + /// For example: return "jvcs" to accept protocols starting with "jvcs://" + fn protocol_name() -> &'static str; + + /// Authentication + /// + /// authorizing is executed on the client side, and for some protocols also on the server side, + /// ultimately outputting the authentication result + /// - Ok(Member): indicates a successfully authenticated member + /// - Err(ProtocolAuthorizeFailed): indicates the reason for failure + fn authorizing( + &self, + ctx: &ProtocolContext<Self::User, Self::Server>, + ) -> impl Future<Output = Result<Member, ProtocolAuthorizeFailed>> + Send; + + /// Host authentication + /// + /// Authenticate as a host (administrator) on the server side. + /// Returns the authenticated host member if successful. + fn authorizing_host( + &self, + ctx: &ProtocolContext<Self::User, Self::Server>, + ) -> impl Future<Output = Result<Member, ProtocolAuthorizeFailed>> + Send; + + /// Build user space + /// + /// Build and return the user space based on the given current directory path. + fn build_user_space(&self, current_dir: PathBuf) -> Space<Self::User>; + + /// Build server space + /// + /// Build and return the server space based on the given current directory path. + fn build_server_space(&self, current_dir: PathBuf) -> Space<Self::Server>; + + /// Fetch the latest information from upstream + /// + /// - On the local side, workspace will be Some + /// - On the remote side, vault will be Some + /// + /// # Result + /// - The remote side returns Ok(Some(FetchedInfo)) to send data to the local side + /// - The local side returns Ok(Some(FetchedInfo)) to get the latest information + fn fetch_latest_info( + &self, + workspace: Option<Space<Workspace>>, + vault: Option<Space<Vault>>, + ) -> impl Future<Output = Result<Option<FetchedInfo>, FetchLatestInfoFailed>> + Send; + + /// Transfer indexes + /// + /// - `index_transfer` and `storage` represent the index file and + /// the corresponding block storage path, respectively. + /// - If `vault` is Some, send block information from the Vault. + /// - If `workspace` is Some, send block information from the Workspace. + /// + /// Each block transfer is atomic, but the overall transfer is not atomic. + /// Blocks that have been successfully transferred are permanently retained on the upstream side. + /// + /// After the block information is correctly stored on the other side, + /// transfer the `index_file` and register it to the corresponding IndexSource. + /// + /// If the IndexSource to be registered is a `local_id`, the server will issue a `remote_id` + /// and write it into the local ID mapping. + fn transfer_indexes( + &self, + index_transfer: IndexesTransfer, + storage: PathBuf, + workspace: Option<Space<Workspace>>, + vault: Option<Space<Vault>>, + ) -> impl Future<Output = Result<(), FetchLatestInfoFailed>> + Send; + + /// Handle operations + /// + /// Requests are sent from the client to the server, + /// and the following requests are uniformly processed on the server side. + fn handle_operation( + &self, + operation: VaultOperations, + authorized_member: Member, + vault: Space<Vault>, + ) -> impl Future<Output = Result<(), VaultOperationFailed>> + Send; + + /// Handle host operations + /// + /// Requests are sent from the host (administrator) to the server, + /// and the following requests are uniformly processed on the server side. + fn handle_host_operation( + &self, + operation: VaultHostOperations, + authorized_host: Member, + vault: Space<Vault>, + ) -> impl Future<Output = Result<(), VaultOperationFailed>> + Send; +} diff --git a/protocol/src/protocol/error.rs b/protocol/src/protocol/error.rs new file mode 100644 index 0000000..ed39aaa --- /dev/null +++ b/protocol/src/protocol/error.rs @@ -0,0 +1,39 @@ +use crate::member::Member; + +#[derive(Debug, thiserror::Error)] +pub enum ProtocolAuthorizeFailed { + #[error("Member not found")] + MemberNotFound, + + #[error("No permission")] + NoPermission, +} + +#[derive(Debug, thiserror::Error)] +pub enum FetchLatestInfoFailed { + #[error("Connection failed")] + Connection, + + #[error("Permission denied")] + PermissionDenied, +} + +#[derive(Debug, thiserror::Error)] +pub enum VaultOperationFailed { + /// Index is already held + /// Cannot advance version, claim, or relinquish ownership + #[error("Index already held by `{0}`")] + IndexAlreadyHeldBy(Member), + + /// Sheet depends on local namespace + /// Cannot backup or write Ref + #[error("Sheet depends on local namespace")] + SheetDependsOnLocalNamespace, + + /// Operation not supported by the current protocol + #[error("Operation not supported")] + OperationNotSupported, + + #[error("IO error: `{0}`")] + IOError(#[from] std::io::Error), +} diff --git a/protocol/src/protocol/fetched_info.rs b/protocol/src/protocol/fetched_info.rs new file mode 100644 index 0000000..1ad64de --- /dev/null +++ b/protocol/src/protocol/fetched_info.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FetchedInfo {} diff --git a/protocol/src/protocol/index_transfer.rs b/protocol/src/protocol/index_transfer.rs new file mode 100644 index 0000000..4170289 --- /dev/null +++ b/protocol/src/protocol/index_transfer.rs @@ -0,0 +1,26 @@ +use std::{collections::HashMap, path::PathBuf}; + +use sheet_system::index_source::IndexSource; + +#[derive(Default)] +pub struct IndexesTransfer { + inner: HashMap<PathBuf, IndexSource>, +} + +impl IndexesTransfer { + /// Insert an index file path and its source into the collection + pub fn insert(&mut self, path: PathBuf, source: IndexSource) -> Option<IndexSource> { + self.inner.insert(path, source) + } + + /// Returns an iterator over the index file paths and their sources + pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &IndexSource)> { + self.inner.iter() + } + + /// Insert an index file path and its source into the collection and return self for chaining + pub fn with(mut self, path: PathBuf, source: IndexSource) -> Self { + self.inner.insert(path, source); + self + } +} diff --git a/protocol/src/protocol/operations.rs b/protocol/src/protocol/operations.rs new file mode 100644 index 0000000..5d49b29 --- /dev/null +++ b/protocol/src/protocol/operations.rs @@ -0,0 +1,42 @@ +use sheet_system::sheet::Sheet; + +use crate::member::Member; + +pub type SheetName = String; + +pub enum VaultOperations { + /// Claim ownership of an index + HoldIndex(u32), + + /// Release ownership of an index + ThrowIndex(u32), + + /// Backup a Sheet to personal space + BackupSheet(Sheet), + + /// Download a Sheet from personal space + DownloadSheet(SheetName), + + /// Download a RefSheet from public space + DownloadRefSheet(SheetName), +} + +pub enum VaultHostOperations { + /// Forcefully grant ownership of an index to a member + HoldIndexForce(Member, u32), + + /// Forcefully discard ownership of some indices + ThrowIndexForce(Vec<u32>), + + /// Write a RefSheet to upstream + WriteRefSheet(Sheet), + + /// Erase a Ref + EraseRefSheet(SheetName), + + /// Erase some indices + DangerousEraseIndex(Vec<u32>), + + /// Erase some versions of an index + DangerousEraseVersion(u32, Vec<u16>), +} diff --git a/protocol/src/server_space.rs b/protocol/src/server_space.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/server_space.rs @@ -0,0 +1 @@ +pub mod jvcs; diff --git a/protocol/src/user_space.rs b/protocol/src/user_space.rs new file mode 100644 index 0000000..453d7dd --- /dev/null +++ b/protocol/src/user_space.rs @@ -0,0 +1 @@ +pub mod jvcs; |
