forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
6.8 kB
226 lines
1use serde::{Deserialize, Serialize};
2use smallvec::SmallVec;
3
4use super::encoding::KeyBuilder;
5use super::keys::{KeyTag, UserHash};
6
7const SCHEMA_VERSION: u8 = 1;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[repr(u8)]
11pub enum RepoStatus {
12 Active = 0,
13 Takendown = 1,
14 Suspended = 2,
15 Deactivated = 3,
16 Deleted = 4,
17}
18
19impl RepoStatus {
20 pub fn is_active(self) -> bool {
21 matches!(self, Self::Active)
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct RepoMetaValue {
27 pub repo_root_cid: Vec<u8>,
28 pub repo_rev: String,
29 pub handle: String,
30 pub status: RepoStatus,
31 pub deactivated_at_ms: Option<u64>,
32 pub takedown_ref: Option<String>,
33 #[serde(default)]
34 pub did: Option<String>,
35}
36
37impl RepoMetaValue {
38 pub fn serialize(&self) -> Vec<u8> {
39 let payload = postcard::to_allocvec(self).expect("RepoMetaValue serialization cannot fail");
40 let mut buf = Vec::with_capacity(1 + payload.len());
41 buf.push(SCHEMA_VERSION);
42 buf.extend_from_slice(&payload);
43 buf
44 }
45
46 pub fn deserialize(bytes: &[u8]) -> Option<Self> {
47 let (&version, payload) = bytes.split_first()?;
48 match version {
49 SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
50 _ => None,
51 }
52 }
53}
54
55pub fn repo_meta_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
56 KeyBuilder::new()
57 .tag(KeyTag::REPO_META)
58 .u64(user_hash.raw())
59 .build()
60}
61
62pub fn repo_meta_prefix() -> SmallVec<[u8; 128]> {
63 KeyBuilder::new().tag(KeyTag::REPO_META).build()
64}
65
66pub fn handle_key(handle_lower: &str) -> SmallVec<[u8; 128]> {
67 KeyBuilder::new()
68 .tag(KeyTag::HANDLES)
69 .string(handle_lower)
70 .build()
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::metastore::encoding::KeyReader;
77
78 #[test]
79 fn repo_meta_value_roundtrip() {
80 let value = RepoMetaValue {
81 repo_root_cid: vec![0x01, 0x71, 0x12, 0x20, 0xAB],
82 repo_rev: "3k2a7bcd".to_string(),
83 handle: "alice.bsky.social".to_string(),
84 status: RepoStatus::Active,
85 deactivated_at_ms: None,
86 takedown_ref: None,
87 did: None,
88 };
89 let bytes = value.serialize();
90 let decoded = RepoMetaValue::deserialize(&bytes).unwrap();
91 assert_eq!(decoded, value);
92 }
93
94 #[test]
95 fn repo_meta_value_with_optional_fields() {
96 let value = RepoMetaValue {
97 repo_root_cid: vec![0x01],
98 repo_rev: "rev1".to_string(),
99 handle: "bob.example.com".to_string(),
100 status: RepoStatus::Deactivated,
101 deactivated_at_ms: Some(1700000000000),
102 takedown_ref: Some("DMCA-123".to_string()),
103 did: Some("did:plc:bob".to_string()),
104 };
105 let bytes = value.serialize();
106 let decoded = RepoMetaValue::deserialize(&bytes).unwrap();
107 assert_eq!(decoded, value);
108 }
109
110 #[test]
111 fn repo_meta_key_roundtrip() {
112 let hash = UserHash::from_raw(0xDEAD_BEEF_CAFE_BABE);
113 let key = repo_meta_key(hash);
114 let mut reader = KeyReader::new(&key);
115 assert_eq!(reader.tag(), Some(KeyTag::REPO_META.raw()));
116 assert_eq!(reader.u64(), Some(0xDEAD_BEEF_CAFE_BABE));
117 assert!(reader.is_empty());
118 }
119
120 #[test]
121 fn repo_meta_keys_sort_by_user_hash() {
122 let k1 = repo_meta_key(UserHash::from_raw(1));
123 let k2 = repo_meta_key(UserHash::from_raw(2));
124 let k3 = repo_meta_key(UserHash::from_raw(u64::MAX));
125 assert!(k1.as_slice() < k2.as_slice());
126 assert!(k2.as_slice() < k3.as_slice());
127 }
128
129 #[test]
130 fn handle_key_roundtrip() {
131 let key = handle_key("alice.bsky.social");
132 let mut reader = KeyReader::new(&key);
133 assert_eq!(reader.tag(), Some(KeyTag::HANDLES.raw()));
134 assert_eq!(reader.string(), Some("alice.bsky.social".to_string()));
135 assert!(reader.is_empty());
136 }
137
138 #[test]
139 fn handle_keys_sort_lexicographically() {
140 let k1 = handle_key("alice.example.com");
141 let k2 = handle_key("bob.example.com");
142 assert!(k1.as_slice() < k2.as_slice());
143 }
144
145 #[test]
146 fn all_repo_statuses_roundtrip() {
147 [
148 RepoStatus::Active,
149 RepoStatus::Takendown,
150 RepoStatus::Suspended,
151 RepoStatus::Deactivated,
152 RepoStatus::Deleted,
153 ]
154 .iter()
155 .for_each(|&status| {
156 let value = RepoMetaValue {
157 repo_root_cid: vec![0x01],
158 repo_rev: "r".to_string(),
159 handle: "h.test".to_string(),
160 status,
161 deactivated_at_ms: None,
162 takedown_ref: None,
163 did: None,
164 };
165 let decoded = RepoMetaValue::deserialize(&value.serialize()).unwrap();
166 assert_eq!(decoded.status, status);
167 });
168 }
169
170 #[test]
171 fn repo_status_serialization_stability() {
172 [
173 (RepoStatus::Active, 0u8),
174 (RepoStatus::Takendown, 1),
175 (RepoStatus::Suspended, 2),
176 (RepoStatus::Deactivated, 3),
177 (RepoStatus::Deleted, 4),
178 ]
179 .iter()
180 .for_each(|&(status, expected_byte)| {
181 let bytes = postcard::to_allocvec(&status).unwrap();
182 assert_eq!(
183 bytes,
184 [expected_byte],
185 "{status:?} serialized to {bytes:?}, expected [{expected_byte}]"
186 );
187 });
188 }
189
190 #[test]
191 fn schema_version_is_first_byte() {
192 let value = RepoMetaValue {
193 repo_root_cid: vec![0x01],
194 repo_rev: "r".to_string(),
195 handle: "h.test".to_string(),
196 status: RepoStatus::Active,
197 deactivated_at_ms: None,
198 takedown_ref: None,
199 did: None,
200 };
201 let bytes = value.serialize();
202 assert_eq!(bytes[0], SCHEMA_VERSION);
203 assert_eq!(bytes[0], 1, "schema version must remain 1 for this format");
204 }
205
206 #[test]
207 fn deserialize_rejects_unknown_schema_version() {
208 let value = RepoMetaValue {
209 repo_root_cid: vec![0x01],
210 repo_rev: "r".to_string(),
211 handle: "h.test".to_string(),
212 status: RepoStatus::Active,
213 deactivated_at_ms: None,
214 takedown_ref: None,
215 did: None,
216 };
217 let mut bytes = value.serialize();
218 bytes[0] = 99;
219 assert!(RepoMetaValue::deserialize(&bytes).is_none());
220 }
221
222 #[test]
223 fn deserialize_rejects_empty_input() {
224 assert!(RepoMetaValue::deserialize(&[]).is_none());
225 }
226}