1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use crate::common::timestamp;
use base32::{decode, encode, Alphabet};
use blake3::Hasher;
use serde::de::DeserializeOwned;
pub trait TimestampId {
/// Creates a unique identifier based on the current timestamp.
fn create_id(&self) -> String {
// Get current time in microseconds since UNIX epoch
let now = timestamp();
// Convert to big-endian bytes
let bytes = now.to_be_bytes();
// Encode the bytes using Base32 with the Crockford alphabet
encode(Alphabet::Crockford, &bytes)
}
/// Validates that the provided ID is a valid Crockford Base32-encoded timestamp,
/// 13 characters long, and represents a reasonable timestamp.
fn validate_id(&self, id: &str) -> Result<(), String> {
// Ensure ID is 13 characters long
if id.len() != 13 {
return Err("Validation Error: Invalid ID length: must be 13 characters".into());
}
// Decode the Crockford Base32-encoded ID
let decoded_bytes =
decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?;
if decoded_bytes.len() != 8 {
return Err("Validation Error: Invalid ID length after decoding".into());
}
// Convert the decoded bytes to a timestamp in microseconds
let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap());
// Get current time in microseconds
let now_micros = timestamp();
// Define October 1st, 2024, in microseconds since UNIX epoch
let oct_first_2024_micros = 1727740800000000; // Timestamp for 2024-10-01 00:00:00 UTC
// Allowable future duration (2 hours) in microseconds
let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000;
// Validate that the ID's timestamp is after October 1st, 2024
if timestamp_micros < oct_first_2024_micros {
return Err(
"Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(),
);
}
// Validate that the ID's timestamp is not more than 2 hours in the future
if timestamp_micros > max_future_micros {
return Err("Validation Error: Invalid ID, timestamp is too far in the future".into());
}
Ok(())
}
}
/// Trait for generating an ID based on the struct's data.
pub trait HashId {
fn get_id_data(&self) -> String;
/// Creates a unique identifier for bookmarks and tag homeserver paths instance.
///
/// The ID is generated by:
/// 1. Concatenating the `uri` and `label` fields of the `PubkyAppTag` with a colon (`:`) separator.
/// 2. Hashing the concatenated string using the `blake3` hashing algorithm.
/// 3. Taking the first half of the bytes from the resulting `blake3` hash.
/// 4. Encoding those bytes using the Crockford alphabet (Base32 variant).
///
/// The resulting Crockford-encoded string is returned as the tag ID.
///
/// # Returns
/// - A `String` representing the Crockford-encoded tag ID derived from the `blake3` hash of the concatenated `uri` and `label`.
fn create_id(&self) -> String {
let data = self.get_id_data();
// Create a Blake3 hash of the input data
let mut hasher = Hasher::new();
hasher.update(data.as_bytes());
let blake3_hash = hasher.finalize();
// Get the first half of the hash bytes
let half_hash_length = blake3_hash.as_bytes().len() / 2;
let half_hash = &blake3_hash.as_bytes()[..half_hash_length];
// Encode the first half of the hash in Base32 using the Z-base32 alphabet
encode(Alphabet::Crockford, half_hash)
}
/// Validates that the provided ID matches the generated ID.
fn validate_id(&self, id: &str) -> Result<(), String> {
let generated_id = self.create_id();
if generated_id != id {
return Err(format!("Invalid ID: expected {generated_id}, found {id}"));
}
Ok(())
}
}
pub trait Validatable: Sized + DeserializeOwned {
fn try_from(blob: &[u8], id: &str) -> Result<Self, String> {
let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?;
instance = instance.sanitize();
instance.validate(Some(id))?;
Ok(instance)
}
fn validate(&self, id: Option<&str>) -> Result<(), String>;
fn sanitize(self) -> Self {
self
}
}
pub trait HasPath {
const PATH_SEGMENT: &'static str;
fn create_path() -> String;
}
pub trait HasIdPath {
const PATH_SEGMENT: &'static str;
fn create_path(id: &str) -> String;
}
#[cfg(target_arch = "wasm32")]
use serde::Serialize;
#[cfg(target_arch = "wasm32")]
use serde_wasm_bindgen::{from_value, to_value};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;
/// Provides a `.to_json()` method returning a `JsValue` with all fields in plain JSON.
#[cfg(target_arch = "wasm32")]
pub trait Json: Serialize + DeserializeOwned + Validatable {
fn export_json(&self) -> Result<JsValue, String> {
to_value(&self).map_err(|e| format!("JSON serialization error: {}", e))
}
fn import_json(js_value: &JsValue) -> Result<Self, String> {
let object: Self =
from_value(js_value.clone()).map_err(|e| format!("Error parsing js object: {}", e))?;
let object = object.sanitize();
object.validate(None)?;
Ok(object)
}
}