summaryrefslogtreecommitdiff
path: root/protocol
diff options
context:
space:
mode:
Diffstat (limited to 'protocol')
-rw-r--r--protocol/Cargo.toml17
-rw-r--r--protocol/src/address.rs56
-rw-r--r--protocol/src/context.rs71
-rw-r--r--protocol/src/impls.rs1
-rw-r--r--protocol/src/lib.rs4
-rw-r--r--protocol/src/member.rs29
-rw-r--r--protocol/src/member/email.rs74
-rw-r--r--protocol/src/member/error.rs8
-rw-r--r--protocol/src/protocol.rs118
-rw-r--r--protocol/src/protocol/error.rs39
-rw-r--r--protocol/src/protocol/fetched_info.rs4
-rw-r--r--protocol/src/protocol/index_transfer.rs26
-rw-r--r--protocol/src/protocol/operations.rs42
-rw-r--r--protocol/src/server_space.rs1
-rw-r--r--protocol/src/user_space.rs1
15 files changed, 491 insertions, 0 deletions
diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml
new file mode 100644
index 0000000..bed182e
--- /dev/null
+++ b/protocol/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "protocol"
+edition = "2024"
+version.workspace = true
+
+[dependencies]
+constants = { path = "../systems/_constants" }
+framework = { path = "../systems/_framework" }
+sheet_system = { path = "../systems/sheet" }
+vault_system = { path = "../systems/vault" }
+workspace_system = { path = "../systems/workspace" }
+
+serde.workspace = true
+thiserror.workspace = true
+tokio.workspace = true
+
+dirs = "6.0.0"
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;