Compare commits

...
Sign in to create a new pull request.

22 commits

Author SHA1 Message Date
e8ad5b5c23 Split protobuf files 2025-10-03 06:42:36 +09:00
6256c61a45 Split protobuf of tripod-id 2025-10-02 07:03:25 +09:00
97a3f86ef9 Fix include_proto and proto::common 2025-10-01 12:50:25 +09:00
ee68c5be0e Rename protobuf 2025-10-01 08:09:36 +09:00
8650746d66 Refactor protobuf 2025-10-01 08:02:25 +09:00
87d78e7605 Update protobuf 2025-10-01 07:19:35 +09:00
de211d2b71 Add protobuf vscode config 2025-09-30 08:09:04 +09:00
20c963f6c8 Rename peer to remote_node 2025-09-30 08:07:50 +09:00
6e13cef237 Add authorization_request.proto 2025-09-29 07:44:53 +09:00
7a77ec87d3 Remove duplicated tripod_id.proto 2025-09-28 08:39:01 +09:00
22fbe53710 Update local data and protobuf 2025-09-27 19:30:17 +09:00
2a29f1fc82 Implement serde feature 2025-09-26 07:48:58 +09:00
65d189c990 Update test about tripod-id 2025-09-25 08:04:15 +09:00
26dda29c8d Addig tests about tripod-id prost feature 2025-09-24 08:07:31 +09:00
4793a96587 Rename caretta-id to tripod-id 2025-09-23 22:20:18 +09:00
9ee5156dfc Implement AuthorizationRequestRecords 2025-09-18 08:14:57 +09:00
88d87bd25d Add test about PeerRecord 2025-09-17 09:20:06 +09:00
7842e4eb3e Update local data and migration 2025-09-15 20:00:57 +09:00
dd43b89086 Implement rusqlite feature to caretta-id 2025-09-15 17:05:30 +09:00
71e0d31d8d Implement TripleId 2025-09-13 12:29:59 +09:00
6fb909cd07 Implement DoubleId 2025-09-13 08:39:06 +09:00
99fdb12712 Add caretta-id 2025-09-12 09:27:14 +09:00
97 changed files with 2545 additions and 292 deletions

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"zxh404.vscode-proto3"
]
}

10
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"protoc": {
"compile_on_save": true,
"options": [
"--proto_path=${workspaceRoot}/tripod-id/proto",
"--proto_path=${workspaceRoot}/core/proto",
"--java_out=${workspaceRoot}/.tmp"
]
}
}

View file

@ -26,7 +26,7 @@ caretta-sync-macros = { path="macros", optional = true}
caretta-sync-core = {workspace = true, features = ["test"]}
[workspace]
members = [ ".", "core", "macros", "cli", "mobile", "examples/*" , "bevy"]
members = [ ".", "core", "macros", "cli", "mobile", "examples/*" , "bevy", "tripod-id"]
resolver = "3"
[workspace.package]
@ -38,11 +38,14 @@ repository = "https://forgejo.fireturlte.net/lazy-supplements"
[workspace.dependencies]
bevy = { git = "https://github.com/bevyengine/bevy.git", rev="16ffdaea0daec11e4347d965f56c9c8e1122a488" }
tripod-id = {path="./tripod-id", features=["prost", "rusqlite", "serde"]}
chrono = "0.4.41"
ciborium = "0.2.2"
clap = { version = "4.5.38", features = ["derive"] }
caretta-sync-core.path = "core"
futures = { version = "0.3.31", features = ["executor"] }
rand = "0.8.5"
rusqlite = "0.37.0"
serde = { version = "1.0.219", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.45.0", features = ["macros", "rt", "rt-multi-thread"] }
@ -50,12 +53,13 @@ tokio-stream = "0.1.17"
tonic = "0.14.0"
url = { version = "2.5.7", features = ["serde"] }
uuid = { version = "1.17.0", features = ["v7"] }
iroh = { version = "0.91.2", features = ["discovery-local-network", "discovery-pkarr-dht"] }
iroh = { version = "0.92.0", features = ["discovery-local-network", "discovery-pkarr-dht"] }
prost = "0.14.1"
prost-types = "0.14.1"
tonic-prost-build = "0.14.0"
tonic-prost = "0.14.0"
[profile.dev]
opt-level = 1

View file

@ -1,24 +0,0 @@
use clap::Args;
use caretta_sync_core::utils::runnable::Runnable;
use crate::cli::ConfigArgs;
use crate::cli::PeerArgs;
#[derive(Debug, Args)]
pub struct DeviceAddCommandArgs {
#[command(flatten)]
peer: PeerArgs,
#[arg(short, long)]
passcode: Option<String>,
#[command(flatten)]
config: ConfigArgs
}
impl Runnable for DeviceAddCommandArgs {
fn run(self, app_name: &'static str) {
todo!()
}
}

View file

View file

View file

View file

View file

@ -13,6 +13,7 @@ test = ["dep:tempfile", ]
[dependencies]
base64 = "0.22.1"
tripod-id.workspace = true
chrono.workspace = true
chrono-tz = "0.10.3"
ciborium.workspace = true
@ -22,7 +23,7 @@ futures.workspace = true
iroh.workspace = true
prost.workspace = true
prost-types.workspace = true
rusqlite = { version = "0.37.0", features = ["bundled", "chrono"] }
rusqlite = { workspace = true, features = ["bundled", "chrono", "uuid"] }
serde.workspace = true
sysinfo = "0.37.0"
tempfile = { version = "3.20.0", optional = true }
@ -36,7 +37,7 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uuid.workspace = true
url.workspace = true
whoami = "1.6.1"
rand = "0.8.5"
rand.workspace = true
ed25519-dalek = { version = "2.2.0", features = ["signature"] }
tokio-stream.workspace = true

View file

@ -1,4 +1,14 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_prost_build::compile_protos("proto/caretta_sync.proto")?;
tonic_prost_build::configure()
.extern_path(".tripod_id", "::tripod_id::prost")
.compile_protos(
&[
"proto/caretta_sync/authorization_request/authorization_request.proto",
"proto/caretta_sync/authorized_node/authorized_node.proto",
"proto/caretta_sync/remote_node/remote_node.proto",
],
&["proto", "../tripod-id/proto"]
)?;
Ok(())
}

View file

@ -1,51 +0,0 @@
syntax = "proto3";
package caretta_sync;
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
service CarettaSync {
rpc RemoteInfo(RemoteInfoRequest) returns (RemoteInfoResponse);
rpc RemoteInfoIter(RemoteInfoIterRequest) returns (stream RemoteInfoResponse);
}
message NodeIdMessage {
bytes node_id = 1;
}
message RemoteInfoRequest {
NodeIdMessage node_id = 1;
}
message RemoteInfoIterRequest {}
message RemoteInfoResponse {
RemoteInfoMessage remote_info = 1;
}
message RemoteInfoMessage {
NodeIdMessage node_id = 1;
string relay_url = 2;
repeated DirectAddrInfoMessage addrs = 3;
string conn_type = 4;
google.protobuf.Duration latency = 5;
google.protobuf.Duration last_used = 6;
}
message DirectAddrInfoMessage {
string addr = 1;
google.protobuf.Duration latency = 2;
LastControlMessage last_control = 3;
google.protobuf.Duration last_payload = 4;
google.protobuf.Duration last_alive = 5;
repeated SourceMessage sources = 6;
}
message LastControlMessage {
google.protobuf.Duration duration = 1;
string control_msg = 2;
}
message SourceMessage {
string source = 1;
google.protobuf.Duration duration = 2;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/identifier.proto";
message AcceptRequest {
Identifier authorization_request = 1;
string passcode = 2;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorized_node/info.proto";
message AcceptResponse {
caretta_sync.authorized_node.Info authorized_node_info = 1;
}

View file

@ -0,0 +1,23 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/accept_request.proto";
import "caretta_sync/authorization_request/accept_response.proto";
import "caretta_sync/authorization_request/info_request.proto";
import "caretta_sync/authorization_request/info_response.proto";
import "caretta_sync/authorization_request/list_request.proto";
import "caretta_sync/authorization_request/list_response.proto";
import "caretta_sync/authorization_request/reject_request.proto";
import "caretta_sync/authorization_request/reject_response.proto";
import "caretta_sync/authorization_request/send_request.proto";
import "caretta_sync/authorization_request/send_response.proto";
service AuthorizationRequest {
rpc Send(SendRequest) returns (SendResponse);
rpc Accept(AcceptRequest) returns (AcceptResponse);
rpc Reject(RejectRequest) returns (RejectResponse);
rpc Info(InfoRequest) returns (InfoResponse);
rpc List(stream ListRequest) returns (stream ListResponse);
}

View file

@ -0,0 +1,12 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/common/uuid.proto";
import "tripod_id/double.proto";
message Identifier {
oneof identifier_value {
caretta_sync.common.Uuid uuid = 1;
tripod_id.Double id = 2;
}
}

View file

@ -0,0 +1,16 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/iroh/public_key.proto";
import "caretta_sync/authorization_request/status.proto";
import "tripod_id/double.proto";
import "google/protobuf/timestamp.proto";
message Info {
tripod_id.Double id = 1;
caretta_sync.iroh.PublicKey public_key = 2;
google.protobuf.Timestamp created_at = 3;
google.protobuf.Timestamp closed_at = 6;
Status status = 4;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/identifier.proto";
message InfoRequest {
Identifier request = 1;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/info.proto";
message InfoResponse{
Info info = 1;
}

View file

@ -0,0 +1,4 @@
syntax = "proto3";
package caretta_sync.authorization_request;
message ListRequest {}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/info.proto";
message ListResponse {
Info request_info = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/identifier.proto";
message RejectRequest {
Identifier authorization_request = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/authorization_request/info.proto";
message RejectResponse {
Info request_info = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/remote_node/identifier.proto";
message SendRequest {
caretta_sync.remote_node.Identifier remote_node = 1;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.authorization_request;
import "caretta_sync/remote_node/info.proto";
message SendResponse {
caretta_sync.remote_node.Info remote_node_info = 1;
string passcode = 2;
}

View file

@ -0,0 +1,10 @@
syntax = "proto3";
package caretta_sync.authorization_request;
enum Status {
UNSPECIFIED = 0;
SENT = 1;
RECEIVED = 2;
ACCEPTED = 3;
REJECTED = 4;
}

View file

@ -0,0 +1,14 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "caretta_sync/authorized_node/info_request.proto";
import "caretta_sync/authorized_node/info_response.proto";
import "caretta_sync/authorized_node/list_request.proto";
import "caretta_sync/authorized_node/list_response.proto";
service AuthorizedNode {
rpc Info(InfoRequest) returns (InfoResponse);
rpc List(stream ListRequest) returns (stream ListResponse);
}

View file

@ -0,0 +1,14 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "tripod_id/single.proto";
import "caretta_sync/iroh/public_key.proto";
message Identifier {
oneof identifier_value {
tripod_id.Single id = 1;
caretta_sync.iroh.PublicKey public_key = 2;
}
}

View file

@ -0,0 +1,12 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "tripod_id/single.proto";
import "caretta_sync/iroh/public_key.proto";
message Info {
tripod_id.Single id = 1;
caretta_sync.iroh.PublicKey public_key = 2;
string note = 3;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "caretta_sync/authorized_node/identifier.proto";
message InfoRequest {
Identifier node = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "caretta_sync/authorized_node/info.proto";
message InfoResponse {
Info node_info = 1;
}

View file

@ -0,0 +1,5 @@
syntax = "proto3";
package caretta_sync.authorized_node;
message ListRequest{}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.authorized_node;
import "caretta_sync/authorized_node/info.proto";
message ListResponse {
Info node_info = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.common;
// protobuf message of url::Url.
message Url {
string url = 1;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.common;
// protobuf message of uuid::Uuid
message Uuid {
uint64 high_bits = 1;
uint64 low_bits = 2;
}

View file

@ -0,0 +1,26 @@
syntax = "proto3";
package caretta_sync.iroh;
import "caretta_sync/common/url.proto";
import "caretta_sync/net/socket_addr.proto";
// A protobuf message of iroh::ConnectionType
message ConnectionType {
message Direct {
caretta_sync.net.SocketAddr direct_value = 1;
}
message Relay {
caretta_sync.common.Url relay_value = 1;
}
message Mixed {
caretta_sync.net.SocketAddr socket_addr = 1;
caretta_sync.common.Url relay_url = 2;
}
message None{}
oneof connection_type_value {
Direct direct = 1;
Relay relay = 2;
Mixed mixed = 3;
None none = 4;
}
}

View file

@ -0,0 +1,18 @@
syntax = "proto3";
package caretta_sync.iroh;
// The message of iroh::endpoint::ControlMsg.
// To ensure compatiility with irof::endpoint::ControlMsg,
// it's implemented as a message rather than an enum to accommodate the posibility
// that values may be added to the enum in rust in the future.
message ControlMsg {
message Ping {}
message Pong {}
message CallMeMayBe {}
oneof control_msg_vaue {
Ping ping = 1;
Pong pong = 2;
CallMeMayBe call_me_maybe = 3;
}
}

View file

@ -0,0 +1,30 @@
syntax = "proto3";
package caretta_sync.iroh;
import "google/protobuf/duration.proto";
import "caretta_sync/iroh/control_msg.proto";
import "caretta_sync/iroh/source.proto";
import "caretta_sync/net/socket_addr.proto";
// A protobuf message of iroh::endpoint::DirectAddrInfo
message DirectAddrInfo {
// A protobuf message of (Duration, ControlMsg)
message DurationControlMsg {
google.protobuf.Duration duration = 1;
ControlMsg control_msg = 2;
}
// A protobuf message of (iroh::Source, Duration)
message SourceDuration {
Source source = 1;
google.protobuf.Duration duration = 2;
}
caretta_sync.net.SocketAddr addr = 1;
google.protobuf.Duration latency = 2;
DurationControlMsg last_control = 3;
google.protobuf.Duration last_payload = 4;
google.protobuf.Duration last_alive = 5;
repeated SourceDuration sources = 6;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.iroh;
// protobuf message of iroh::PublicKey.
message PublicKey {
bytes key = 1;
}

View file

@ -0,0 +1,12 @@
syntax = "proto3";
package caretta_sync.iroh;
import "google/protobuf/duration.proto";
import "caretta_sync/common/url.proto";
// A protobuf message of iroh::RelayUrlInfo
message RelayUrlInfo {
caretta_sync.common.Url relay_url = 1;
google.protobuf.Duration last_alive = 2;
google.protobuf.Duration latency = 3;
}

View file

@ -0,0 +1,18 @@
syntax = "proto3";
package caretta_sync.iroh;
import "caretta_sync/iroh/public_key.proto";
import "caretta_sync/iroh/relay_url_info.proto";
import "caretta_sync/iroh/direct_addr_info.proto";
import "caretta_sync/iroh/connection_type.proto";
import "google/protobuf/duration.proto";
// A messege of iroh::RemoteInfo.
message RemoteInfo {
PublicKey node_id = 3;
RelayUrlInfo relay_url = 4;
repeated DirectAddrInfo addrs = 5;
ConnectionType conn_type = 6;
google.protobuf.Duration latency = 7;
google.protobuf.Duration last_used = 8;
}

View file

@ -0,0 +1,23 @@
syntax = "proto3";
package caretta_sync.iroh;
message Source {
message Saved{}
message Udp{}
message Relay{}
message App{}
message Discovery{
string value = 1;
}
message NamedApp {
string value = 1;
}
oneof source_value {
Saved saved = 1;
Udp udp = 2;
Relay relay = 3;
App app = 4;
Discovery discovery = 5;
NamedApp named_app = 6;
};
}

View file

@ -0,0 +1,7 @@
syntax = "proto3";
package caretta_sync.net;
message Ipv4Addr {
uint32 bits = 1;
}

View file

@ -0,0 +1,7 @@
syntax = "proto3";
package caretta_sync.net;
message Ipv6Addr {
uint64 high_bits = 1;
uint64 low_bits = 2;
}

View file

@ -0,0 +1,19 @@
syntax = "proto3";
package caretta_sync.net;
import "caretta_sync/net/socket_addr_v4.proto";
import "caretta_sync/net/socket_addr_v6.proto";
// Protobuf message of std::net::SocketAddr.
message SocketAddr {
oneof socket_addr_value {
SocketAddrV4 v4 = 1;
SocketAddrV6 v6 = 2;
}
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.net;
import "caretta_sync/net/ipv4_addr.proto";
message SocketAddrV4 {
Ipv4Addr ip = 1;
uint32 port = 2;
}

View file

@ -0,0 +1,9 @@
syntax = "proto3";
package caretta_sync.net;
import "caretta_sync/net/ipv6_addr.proto";
message SocketAddrV6 {
Ipv6Addr ip = 1;
uint32 port = 2;
}

View file

@ -0,0 +1,13 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/iroh/public_key.proto";
import "tripod_id/double.proto";
message Identifier {
oneof identifier_value {
tripod_id.Double id = 1;
caretta_sync.iroh.PublicKey public_key = 2;
}
}

View file

@ -0,0 +1,19 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/iroh/remote_info.proto";
import "tripod_id/double.proto";
message Info {
tripod_id.Double public_id = 1;
caretta_sync.iroh.RemoteInfo remote_info = 2;
}

View file

@ -0,0 +1,16 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/remote_node/identifier.proto";
message InfoRequest {
Identifier remote_node = 1;
}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/remote_node/info.proto";
message InfoResponse {
Info remote_node_info = 1;
}

View file

@ -0,0 +1,4 @@
syntax = "proto3";
package caretta_sync.remote_node;
message ListRequest{}

View file

@ -0,0 +1,8 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/remote_node/info.proto";
message ListResponse {
Info remote_node_info = 1;
}

View file

@ -0,0 +1,12 @@
syntax = "proto3";
package caretta_sync.remote_node;
import "caretta_sync/remote_node/info_request.proto";
import "caretta_sync/remote_node/info_response.proto";
import "caretta_sync/remote_node/list_request.proto";
import "caretta_sync/remote_node/list_response.proto";
service RemoteNode {
rpc Info(InfoRequest) returns (InfoResponse);
rpc List(stream ListRequest) returns (stream ListResponse);
}

View file

@ -1,87 +0,0 @@
//! Structs about authorization.
mod request;
mod response;
use std::os::unix::raw::time_t;
use chrono::{DateTime, Local, NaiveDateTime};
use iroh::NodeId;
pub use request::*;
pub use response::*;
use rusqlite::{params, types::FromSqlError, Connection};
use crate::data::local::RusqliteRecord;
/// On going authorization
pub struct Authorization {
node_id: NodeId,
passcode: String,
created_at: DateTime<Local>,
updated_at: DateTime<Local>,
}
static TABLE_NAME: &str = "authorization";
static DEFAULT_COLUMNS: [&str;4] = [
"node_id",
"passcode",
"created_at",
"updated_at"
];
impl Authorization {
pub fn new(node_id: NodeId, passcode: String) -> Self {
let timestamp = Local::now();
Self {
node_id: node_id,
passcode: passcode,
created_at: timestamp.clone(),
updated_at: timestamp
}
}
pub fn get_by_node_id(node_id: NodeId, connection: &Connection) -> Result<Self, rusqlite::Error> {
connection.query_row(
"SELECT node_id, passcode, created_at, updated_at FROM authorizaation WHRE node_id=(?1)",
params![node_id.as_bytes()],
Self::from_row
)
}
}
impl RusqliteRecord for Authorization {
fn from_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
let created_at: NaiveDateTime = row.get(2)?;
let updated_at: NaiveDateTime = row.get(3)?;
let node_id: Vec<u8> = row.get(0)?;
Ok(Self {
node_id: NodeId::from_bytes(node_id[..32].try_into().or_else(|e| {
Err(rusqlite::types::FromSqlError::InvalidBlobSize {
expected_size: 32,
blob_size: node_id.len()
})
})?).or(Err(FromSqlError::InvalidType))?,
passcode: row.get(1)?,
created_at: DateTime::from(created_at.and_utc()),
updated_at: DateTime::from(updated_at.and_utc()),
})
}
fn insert(&self, connection: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
connection.execute(
"INSERT INTO authorization (node_id, passcode, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
(&self.node_id.as_bytes(), &self.passcode, &self.created_at.naive_utc(), &self.updated_at.naive_utc()),
)?;
Ok(())
}
fn get_all(connection: &rusqlite::Connection) -> Result<Vec<Self>, rusqlite::Error> {
let mut stmt = connection.prepare(&(String::from("SELECT ") + &DEFAULT_COLUMNS.join(", ") + " FROM " + TABLE_NAME))?;
let rows = stmt.query_map(
[],
Self::from_row
)?;
let mut result= Vec::new();
for row in rows {
result.push(row?);
}
Ok(result)
}
}

View file

@ -1,8 +0,0 @@
use iroh::NodeId;
/// Request of node authentication.
#[derive(Debug, Clone)]
pub struct AuthorizationRequest {
sender_id: NodeId,
sender_info: String,
}

View file

@ -1,12 +0,0 @@
use iroh::NodeId;
/// Response of node authentication.
#[derive(Debug, Clone)]
pub struct AuthorizationResponse {
sender_id: NodeId,
passcode: String,
}

View file

@ -0,0 +1,80 @@
//! Structs about authorization.
mod sent;
mod received;
use std::os::unix::raw::time_t;
use tripod_id::Double;
use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey};
pub use sent::*;
pub use received::*;
use rusqlite::{params, types::FromSqlError, Connection};
use uuid::Uuid;
use crate::data::local::LocalRecord;
/// Request of node authentication.
#[derive(Debug, Clone)]
pub struct AuthorizationRequestRecord {
id: u32,
uuid: Uuid,
public_id: Double,
peer_id: u32,
created_at: DateTime<Local>,
closed_at: Option<DateTime<Local>>,
}
impl LocalRecord for AuthorizationRequestRecord {
const TABLE_NAME: &str = "authorization_request";
const SELECT_COLUMNS: &[&str] = &[
"id",
"uuid",
"public_id",
"peer_id",
"created_at",
"closed_at"
];
const INSERT_COLUMNS: &[&str] = &[
"uuid",
"public_id",
"peer_id",
"created_at"
];
type InsertParams<'a> = (&'a Double, &'a [u8;32], &'a NaiveDateTime);
type SelectValues = (u32, Uuid, Double, PublicKey, NaiveDateTime, Option<NaiveDateTime>);
fn from_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
let created_at: NaiveDateTime = row.get(4)?;
let closed_at: Option<NaiveDateTime> = row.get(5)?;
Ok(Self {
id: row.get(0)?,
uuid: row.get(1)?,
public_id: row.get(2)?,
peer_id: row.get(3)?,
created_at: created_at.and_utc().into(),
closed_at: closed_at.map(|x| x.and_utc().into())
})
}
}
impl From<(u32, Uuid, Double, u32, NaiveDateTime, Option<NaiveDateTime>)> for AuthorizationRequestRecord {
fn from(value: (u32, Uuid, Double, u32, NaiveDateTime, Option<NaiveDateTime>)) -> Self {
Self {
id: value.0,
uuid: value.1,
public_id: value.2,
peer_id: value.3,
created_at: value.4.and_utc().into(),
closed_at: value.5.map(|x| x.and_utc().into())
}
}
}
impl<'a> From<&'a rusqlite::Row<'_>> for AuthorizationRequestRecord {
fn from(value: &'a rusqlite::Row<'_>) -> Self {
todo!()
}
}

View file

@ -0,0 +1,41 @@
use caretta_id::{DoubleId, SingleId};
use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey};
use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION};
/// Response of node authentication.
#[derive(Debug, Clone)]
pub struct ReceivedAuthorizationRequestRecord {
id: u32,
authorization_request_id: u32,
peer_note: String,
}
impl LocalRecord for ReceivedAuthorizationRequestRecord {
const TABLE_NAME: &str = "received_authorization_request";
const SELECT_COLUMNS: &[&str] = &[
"id",
"authorization_request_id",
"peer_note"
];
const INSERT_COLUMNS: &[&str] = &[
"authorization_request_id",
"peer_note"
];
type InsertParams<'a> = (&'a u32, &'a str);
fn from_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self {
id: row.get(0)?,
authorization_request_id: row.get(1)?,
peer_note: row.get(2)?
})
}
}

View file

@ -0,0 +1,39 @@
use caretta_id::SingleId;
use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey};
use rusqlite::types::FromSqlError;
use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION};
/// Request of node authentication.
#[derive(Debug, Clone)]
pub struct SentAuthorizationRequestRecord {
id: u32,
authorization_request_id: u32,
passcode: String,
}
impl LocalRecord for SentAuthorizationRequestRecord {
const TABLE_NAME: &str = "sent_authorization_request";
const SELECT_COLUMNS: &[&str] = &[
"id",
"authorization_request_id",
"passcode",
];
const INSERT_COLUMNS: &[&str] = &[
"authorization_request_id",
"passcode"
];
type InsertParams<'a> = (&'a u32, &'a str);
fn from_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self{
id: row.get(0)?,
authorization_request_id: row.get(0)?,
passcode: row.get(2)?
})
}
}

View file

@ -6,8 +6,11 @@ use tracing::{event, Level};
pub fn migrate(con: &mut Connection) -> Result<(), Error>{
let version: u32 = con.pragma_query_value(None,"user_version", |row| row.get(0)).expect("Failed to get user_version");
if version < 1 {
let tx = con.transaction()?;
event!(Level::INFO, "Migrate local db to version 1");
v1::migrate(con)?;
v1::migrate(&tx)?;
tx.pragma_update(None, "user_version", 1)?;
tx.commit()?;
event!(Level::INFO, "Migration done.");
}
Ok(())

View file

@ -1,27 +1,45 @@
use rusqlite::{Error, Connection};
use rusqlite::{Connection, Error, Transaction};
pub fn migrate(con: &mut Connection) -> Result<(), Error>{
let tx = con.transaction()?;
pub fn migrate(tx: &Transaction) -> Result<(), Error>{
tx.execute_batch(
"BEGIN;
CREATE TABLE authorized_peer (
id INTEGER PRIMARY KEY,
node_id BLOB NOT NULL UNIQUE,
last_synced_at TEXT,
last_sent_version_vector BLOB
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
);
CREATE TABLE authorization (
"CREATE TABLE remote_node (
id INTEGER PRIMARY KEY,
node_id BLOB UNIQUE NOT NULL,
passcode TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
public_id INTEGER NOT NULL UNIQUE,
public_key BLOB UNIQUE NOT NULL
);
COMMIT;",
CREATE TABLE authorization_request (
id INTEGER PRIMARY KEY,
uuid BLOB NOT NULL UNIQUE,
public_id INTEGER NOT NULL UNIQUE,
remote_node_id INTEGER NOT NULL UNIQUE,
created_at TEXT NOT NULL,
closed_at TEXT,
FOREIGN KEY(remote_node_id) REFERENCES remote_node(id)
);
CREATE TABLE received_authorization_request (
id INTEGER PRIMARY KEY,
authorization_request_id INTEGER NOT NULL UNIQUE,
node_note TEXT,
FOREIGN KEY(authorization_request_id) REFERENCES authorization_request(id)
);
CREATE TABLE sent_authorization_request (
id INTEGER PRIMARY KEY,
authorization_request_id INTEGER NOT NULL UNIQUE,
passcode TEXT NOT NULL,
FOREIGN KEY(authorization_request_id) REFERENCES authorization_request(id)
);
CREATE TABLE authorized_remote_node (
id INTEGER PRIMARY KEY,
uuid BLOB UNIQUE NOT NULL,
public_id INTEGER NOT NULL UNIQUE,
public_key BLOB NOT NULL UNIQUE,
note TEXT NOT NULL,
last_synced_at TEXT,
last_sent_version_vector BLOB,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);",
)?;
tx.pragma_update(None, "user_version", 1)?;
tx.commit()?;
Ok(())
}

View file

@ -1,27 +1,121 @@
mod authorization;
// mod authorization_request;
mod remote_node;
pub use remote_node::RemoteNodeRecord;
pub mod migration;
use std::{cell::OnceCell, iter::Map, path::Path, sync::{LazyLock, OnceLock}};
use std::{cell::OnceCell, convert::Infallible, iter::Map, path::Path, sync::{LazyLock, OnceLock}};
use migration::migrate;
use rusqlite::{ffi::Error, Connection, MappedRows, Row};
use rusqlite::{ffi::Error, params, types::FromSql, Connection, MappedRows, OptionalExtension, Params, Row, ToSql};
use crate::{config::StorageConfig, global::{CONFIG, LOCAL_DATABASE_CONNECTION}};
pub use authorization::*;
// pub use authorization_request::*;
pub type LocalRecordError = rusqlite::Error;
pub trait RusqliteRecord: Sized {
fn insert(&self, connection: &Connection) -> Result<(), rusqlite::Error>;
/// a struct of id for local database record.
pub type LocalRecordId = u32;
/// a struct for the record without id before inserted
pub type NoLocalRecordId = rusqlite::types::Null;
/// A id struct
/// Model trait for local database data.
/// use LOCAL_DATABASE_CONNECTION for database connection.
pub trait LocalRecord: Sized {
const TABLE_NAME: &str;
const COLUMNS: &[&str];
/// Tuple form of the record.
/// the order of field must be same as COLUMNS.
type RowValues;
}
pub trait SelectableLocalRecord: LocalRecord<RowValues: TryInto<Self>> {
const SELECT_STATEMENT: LazyLock<String> = LazyLock::new(|| {
String::from("SELECT ") + &Self::COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME
});
const SELECT_PLACEHOLDER: LazyLock<String> = LazyLock::new(|| {
let mut result : Vec<String> = Vec::new();
for i in 0..Self::COLUMNS.len() {
result.push(String::from("?") + &(i+1).to_string());
}
result.join(", ")
});
fn get_one_where<P>(where_statement: &str, params: P) -> Result<Self, rusqlite::Error>
where P: Params
{
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Ok(connection.query_row(
&(String::new() + &Self::SELECT_STATEMENT + " " + where_statement),
params,
Self::from_row
)?)
}
fn get_one_by_field<T>(field_name: &str, field_value: T) -> Result<Self, rusqlite::Error>
where
T: ToSql
{
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Ok(connection.query_row(
&([&Self::SELECT_STATEMENT, "FROM", Self::TABLE_NAME, "WHERE", field_name,"= ?1"].join(" ")),
params![field_value],
Self::from_row
)?)
}
fn get_one_by_id(id: u32) -> Result<Self, rusqlite::Error> {
Self::get_one_by_field("id", id )
}
fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error>;
fn get_all(connection: &Connection) -> Result<Vec<Self>, rusqlite::Error>;
fn get_all() -> Result<Vec<Self>, rusqlite::Error> {
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
let mut stmt = connection.prepare(&("SELECT ".to_string() + &Self::COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME))?;
let rows = stmt.query_map(
[],
Self::from_row
)?;
let mut result= Vec::new();
for row in rows {
result.push(row?);
}
Ok(result)
}
}
pub trait LocalRecord : RusqliteRecord{
fn insert_global(&self) -> Result<(), rusqlite::Error> {
self.insert(&LOCAL_DATABASE_CONNECTION.get_unchecked())
}
fn get_all_global() -> Result<Vec<Self>, rusqlite::Error> {
pub trait InsertableLocalRecord: LocalRecord<RowValues: From<Self> + Params> {
type LocalRecord: Sized + SelectableLocalRecord;
/// Place holder for insertion.
/// Generated from Columns
const INSERT_PLACEHOLDER: LazyLock<String> = LazyLock::new(|| {
let mut result : Vec<String> = Vec::new();
for i in 0..Self::COLUMNS.len() {
result.push(String::from("?") + &(i+1).to_string());
}
result.join(", ")
});
/// Insert and get the inserted record.
fn insert(self) -> Result<Self::LocalRecord, rusqlite::Error>{
let params= Self::RowValues::from(self);
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Self::get_all(&connection)
Ok(connection.query_row(
&[
"INSERT INTO ", Self::TABLE_NAME, "(" , &Self::COLUMNS.join(", "), ")",
"VALUES (" , &*Self::INSERT_PLACEHOLDER , ")",
"RETURNING", &Self::COLUMNS.join(", ")
].join(" "),
params,
Self::LocalRecord::from_row
)?)
}
}

View file

@ -0,0 +1,120 @@
//! Structs about cached remote_node.
use std::os::unix::raw::time_t;
use tripod_id::Double;
use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey};
use rusqlite::{params, types::{FromSqlError, Null}, Connection};
use uuid::Uuid;
use crate::{data::local::{self, InsertableLocalRecord, LocalRecord, LocalRecordId, NoLocalRecordId, SelectableLocalRecord}, global::LOCAL_DATABASE_CONNECTION};
/// RemoteNode information cached in local database.
///
/// - Currently this only contain local uid and public key (=node id) of iroh.
/// - This is a junction table enable to use caretta-id to specify items in the UI, especially on the CLI.
/// - Actual remote_node information is managed by iroh endpoint and not contained in this model.
/// - Once a remote_node is authorized, it is assigned a global (=synced) ID as authorized_remote_node so essentially this local id targets unauthorized remote_nodes.
///
#[derive(Clone, Debug, PartialEq)]
pub struct RemoteNodeRecord<T> {
/// serial primary key.
pub id: T,
/// public tripod id of remote_node.
/// this id is use only the node itself and not synced so another node has different local_remote_node_id even if its public_key is same.
pub public_id: Double,
/// Iroh public key
pub public_key: PublicKey,
}
impl RemoteNodeRecord<LocalRecordId> {
pub fn get_or_insert_by_public_key(public_key: &PublicKey) -> Result<Self, rusqlite::Error> {
match Self::get_by_public_key(public_key) {
Ok(x) => Ok(x),
Err(rusqlite::Error::QueryReturnedNoRows) => {
let new = RemoteNodeRecord{
id: NoLocalRecordId{},
public_id: rand::random(),
public_key: public_key.clone()
};
Ok(new.insert()?)
},
Err(e) => Err(e)
}
}
pub fn get_by_public_id(public_id: &Double) -> Result<Self, rusqlite::Error> {
Self::get_one_where("WHERE public_id = ?1", (public_id,))
}
pub fn get_by_public_key(public_key: &PublicKey) -> Result<Self, rusqlite::Error> {
Self::get_one_where("WHERE public_Key = ?1", (public_key.as_bytes(),))
}
}
impl<T> LocalRecord for RemoteNodeRecord<T> {
const TABLE_NAME: &str = "remote_node";
const COLUMNS: &[&str] = &[
"id",
"public_id",
"public_key"
];
type RowValues = (T, Double, [u8;32]);
}
impl SelectableLocalRecord for RemoteNodeRecord<LocalRecordId> {
fn from_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self {
id: row.get(0)?,
public_id: row.get(1)?,
public_key: PublicKey::from_bytes(&row.get(2)?).map_err(|e| FromSqlError::Other(Box::new(e)))?
})
}
}
impl TryFrom<(LocalRecordId, Double, [u8;32])> for RemoteNodeRecord<LocalRecordId> {
type Error = rusqlite::Error;
fn try_from(value: (LocalRecordId, Double, [u8;32])) -> Result<Self, Self::Error> {
Ok(Self {
id: value.0,
public_id: value.1,
public_key: PublicKey::from_bytes(&value.2).map_err(|x| FromSqlError::Other(Box::new(x)))?
})
}
}
impl InsertableLocalRecord for RemoteNodeRecord<NoLocalRecordId> {
type LocalRecord = RemoteNodeRecord<LocalRecordId>;
}
impl From<RemoteNodeRecord<NoLocalRecordId>> for (NoLocalRecordId, Double, [u8;32]){
fn from(value: RemoteNodeRecord<NoLocalRecordId>) -> Self {
(value.id, value.public_id, value.public_key.as_bytes().to_owned())
}
}
#[cfg(test)]
mod tests {
use iroh::SecretKey;
use crate::tests::TEST_CONFIG;
use super::*;
#[test]
fn insert_get_remote_node_record() {
LOCAL_DATABASE_CONNECTION.get_or_init(&TEST_CONFIG.storage.get_local_database_path());
let key = SecretKey::generate(&mut rand::rngs::OsRng);
let pubkey = key.public();
let record = RemoteNodeRecord::get_or_insert_by_public_key(&pubkey).unwrap();
assert_eq!(record, RemoteNodeRecord::get_by_public_id(&record.public_id).unwrap());
assert_eq!(record, RemoteNodeRecord::get_by_public_key(&record.public_key).unwrap());
}
}

View file

@ -1,4 +1,7 @@
use std::{array::TryFromSliceError, ffi::OsString};
use tonic::Status;
use crate::proto::ProtoDeserializeError;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@ -31,10 +34,30 @@ pub enum Error {
TomlDe(#[from] toml::de::Error),
#[error("toml serialization error: {0}")]
TomlSer(#[from] toml::ser::Error),
#[error("protobuf serialization error: {0}")]
ProtoSerialize(#[from] crate::proto::ProtoSerializeError),
#[error("protobuf deserialization error: {0}")]
ProtoDeserialize(#[from] crate::proto::ProtoDeserializeError),
#[error("Local record error: {0}")]
LocalRecord(#[from] crate::data::local::LocalRecordError),
#[error("Tripod id error: {0}")]
TripodId(#[from] tripod_id::Error),
}
impl From<std::ffi::OsString> for Error {
fn from(s: OsString) -> Error {
Self::OsStringConvert(s)
}
}
impl From<Error> for Status {
fn from(value: Error) -> Self {
match value {
Error::ProtoDeserialize(x) => { match x {
ProtoDeserializeError::MissingField(x) => Self::invalid_argument(format!("{} is required", x)),
_ => Status::unimplemented("Unimplemented protobuf deserialize error status")
}},
_ => Status::unimplemented("Unimplemented error status")
}
}
}

View file

@ -0,0 +1,27 @@
use std::pin::Pin;
use futures::Stream;
use tonic::{Request, Response, Streaming};
tonic::include_proto!("caretta_sync.authorization_request");
pub struct AuthorizationRequestService {}
#[tonic::async_trait]
impl authorization_request_server::AuthorizationRequest for AuthorizationRequestService {
type ListStream = Pin<Box<dyn Stream<Item = Result<ListResponse, tonic::Status>> + Send>>;
async fn send(&self, request: Request<SendRequest>) -> Result<Response<SendResponse>, tonic::Status> {
todo!()
}
async fn accept(&self, request: Request<AcceptRequest>) -> Result<Response<AcceptResponse>, tonic::Status>{
todo!()
}
async fn reject(&self, request: Request<RejectRequest>) -> Result<Response<RejectResponse>, tonic::Status>{
todo!()
}
async fn info(&self, request: Request<InfoRequest>) -> Result<Response<InfoResponse>, tonic::Status>{
todo!()
}
async fn list(&self, request: Request<Streaming<ListRequest>>) -> Result<Response<Self::ListStream>, tonic::Status> {
todo!()
}
}

View file

@ -0,0 +1,19 @@
use std::pin::Pin;
use futures::Stream;
use tonic::{Request, Response, Streaming, Status};
tonic::include_proto!("caretta_sync.authorized_node");
pub struct AuthorizedNodeService {}
#[tonic::async_trait]
impl authorized_node_server::AuthorizedNode for AuthorizedNodeService {
type ListStream = Pin<Box<dyn Stream<Item = Result<ListResponse, tonic::Status>> + Send>>;
async fn info(&self, request: Request<InfoRequest>) -> Result<Response<InfoResponse>, tonic::Status>{
todo!()
}
async fn list(&self, request: Request<Streaming<ListRequest>>) -> Result<Response<Self::ListStream>, tonic::Status> {
todo!()
}
}

54
core/src/proto/common.rs Normal file
View file

@ -0,0 +1,54 @@
use super::*;
tonic::include_proto!("caretta_sync.common");
use crate::proto::{error::{ProtoDeserializeError, ProtoSerializeError}};
impl From<uuid::Uuid> for Uuid {
fn from(value: uuid::Uuid) -> Self {
let (first_half, second_half) = value.as_u64_pair();
Self {
high_bits: first_half,
low_bits: second_half
}
}
}
impl From<Uuid> for uuid::Uuid {
fn from(value: Uuid) -> Self {
uuid::Uuid::from_u64_pair(value.high_bits, value.low_bits)
}
}
impl From<url::Url> for Url {
fn from(value: url::Url) -> Self {
todo!()
}
}
impl TryFrom<Url> for url::Url {
type Error = ProtoDeserializeError;
fn try_from(value: Url) -> Result<Self, Self::Error> {
todo!()
}
}
#[cfg(test)]
mod tests {
use std::{net::{self, Ipv4Addr}, u16};
use super::*;
fn validate_uuid_conversion(uuid: uuid::Uuid) -> bool{
let message = Uuid::from(uuid);
uuid == uuid::Uuid::from(message)
}
#[test]
fn uuid_conversion() {
assert!(validate_uuid_conversion(uuid::Uuid::nil()));
assert!(validate_uuid_conversion(uuid::Uuid::max()));
assert!(validate_uuid_conversion(uuid::Uuid::now_v7()));
}
}

View file

@ -1,17 +0,0 @@
use iroh::NodeId;
use crate::proto::{error::{ProtoDeserializeError, ProtoSerializeError}, NodeIdMessage};
impl From<NodeId> for NodeIdMessage {
fn from(value: NodeId) -> Self {
NodeIdMessage { node_id: Vec::from(value.as_bytes()) }
}
}
impl TryFrom<NodeIdMessage> for NodeId {
type Error = ProtoDeserializeError;
fn try_from(value: NodeIdMessage) -> Result<Self, Self::Error> {
let slice: [u8; 32] = value.node_id[0..32].try_into()?;
Ok(NodeId::from_bytes(&slice)?)
}
}

View file

@ -1,7 +0,0 @@
use crate::proto::RemoteInfoIterRequest;
impl RemoteInfoIterRequest {
pub fn new() -> Self {
Self{}
}
}

View file

@ -1,11 +0,0 @@
use iroh::NodeId;
use crate::proto::{error::ProtoDeserializeError, NodeIdMessage, RemoteInfoRequest};
impl From<NodeIdMessage> for RemoteInfoRequest {
fn from(value: NodeIdMessage) -> Self {
Self {
node_id : Some(value)
}
}
}

View file

@ -1,16 +0,0 @@
use crate::{ proto::{RemoteInfoMessage, RemoteInfoResponse}};
impl From<RemoteInfoMessage> for RemoteInfoResponse {
fn from(value: RemoteInfoMessage) -> Self {
Self {
remote_info: Some(value)
}
}
}
impl From<Option<RemoteInfoMessage>> for RemoteInfoResponse {
fn from(value: Option<RemoteInfoMessage>) -> Self {
Self{
remote_info: value,
}
}
}

View file

@ -1,16 +0,0 @@
use std::time::Duration;
use iroh::endpoint::Source;
use crate::{error::Error, proto::{error::ProtoSerializeError, SourceMessage}};
impl TryFrom<(Source, Duration)> for SourceMessage {
type Error = ProtoSerializeError;
fn try_from(src: (Source, Duration)) -> Result<Self, Self::Error> {
let (source, duration )= src;
Ok(Self {
source: source.to_string(),
duration: Some(duration.try_into()?),
})
}
}

View file

@ -12,4 +12,6 @@ pub enum ProtoDeserializeError {
Signature(#[from] ed25519_dalek::SignatureError),
#[error("slice parse error: {0}")]
SliceTryFrom(#[from] std::array::TryFromSliceError),
#[error("Int parse error: {0}")]
IntTryFrom(#[from] std::num::TryFromIntError),
}

120
core/src/proto/iroh.rs Normal file
View file

@ -0,0 +1,120 @@
use std::pin::Pin;
use futures::Stream;
use tonic::{async_trait, Request, Response, Status};
use crate::proto::{net::SocketAddr, remote_node_server, ProtoDeserializeError, ProtoSerializeError, };
tonic::include_proto!("caretta_sync.iroh");
impl From<iroh::endpoint::ConnectionType> for ConnectionType {
fn from(value: iroh::endpoint::ConnectionType) -> Self {
use connection_type::*;
Self {
connection_type_value: Some(match value {
iroh::endpoint::ConnectionType::Direct(socket_addr) => {
connection_type::ConnectionTypeValue::Direct(connection_type::Direct{direct_value: Some(SocketAddr::from(socket_addr))})
},
iroh::endpoint::ConnectionType::Relay(relay_url) => {
connection_type::ConnectionTypeValue::Relay(connection_type::Relay { relay_value: Some(super::common::Url::from((*relay_url).clone()))})
},
iroh::endpoint::ConnectionType::Mixed(socket_addr, relay_url) => {
connection_type::ConnectionTypeValue::Mixed(connection_type::Mixed { socket_addr: Some(SocketAddr::from(socket_addr)), relay_url: Some(super::common::Url::from((*relay_url).clone()))})
},
iroh::endpoint::ConnectionType::None => {
ConnectionTypeValue::None(None{})
}
})
}
}
}
impl From<iroh::endpoint::ControlMsg> for ControlMsg {
fn from(value: iroh::endpoint::ControlMsg) -> Self {
use control_msg::*;
Self { control_msg_vaue: Some(match value {
iroh::endpoint::ControlMsg::Ping => ControlMsgVaue::Ping(Ping{}),
iroh::endpoint::ControlMsg::Pong => ControlMsgVaue::Pong(Pong {}),
iroh::endpoint::ControlMsg::CallMeMaybe => ControlMsgVaue::CallMeMaybe(CallMeMayBe { }),
}) }
}
}
impl TryFrom<iroh::endpoint::DirectAddrInfo> for DirectAddrInfo {
type Error = ProtoSerializeError;
fn try_from(value: iroh::endpoint::DirectAddrInfo) -> Result<Self, Self::Error> {
use direct_addr_info::*;
let last_control: Option<DurationControlMsg> = if let Some((duration, control_msg)) = value.last_control {
Some(DurationControlMsg{
control_msg: Some(control_msg.into()),
duration: Some(duration.try_into()?)
})
} else {
None
};
Ok(Self {
addr: Some(value.addr.into()),
latency: value.latency.map(|x| x.try_into()).transpose()?,
last_control: last_control,
last_payload: value.last_payload.map(|x| x.try_into()).transpose()?,
last_alive: value.last_alive.map(|x| x.try_into()).transpose()?,
sources: value.sources.into_iter().map(|(s, d)| {
Ok::<SourceDuration, ProtoSerializeError>(SourceDuration{
source: Some(s.into()),
duration: Some(d.try_into()?)
})
}).collect::<Result<Vec<SourceDuration>, ProtoSerializeError>>()?,
})
}
}
impl From<iroh::PublicKey> for PublicKey {
fn from(value: iroh::PublicKey) -> Self {
Self{ key: Vec::from(value.as_bytes()) }
}
}
impl TryFrom<PublicKey> for iroh::PublicKey {
type Error = ProtoDeserializeError;
fn try_from(value: PublicKey) -> Result<Self, Self::Error> {
let slice: [u8; 32] = value.key[0..32].try_into()?;
Ok(iroh::PublicKey::from_bytes(&slice)?)
}
}
impl TryFrom<iroh::endpoint::RemoteInfo> for RemoteInfo {
type Error = ProtoSerializeError;
fn try_from(value: iroh::endpoint::RemoteInfo) -> Result<Self, Self::Error> {
Ok(Self {
node_id: Some(value.node_id.into()),
relay_url: value.relay_url.map(|x| {
Ok::<RelayUrlInfo, ProtoSerializeError>(RelayUrlInfo {
relay_url: Some((*x.relay_url).clone().into()),
last_alive: x.last_alive.map(|x| x.try_into()).transpose()?,
latency: x.latency.map(|x| x.try_into()).transpose()?
})}).transpose()?,
addrs: value.addrs.into_iter().map(|x| {
x.try_into()
}).collect::<Result<Vec<DirectAddrInfo>, ProtoSerializeError>>()?,
conn_type: Some(value.conn_type.into()),
latency: value.latency.map(|x| x.try_into()).transpose()?,
last_used:value.last_used.map(|x| x.try_into()).transpose()?
})
}
}
impl From<iroh::endpoint::Source> for Source {
fn from(value: iroh::endpoint::Source) -> Self {
use source::*;
Self {
source_value:Some(match value {
iroh::endpoint::Source::Saved => SourceValue::Saved(Saved { }),
iroh::endpoint::Source::Udp => SourceValue::Udp(Udp { }),
iroh::endpoint::Source::Relay => SourceValue::Relay(Relay { }),
iroh::endpoint::Source::App => SourceValue::App(App{}),
iroh::endpoint::Source::Discovery { name } => SourceValue::Discovery(Discovery { value: name }),
iroh::endpoint::Source::NamedApp { name } => SourceValue::NamedApp(NamedApp { value: name }),
}) }
}
}

View file

@ -1,5 +1,12 @@
mod convert;
mod authorization_request;
mod authorized_node;
mod remote_node;
mod common;
mod error;
mod server;
mod iroh;
mod net;
tonic::include_proto!("caretta_sync");
pub use common::*;
pub use error::*;
pub use remote_node::*;

140
core/src/proto/net.rs Normal file
View file

@ -0,0 +1,140 @@
tonic::include_proto!("caretta_sync.net");
use crate::proto::{error::{ProtoDeserializeError, ProtoSerializeError}};
type Ipv4AddrMessage = Ipv4Addr;
type Ipv6AddrMessage = Ipv6Addr;
type SocketAddrMessage = SocketAddr;
type SocketAddrV4Message = SocketAddrV4;
type SocketAddrV6Message = SocketAddrV6;
impl From<std::net::SocketAddr> for SocketAddrMessage {
fn from(value: std::net::SocketAddr) -> Self {
Self{
socket_addr_value: Some(match value {
std::net::SocketAddr::V4(x) => socket_addr::SocketAddrValue::V4(SocketAddrV4Message::from(x)),
std::net::SocketAddr::V6(x) => socket_addr::SocketAddrValue::V6(SocketAddrV6Message::from(x)),
})}
}
}
impl TryFrom<SocketAddrMessage> for std::net::SocketAddr {
type Error = ProtoDeserializeError;
fn try_from(value: SocketAddrMessage) -> Result<Self, Self::Error> {
Ok(match value.socket_addr_value.ok_or(Self::Error::MissingField("SocketAddr.socket_addr"))? {
socket_addr::SocketAddrValue::V4(x) => std::net::SocketAddr::V4(x.try_into()?),
socket_addr::SocketAddrValue::V6(x) => std::net::SocketAddr::V6(x.try_into()?),
})
}
}
impl From<std::net::SocketAddrV4> for SocketAddrV4Message {
fn from(value: std::net::SocketAddrV4) -> Self {
Self {
ip : Some(value.ip().clone().into()),
port: value.port().into(),
}
}
}
impl TryFrom<SocketAddrV4Message> for std::net::SocketAddrV4 {
type Error = ProtoDeserializeError;
fn try_from(value: SocketAddrV4Message) -> Result<Self, Self::Error> {
Ok(Self::new(value.ip.ok_or(ProtoDeserializeError::MissingField("SocketAddrV4.ip"))?.into(), value.port.try_into()?))
}
}
impl From<std::net::Ipv4Addr> for Ipv4AddrMessage {
fn from(value: std::net::Ipv4Addr) -> Self {
Self{
bits: value.to_bits()
}
}
}
impl From<Ipv4AddrMessage> for std::net::Ipv4Addr {
fn from(value: Ipv4AddrMessage) -> Self{
Self::from_bits(value.bits)
}
}
impl From<std::net::SocketAddrV6> for SocketAddrV6Message {
fn from(value: std::net::SocketAddrV6) -> Self {
Self{
ip: Some(value.ip().clone().into()),
port: value.port().into()
}
}
}
impl TryFrom<SocketAddrV6Message> for std::net::SocketAddrV6 {
type Error = ProtoDeserializeError;
fn try_from(value: SocketAddrV6Message) -> Result<Self, Self::Error> {
Ok(Self::new(
value.ip.ok_or(ProtoDeserializeError::MissingField("SocketAddrV6.ip"))?.into(),
value.port.try_into()?,
0,
0
))
}
}
impl From<std::net::Ipv6Addr> for Ipv6AddrMessage {
fn from(value: std::net::Ipv6Addr) -> Self {
let bits = value.to_bits();
Self{
high_bits: (bits >> 64) as u64,
low_bits: bits as u64,
}
}
}
impl From<Ipv6AddrMessage> for std::net::Ipv6Addr{
fn from(value: Ipv6AddrMessage) -> Self {
Self::from_bits(
((value.high_bits as u128) << 64) + (value.low_bits as u128)
)
}
}
#[cfg(test)]
mod tests {
use std::{net::{self, Ipv4Addr}, u16, u8};
use rand::random;
use super::*;
fn validate_socket_addr_conversion(socket_addr: net::SocketAddr) -> Result<bool, ProtoDeserializeError> {
let message = SocketAddrMessage::from(socket_addr);
Ok(socket_addr == message.try_into()?)
}
#[test]
fn socket_addr_conversion_ipv4_min() {
assert!(validate_socket_addr_conversion(net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(0, 0, 0, 0)),u16::MIN)).unwrap());
}
#[test]
fn socket_addr_conversion_ipv4_max() {
assert!(validate_socket_addr_conversion(net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(u8::MAX, u8::MAX, u8::MAX, u8::MAX)),u16::MAX)).unwrap());
}
#[test]
fn socket_addr_conversion_ipv4_random() {
for _ in 0..10 {
assert!(validate_socket_addr_conversion(net::SocketAddr::new(net::IpAddr::V4(
net::Ipv4Addr::new(random(), random(), random(), random())
),
random()
)).unwrap())
}
}
#[test]
fn socket_addr_conversion_ipv6_min() {
assert!(validate_socket_addr_conversion(net::SocketAddr::new(net::IpAddr::V6(net::Ipv6Addr::new(0,0,0,0,0,0,0,0)), u16::MIN)).unwrap());
}
#[test]
fn socket_addr_conversion_ipv6_max() {
assert!(validate_socket_addr_conversion(net::SocketAddr::new(net::IpAddr::V6(net::Ipv6Addr::new(u16::MAX, u16::MAX, u16::MAX, u16::MAX, u16::MAX, u16::MAX, u16::MAX, u16::MAX)), u16::MAX)).unwrap());
}
}

View file

@ -0,0 +1,31 @@
use std::{pin::Pin, time::Duration};
use futures::{future::Remote, Stream};
use iroh::{endpoint::{DirectAddrInfo, RemoteInfo}, PublicKey};
use tonic::{Request, Response, Status, Streaming};
use tripod_id::Double;
use crate::{data::local::{LocalRecordId, RemoteNodeRecord}, error::Error, global::IROH_ENDPOINT, proto::{error::{ProtoDeserializeError, ProtoSerializeError}}};
tonic::include_proto!("caretta_sync.remote_node");
pub struct RemoteNodeServer{}
#[tonic::async_trait]
impl remote_node_server::RemoteNode for RemoteNodeServer {
type ListStream = Pin<Box<dyn Stream<Item = Result<ListResponse, Status>> + Send>>;
async fn info(&self, request: Request<InfoRequest>) -> Result<Response<InfoResponse>, Status> {
todo!()
}
async fn list(&self, request: Request<Streaming<ListRequest>>)
-> Result<Response<Self::ListStream>, Status> {
let iter = IROH_ENDPOINT.get_unchecked().remote_info_iter()
.map(|x| {
todo!();
});
let stream = futures::stream::iter(iter);
Ok(Response::new(Box::pin(stream)))
}
}

View file

@ -4,7 +4,7 @@ use caretta_sync::{
config::P2pConfig,
proto::cached_peer_service_server::CachedPeerServiceServer,
server::ServerTrait,
rpc::service::cached_peer::CachedPeerService
rpc::service::iroh::CachedPeerService
};
use libp2p::{futures::StreamExt, noise, swarm::SwarmEvent, tcp, yamux};
use tokio::net::UnixListener;

27
tripod-id/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "tripod-id"
edition.workspace = true
version = "0.1.0-alpha"
description.workspace = true
license.workspace = true
repository.workspace = true
[features]
default=[]
prost = ["dep:prost", "dep:prost-build"]
rusqlite = ["dep:rusqlite"]
serde = ["dep:serde"]
[dependencies]
prost = { workspace = true, optional = true }
rand.workspace = true
rusqlite = {workspace = true, optional = true}
serde = {workspace = true, optional = true}
thiserror.workspace = true
[build-dependencies]
prost-build = {version = "0.14.1", optional = true}
[dev-dependencies]
serde_test = "1.0.177"

22
tripod-id/README.md Normal file
View file

@ -0,0 +1,22 @@
# Tripod ID
Distributable user-friendly id.
## Examples
- `123` : shortest version
- `456-789` : default size, still user freindly and sufficient randomness (for personal data)
- `abc-def-ghj` : long version. alphabets except i, l and o are also valid 
## Specs
### Characters
## Perpose
When I considering implementing IDs for users(not for internal system) to specify items, such as GitHub commit hashes or issue numbers, the following issues arose.
- Sequential numbers like Git issues are difficult to implement in distributes systems because collitions are unavoidable.
- Random number like UUID is too long for users
- Short random number like 7-digit commit hash seems good but is is not standardized specification.
So I decided to make my own ID specifications.

12
tripod-id/build.rs Normal file
View file

@ -0,0 +1,12 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature="prost")]
prost_build::compile_protos(
&[
"proto/tripod_id/single.proto",
"proto/tripod_id/double.proto",
"proto/tripod_id/triple.proto"
],
&["proto/"]
)?;
Ok(())
}

View file

@ -0,0 +1,7 @@
syntax = "proto3";
package tripod_id;
// Double size tripod id message
message Double {
uint32 id = 1;
}

View file

@ -0,0 +1,7 @@
syntax = "proto3";
package tripod_id;
// Single size tripod id message
message Single {
uint32 id = 1;
}

View file

@ -0,0 +1,7 @@
syntax = "proto3";
package tripod_id;
// Triple size tripod id message
message Triple {
uint64 id = 1;
}

168
tripod-id/src/double.rs Normal file
View file

@ -0,0 +1,168 @@
use std::{fmt::Display, str::FromStr};
use rand::{distributions::Standard, prelude::Distribution, Rng};
#[cfg(feature="prost")]
use crate::DoubleMessage;
use crate::{utils::is_delimiter, Error, TripodId, Single};
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
pub struct Double(u32);
impl TripodId for Double{
type Tuple = (Single, Single);
type Integer = u32;
#[cfg(feature="prost")]
type Message = DoubleMessage;
const CAPACITY: Self::Integer = (Single::CAPACITY as u32).pow(2);
const NIL: Self = Self(0);
const MAX: Self = Self(Self::CAPACITY -1);
#[cfg(test)]
fn validate_inner(self) -> bool {
self.0 < Self::CAPACITY
}
}
impl Display for Double {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let tuple: (Single, Single) = (*self).into();
write!(f, "{}-{}", tuple.0, tuple.1)
}
}
impl From<(Single, Single)> for Double {
fn from(value: (Single, Single)) -> Self {
Self(u32::from(u16::from(value.0)) * u32::from(Single::CAPACITY) + u32::from(u16::from(value.1)))
}
}
impl From<Double> for (Single, Single) {
fn from(value: Double) -> Self {
(
Single::try_from(u16::try_from(value.0/(Single::CAPACITY as u32)).unwrap()).unwrap(),
Single::try_from(u16::try_from(value.0 % (Single::CAPACITY as u32)).unwrap()).unwrap()
)
}
}
impl FromStr for Double {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let tuple = match s.len() {
7 => {
let delimiter = s[3..4].chars().next().unwrap();
if is_delimiter(delimiter) {
Ok((Single::from_str(&s[0..3])?,Single::from_str(&s[4..7])?))
} else {
Err(Error::InvalidDelimiter{
found: vec![delimiter],
raw: s.to_string()
})
}
},
6 => {
Ok((Single::from_str(&s[0..3])?,Single::from_str(&s[3..6])?))
},
x => {
Err(Error::InvalidLength{
expected: (6, 7),
found: x,
raw: s.to_string()
})
}
}?;
Ok(Self::from(tuple))
}
}
impl Distribution<Double> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Double {
Double(rng.gen_range(0..Double::CAPACITY))
}
}
impl TryFrom<u32> for Double {
type Error = Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value < Self::CAPACITY {
Ok(Self(value))
} else {
Err(Error::OutsideOfRange{
expected: Self::CAPACITY as u64,
found: value as u64
})
}
}
}
impl From<Double> for u32 {
fn from(value: Double) -> Self {
value.0
}
}
impl PartialEq<u32> for Double {
fn eq(&self, other: &u32) -> bool {
&u32::from(*self) == other
}
}
impl PartialEq<String> for Double {
fn eq(&self, other: &String) -> bool {
match Self::from_str(other) {
Ok(x) => *self == x,
Err(_) => false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nil() {
assert!(Double::NIL.validate_all().unwrap());
assert_eq!(Double::NIL, 0);
assert_eq!(Double::NIL, "000000".to_string());
assert_eq!(Double::NIL, "000-000".to_string());
assert!(Double::NIL.is_nil());
assert!(!Double::NIL.is_max());
}
#[test]
fn max() {
assert!(Double::MAX.validate_all().unwrap());
assert_eq!(Double::MAX, Double::CAPACITY-1);
assert_eq!(Double::MAX, "zzzzzz".to_string());
assert_eq!(Double::MAX, "ZZZ-ZZZ".to_string());
assert!(Double::MAX.is_max());
assert!(!Double::MAX.is_nil());
}
#[test]
#[should_panic]
fn over_sized() {
Double::try_from(Double::CAPACITY).unwrap();
}
#[test]
fn random() {
let mut rng = rand::thread_rng();
for _ in 0..10 {
let id: Double = rng.r#gen();
assert!(id.validate_all().unwrap());
}
}
}

28
tripod-id/src/error.rs Normal file
View file

@ -0,0 +1,28 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("expected under {expected}, found {found}")]
OutsideOfRange{
expected: u64,
found: u64,
},
#[error("Invalid chunk: {0}")]
InvalidChunk(String),
#[error("Length of id expected {} or {} but found {found}: {raw}", .expected.0, .expected.1 )]
InvalidLength{
expected: (u8, u8),
found: usize,
raw: String
},
#[error("Number of chunks expected {expected} but {found}: {raw}")]
InvalidLengthOfChunks{
expected: u8,
found: usize,
raw: String,
},
#[error("Delimiter expected '-' or '_' but '{found:?}' found: {raw}")]
InvalidDelimiter{
found: Vec<char>,
raw: String,
}
}

128
tripod-id/src/lib.rs Normal file
View file

@ -0,0 +1,128 @@
mod single;
mod double;
mod error;
mod triple;
mod utils;
#[cfg(feature="rusqlite")]
mod rusqlite;
#[cfg(feature="serde")]
mod serde;
use std::{fmt::Display, ops::Sub, str::FromStr};
pub use single::*;
pub use double::*;
pub use triple::*;
pub use error::*;
#[cfg(feature="prost")]
pub mod prost;
#[cfg(feature="prost")]
pub use prost::{ SingleMessage, DoubleMessage, TripleMessage ,TripodIdMessage};
/// The main trait for the tripod id
pub trait TripodId: Copy + Display + TryFrom<Self::Integer, Error=Error> + From<Self::Tuple> + FromStr<Err=Error> + PartialEq + PartialEq<String> {
/// An associated integer type.
/// This type is used to store the actual value of id.
type Integer: From<Self> + Sub;
/// An associated tuple type containing SingleId.
/// This type is used to represent the id as the tuple of SingleId.
type Tuple: From<Self>;
/// An associated protobuf message type.
/// This type is used for conversion between the protobuf message.
#[cfg(feature="prost")]
type Message: From<Self> + TryInto<Self, Error=Error>;
/// The nil Tripod ID.
///
/// # Example
///
/// Basic usage:
///
/// ```
/// # use tripod_id::{TripodId, Single};
/// let id = Single::NIL;
///
/// assert_eq!(id, 0);
/// assert_eq!(id, "000".to_string());
/// ```
const NIL: Self;
/// The max Tripod ID.
///
/// # Example
///
/// Basic usage:
///
/// ```
/// # use tripod_id::{TripodId, Double};
/// let id = Double::MAX;
///
/// assert_eq!(id, Double::CAPACITY - 1);
/// assert_eq!(id, "ZZZ-ZZZ".to_string())
/// ```
const MAX: Self;
/// The capacity of the Tripod ID.
const CAPACITY: Self::Integer;
/// Test if the Tripod ID is nil (=0).
fn is_nil(self) -> bool {
self == Self::NIL
}
/// Test if the id is max(=Self::CAPACITY-1)
fn is_max(self) -> bool {
self == Self::MAX
}
/// Validate the internal value has not reached capacity.
/// Fundamentally, the internal value are private, and unvalidated value should not be enterd,
/// so this function is only for testing purpose.
#[cfg(test)]
fn validate_inner(self) -> bool;
#[cfg(test)]
fn validate_parse_strings(self, strings: &[&str]) -> Result<bool, Error> {
let mut result: bool = true;
for string in strings {
result = result && (self == string.to_string())
}
Ok(result)
}
#[cfg(test)]
fn validate_string_convertion(self) -> Result<bool, Error> {
Ok(self == Self::from_str(&self.to_string())?)
}
#[cfg(test)]
fn validate_integer_conversion(self) -> Result<bool, Error> {
Ok(self == Self::try_from(Self::Integer::from(self))?)
}
#[cfg(test)]
fn validate_tuple_conversion(self) -> bool {
self == Self::from(Self::Tuple::from(self))
}
#[cfg(all(test, feature="prost"))]
fn validate_message_conversion(self) -> Result<bool, Error> {
Ok(self == Self::Message::from(self).try_into()?)
}
#[cfg(test)]
fn validate_all(self) -> Result<bool, Error> {
let mut result = self.validate_inner()
&& self.validate_string_convertion()?
&& self.validate_integer_conversion()?;
#[cfg(feature="prost")]
{
result = result && self.validate_message_conversion()?;
}
Ok(result)
}
}

View file

@ -0,0 +1,53 @@
use prost::Name;
use crate::{prost::{Double, TripodIdMessage}, Error};
impl Name for Double {
const NAME: &'static str = "Double";
const PACKAGE: &'static str = super::PACKAGE_NAME;
}
impl TripodIdMessage for Double {
type TripodId = crate::Double;
}
impl From<crate::Double> for Double {
fn from(value: crate::Double) -> Self {
Self {
id: u32::from(value)
}
}
}
impl TryFrom<Double> for crate::Double {
type Error = Error;
fn try_from(value: Double) -> Result<Self, Self::Error> {
Self::try_from(
value.id
)
}
}
#[cfg(test)]
mod tests {
use crate::{Double, DoubleMessage, TripodId};
#[test]
fn nil() {
let nil = DoubleMessage{id: 0};
assert_eq!(Double::NIL, Double::try_from(nil).unwrap());
}
#[test]
fn max() {
let max = DoubleMessage{id: u32::from(Double::CAPACITY)-1};
assert_eq!(Double::MAX, Double::try_from(max).unwrap());
}
#[test]
#[should_panic]
fn oversized () {
let oversized = DoubleMessage{id: u32::from(Double::CAPACITY)};
let _ = Double::try_from(oversized).unwrap();
}
}

View file

@ -0,0 +1,26 @@
mod single;
mod double;
mod triple;
use crate::TripodId;
const PACKAGE_NAME: &'static str = "tripod_id";
include!(concat!(env!("OUT_DIR"), "/tripod_id.rs"));
/// Alias of single tripod-id message
pub type SingleMessage = Single;
/// Alias of double tripod-id message
pub type DoubleMessage = Double;
/// Alias of triple tripod-id message
pub type TripleMessage = Triple;
pub trait TripodIdMessage: From<Self::TripodId> {
type TripodId: TripodId + TryFrom<Self>;
fn is_valid(self) -> bool {
Self::TripodId::try_from(self).is_ok()
}
}

View file

@ -0,0 +1,57 @@
use prost::Name;
use crate::{prost::{Single, TripodIdMessage}, Error, TripodId};
impl Name for Single {
const NAME: &'static str = "Single";
const PACKAGE: &'static str = super::PACKAGE_NAME;
}
impl TripodIdMessage for Single {
type TripodId = crate::Single;
}
impl From<crate::Single> for Single {
fn from(value: crate::Single) -> Self {
Self {
id: u32::from(u16::from(value))
}
}
}
impl TryFrom<Single> for crate::Single {
type Error = Error;
fn try_from(value: Single) -> Result<Self, Self::Error> {
Self::try_from(
u16::try_from(value.id).or(Err(Error::OutsideOfRange {
expected: u64::from(crate::Single::CAPACITY),
found: u64::from(value.id)
}))?
)
}
}
#[cfg(test)]
mod tests {
use crate::{Single, SingleMessage, TripodId};
#[test]
fn nil() {
let nil = SingleMessage{id: 0};
assert_eq!(Single::NIL, Single::try_from(nil).unwrap());
}
#[test]
fn max() {
let max = SingleMessage{id: u32::from(Single::CAPACITY)-1};
assert_eq!(Single::MAX, Single::try_from(max).unwrap());
}
#[test]
#[should_panic]
fn oversized () {
let oversized = SingleMessage{id: u32::from(Single::CAPACITY)};
let _ = Single::try_from(oversized).unwrap();
}
}

View file

@ -0,0 +1,53 @@
use prost::Name;
use crate::{prost::{Triple, TripodIdMessage}, Error};
impl Name for Triple {
const NAME: &'static str = "Triple";
const PACKAGE: &'static str = super::PACKAGE_NAME;
}
impl TripodIdMessage for Triple{
type TripodId = crate::Triple;
}
impl From<crate::Triple> for Triple {
fn from(value: crate::Triple) -> Self {
Self {
id: u64::from(value)
}
}
}
impl TryFrom<Triple> for crate::Triple {
type Error = Error;
fn try_from(value: Triple) -> Result<Self, Self::Error> {
Self::try_from(
value.id
)
}
}
#[cfg(test)]
mod tests {
use crate::{Triple, TripleMessage, TripodId};
#[test]
fn nil() {
let nil = TripleMessage{id: 0};
assert_eq!(Triple::NIL, Triple::try_from(nil).unwrap());
}
#[test]
fn max() {
let max = TripleMessage{id: u64::from(Triple::CAPACITY)-1};
assert_eq!(Triple::MAX, Triple::try_from(max).unwrap());
}
#[test]
#[should_panic]
fn oversized () {
let oversized = TripleMessage{id: u64::from(Triple::CAPACITY)};
let _ = Triple::try_from(oversized).unwrap();
}
}

52
tripod-id/src/rusqlite.rs Normal file
View file

@ -0,0 +1,52 @@
use rusqlite::{types::FromSql, Error, ToSql};
use crate::{Double, Single, Triple};
impl FromSql for Single {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let int = u16::column_result(value)?;
Self::try_from(int).or_else(|e| {
Err(rusqlite::types::FromSqlError::Other(Box::new(e)))
})
}
}
impl ToSql for Single {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(u16::from(*self).into())
}
}
impl FromSql for Double {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let int = u32::column_result(value)?;
Self::try_from(int).or_else(|e| {
Err(rusqlite::types::FromSqlError::Other(Box::new(e)))
})
}
}
impl ToSql for Double {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(u32::from(*self).into())
}
}
impl FromSql for Triple {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let int = u64::column_result(value)?;
Self::try_from(int).or_else(|e| {
Err(rusqlite::types::FromSqlError::Other(Box::new(e)))
})
}
}
impl ToSql for Triple {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(rusqlite::types::Value::Integer(
i64::try_from(u64::from(*self)).map_err(
|err| Error::ToSqlConversionFailure(err.into())
)?
)))
}
}

78
tripod-id/src/serde.rs Normal file
View file

@ -0,0 +1,78 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize, de::Error};
use crate::{Double, Single, Triple};
impl Serialize for Single {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Single {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de> {
let s = String::deserialize(deserializer)?;
Single::from_str(&s).map_err(|e| D::Error::custom(e))
}
}
impl Serialize for Double {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Double {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de> {
let s = String::deserialize(deserializer)?;
Double::from_str(&s).map_err(|e| D::Error::custom(e))
}
}
impl Serialize for Triple {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Triple {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de> {
let s = String::deserialize(deserializer)?;
Triple::from_str(&s).map_err(|e| D::Error::custom(e))
}
}
#[cfg(test)]
mod tests {
use serde_test::{assert_tokens, Token};
use crate::TripodId;
#[test]
fn single() {
assert_tokens(&crate::Single::NIL, &[Token::Str("000")]);
}
#[test]
fn double() {
assert_tokens(&crate::Double::NIL, &[Token::Str("000-000")]);
}
#[test]
fn triple() {
assert_tokens(&crate::Triple::NIL, &[Token::Str("000-000-000")]);
}
}

244
tripod-id/src/single.rs Normal file
View file

@ -0,0 +1,244 @@
use std::{fmt::Display, str::FromStr};
use rand::{distributions::Standard, prelude::Distribution, Rng};
#[cfg(feature="prost")]
use crate::SingleMessage;
use crate::{error::Error, TripodId};
const CHARACTERS: &[u8;33] = b"0123456789abcdefghjkmnpqrstuvwxyz";
const BASE: u16 = 33;
const SQUARED_BASE: u16 = BASE.pow(2);
const CUBED_BASE: u16 = BASE.pow(3);
fn char_to_u8(c: char) -> Option<u8> {
Some(match c {
'0' => 0,
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'a' => 10,
'b' => 11,
'c' => 12,
'd' => 13,
'e' => 14,
'f' => 15,
'g' => 16,
'h' => 17,
'i' => 1,
'j' => 18,
'k' => 19,
'l' => 1,
'm' => 20,
'n' => 21,
'o' => 0,
'p' => 22,
'q' => 23,
'r' => 24,
's' => 25,
't' => 26,
'u' => 27,
'v' => 28,
'w' => 29,
'x' => 30,
'y' => 31,
'z' => 32,
'A' => 10,
'B' => 11,
'C' => 12,
'D' => 13,
'E' => 14,
'F' => 15,
'G' => 16,
'H' => 17,
'I' => 1,
'J' => 18,
'K' => 19,
'L' => 1,
'M' => 20,
'N' => 21,
'O' => 0,
'P' => 22,
'Q' => 23,
'R' => 24,
'S' => 25,
'T' => 26,
'U' => 27,
'V' => 28,
'W' => 29,
'X' => 30,
'Y' => 31,
'Z' => 32,
_ => return None
})
}
fn str_to_u16(s: &str) -> Result<u16, Error> {
if s.len() != 3 {
return Err(Error::InvalidChunk(format!("Chunk '{}' is not 3 characters", s)))
}
let mut buf : [u16;3] = [0;3];
for (i, c) in s.chars().enumerate() {
buf[i] = BASE.pow((2 - i) as u32) * (char_to_u8(c).ok_or(Error::InvalidChunk(format!("Invalid char: {}", c)))? as u16);
}
Ok(buf.iter().sum())
}
fn u16_to_string(int: u16) -> Result<String, Error> {
if int >= CUBED_BASE {
return Err(Error::OutsideOfRange{
expected: CUBED_BASE as u64,
found: int as u64
})
}
let first_char = char::from(CHARACTERS[usize::try_from(int / SQUARED_BASE).unwrap()]);
let second_char = char::from(CHARACTERS[usize::try_from((int % SQUARED_BASE)/ BASE).unwrap()]);
let third_char = char::from(CHARACTERS[usize::try_from(int % BASE).unwrap()]);
Ok(format!("{}{}{}", first_char, second_char, third_char))
}
/// Single size tripod id.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use tripod_id::{TripodId,Single};
///
/// assert_eq!(Single::from_str("012").unwrap(), Single::try_from(35).unwrap());
/// ```
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
pub struct Single(u16);
impl TripodId for Single {
type Integer = u16;
type Tuple = (Single,);
#[cfg(feature="prost")]
type Message = SingleMessage;
const CAPACITY: Self::Integer = CUBED_BASE;
const NIL: Single = Single(0);
const MAX: Single = Single(Self::CAPACITY-1);
#[cfg(test)]
fn validate_inner(self) -> bool {
self.0 < Self::CAPACITY
}
}
impl Display for Single {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", u16_to_string(self.0).unwrap())
}
}
impl FromStr for Single {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(str_to_u16(s)?))
}
}
impl Distribution<Single> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Single {
Single(rng.gen_range(0..Single::CAPACITY))
}
}
impl TryFrom<u16> for Single {
type Error = Error;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value < Self::CAPACITY {
Ok(Self(value))
} else {
Err(Error::OutsideOfRange{
expected: Self::CAPACITY as u64,
found: value as u64
})
}
}
}
impl From<Single> for u16 {
fn from(value: Single) -> Self {
value.0
}
}
impl From<(Single,)> for Single {
fn from(value: (Single,)) -> Self {
value.0
}
}
impl From<Single> for (Single,) {
fn from(value: Single) -> Self {
(value,)
}
}
impl PartialEq<u16> for Single {
fn eq(&self, other: &u16) -> bool {
&u16::from(*self) == other
}
}
impl PartialEq<String> for Single {
fn eq(&self, other: &String) -> bool {
match Self::from_str(other) {
Ok(x) => *self == x,
Err(_) => false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nil() {
assert!(Single::NIL.validate_all().unwrap());
assert_eq!(Single::NIL, 0);
assert!(Single::NIL.validate_parse_strings(&["000"]).unwrap());
assert!(Single::NIL.is_nil());
assert!(!Single::NIL.is_max())
}
#[test]
fn max() {
assert!(Single::MAX.validate_all().unwrap());
assert_eq!(Single::MAX, Single::CAPACITY - 1);
assert!(Single::MAX.validate_parse_strings(&["zzz", "ZZZ"]).unwrap());
assert!(Single::MAX.is_max());
assert!(!Single::MAX.is_nil());
}
#[test]
#[should_panic]
fn over_sized() {
Single::try_from(Single::CAPACITY).unwrap();
}
#[test]
fn random() {
let mut rng = rand::thread_rng();
for _ in 0..10 {
let single: Single = rng.r#gen();
assert!(single.validate_all().unwrap());
}
}
}

184
tripod-id/src/triple.rs Normal file
View file

@ -0,0 +1,184 @@
#[cfg(feature="prost")]
use crate::TripleMessage;
use crate::{utils::is_delimiter, Double, Error, Single};
use std::{fmt::Display, str::FromStr};
use rand::{distributions::Standard, prelude::Distribution, Rng};
use crate::TripodId;
/// Triple length tripod id.
///
/// # Examples
/// ```
/// # use tripod_id::{TripodId, Triple};
/// # use std::str::FromStr;
///
/// let _ = Triple::from_str("012-abc-def");
/// ```
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Triple(u64);
impl TripodId for Triple{
type Integer = u64;
type Tuple = (Single, Single, Single);
#[cfg(feature="prost")]
type Message = TripleMessage;
const CAPACITY: Self::Integer = (Single::CAPACITY as u64).pow(3);
const NIL: Self = Self(0);
const MAX: Self = Self(Self::CAPACITY - 1);
#[cfg(test)]
fn validate_inner(self) -> bool {
self.0 < Self::CAPACITY
}
}
impl Display for Triple {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let tuple: (Single, Single, Single) = (*self).into();
write!(f, "{}-{}-{}", tuple.0, tuple.1, tuple.2)
}
}
impl FromStr for Triple {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.len() {
11 => {
let delimiter = [
s[3..4].chars().next().unwrap(),
s[7..8].chars().next().unwrap(),
];
if is_delimiter(delimiter[0]) && is_delimiter(delimiter[1]) {
Ok(Self::from((Single::from_str(&s[0..3])?,Single::from_str(&s[4..7])?,Single::from_str(&s[8..11])?)))
} else {
Err(Error::InvalidDelimiter{
found: Vec::from(delimiter),
raw: s.to_string()
})
}
}
9 => {
Ok(Self::from((Single::from_str(&s[0..3])?,Single::from_str(&s[3..6])?,Single::from_str(&s[6..9])?)))
}
x => {
Err(Self::Err::InvalidLength{
expected: (9, 11),
found: x,
raw: s.to_string()
})
}
} ?
)
}
}
impl Distribution<Triple> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Triple {
Triple(rng.gen_range(0..Triple::CAPACITY))
}
}
impl TryFrom<u64> for Triple {
type Error = Error;
fn try_from(value: u64) -> Result<Self, Self::Error> {
if value < Self::CAPACITY {
Ok(Self(value))
} else {
Err(Error::OutsideOfRange{
expected: Self::CAPACITY as u64,
found: value as u64
})
}
}
}
impl From<Triple> for u64 {
fn from(value: Triple) -> Self {
value.0
}
}
impl From<(Single, Single, Single)> for Triple {
fn from(value: (Single, Single, Single)) -> Self {
Self(
(u16::from(value.0) as u64) * (Double::CAPACITY as u64)
+ (u16::from(value.1) as u64) * (Single::CAPACITY as u64)
+ (u16::from(value.2) as u64)
)
}
}
impl From<Triple> for (Single, Single, Single) {
fn from(value: Triple) -> Self {
(
Single::try_from(u16::try_from(value.0 / (Double::CAPACITY as u64)).unwrap()).unwrap(),
Single::try_from(u16::try_from((value.0 % (Double::CAPACITY as u64)) /(Single::CAPACITY as u64)).unwrap()).unwrap(),
Single::try_from(u16::try_from(value.0 % (Single::CAPACITY as u64)).unwrap()).unwrap()
)
}
}
impl PartialEq<u64> for Triple {
fn eq(&self, other: &u64) -> bool {
&u64::from(*self) == other
}
}
impl PartialEq<String> for Triple {
fn eq(&self, other: &String) -> bool {
match Self::from_str(other) {
Ok(x) => *self == x,
Err(_) => false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nil() {
assert!(Triple::NIL.validate_all().unwrap());
assert_eq!(Triple::NIL, 0);
assert_eq!(Triple::NIL, "000000000".to_string());
assert_eq!(Triple::NIL, "000-000-000".to_string());
}
#[test]
fn max() {
assert!(Triple::MAX.validate_all().unwrap());
assert_eq!(Triple::MAX, Triple::CAPACITY-1);
assert_eq!(Triple::MAX, "zzzzzzzzz".to_string());
assert_eq!(Triple::MAX, "ZZZ-ZZZ-ZZZ".to_string());
assert_eq!((Single::MAX, Single::MAX, Single::MAX), Triple::MAX.into())
}
#[test]
#[should_panic]
fn over_sized() {
Triple::try_from(Triple::CAPACITY).unwrap();
}
#[test]
fn random() {
let mut rng = rand::thread_rng();
for _ in 0..10 {
let id: Triple = rng.r#gen();
assert!(id.validate_all().unwrap());
}
}
}

7
tripod-id/src/utils.rs Normal file
View file

@ -0,0 +1,7 @@
/// Test if the character is valid delimiter.
pub fn is_delimiter(c: char) -> bool {
match c {
'-' | '_' => true,
_ => false,
}
}