1use async_trait::async_trait;
2use libp2p::futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
3use libp2p::request_response::Codec;
4use libp2p::StreamProtocol;
5use serde::{Deserialize, Serialize};
6use std::io;
7use std::sync::Arc;
8
9#[cfg(not(target_arch = "wasm32"))]
10use crate::q42_lexicon::{CborLdError, Q42CborLdParser, Q42Context, SemanticPayload};
11#[cfg(not(target_arch = "wasm32"))]
12use crate::q42_volume::Q42Volume;
13
14#[repr(C)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct NQuin {
17 pub subject: [u8; 8],
18 pub predicate: [u8; 8],
19 pub object: [u8; 8],
20 pub context: [u8; 8],
21 pub clock_sig: [u8; 16],
22}
23const _: () = assert!(std::mem::size_of::<NQuin>() == 48);
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum QualiaRequest {
27 Handshake {
28 #[serde(rename = "@context")]
30 context: String,
31 #[serde(rename = "type")]
32 request_type: String,
33 #[serde(rename = "did_q42")]
34 did_q42: u64,
35 #[serde(rename = "semantic_context")]
36 semantic_context: u64,
37 credentials: Vec<u8>,
39 },
40 Sync {
41 #[serde(rename = "@context")]
43 context: String,
44 #[serde(rename = "type")]
45 request_type: String,
46 #[serde(rename = "did_q42")]
47 did_q42: u64,
48 hop_count: u8,
49 gatekeeper_token: Option<String>,
50 #[serde(rename = "target_shapes")]
51 target_shapes: Vec<String>,
52 #[serde(rename = "routing_constraints")]
53 routing_constraints: u8,
54 },
55}
56
57#[cfg(not(target_arch = "wasm32"))]
58impl QualiaRequest {
59 pub fn from_semantic_payload(payload: SemanticPayload) -> Self {
61 let did_q42 = match payload.did_q42 {
62 Some(d) => crate::q_hash(&d),
63 None => 0,
64 };
65
66 let semantic_context = payload
68 .semantic_context
69 .get("context")
70 .map(|s| crate::q_hash(s))
71 .unwrap_or(0);
72
73 Self::Handshake {
74 context: "https://webizen.org/ld/context/v1".to_string(),
75 request_type: "Handshake".to_string(),
76 did_q42,
77 semantic_context,
78 credentials: Vec::new(), }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum QualiaResponse {
85 HandshakeAck {
86 #[serde(rename = "@context")]
88 context: String,
89 #[serde(rename = "type")]
90 response_type: String,
91 success: bool,
92 #[serde(rename = "did_q42")]
93 did_q42: u64,
94 #[serde(rename = "semantic_context")]
95 semantic_context: u64,
96 },
97 SyncAck {
98 #[serde(rename = "@context")]
100 context: String,
101 #[serde(rename = "type")]
102 response_type: String,
103 success: bool,
104 message: String,
105 blocks_sent: u64,
106 #[serde(rename = "did_q42")]
107 did_q42: u64,
108 #[serde(rename = "routing_constraints")]
109 routing_constraints: u8,
110 },
111}
112
113#[cfg(not(target_arch = "wasm32"))]
114impl QualiaResponse {
115 pub fn from_semantic_payload(payload: SemanticPayload) -> Self {
117 let did_q42 = match payload.did_q42 {
118 Some(d) => crate::q_hash(&d),
119 None => 0,
120 };
121
122 let semantic_context = payload
123 .semantic_context
124 .get("context")
125 .map(|s| crate::q_hash(s))
126 .unwrap_or(0);
127
128 Self::HandshakeAck {
129 context: "https://webizen.org/ld/context/v1".to_string(),
130 response_type: "HandshakeAck".to_string(),
131 success: true,
132 did_q42,
133 semantic_context,
134 }
135 }
136}
137
138#[cfg(not(target_arch = "wasm32"))]
149pub(crate) mod qcborld {
150 use super::{QualiaRequest, QualiaResponse};
151 use crate::q42::q42_lexicon::Q42Lexicon;
152 use ciborium::value::Value;
153
154 pub const CONTEXT_IRI: &str = "https://webizen.org/ld/context/v1";
155 const MAGIC_TERM: &str = "@cbor-ld/q42";
156 const VERSION: u64 = 1;
157
158 #[inline]
161 fn key(lex: &Q42Lexicon, term: &str) -> u64 {
162 lex.resolve_term(term)
163 .unwrap_or_else(|| crate::q_hash(term))
164 }
165 #[inline]
166 fn kv(lex: &Q42Lexicon, term: &str, v: Value) -> (Value, Value) {
167 (Value::from(key(lex, term)), v)
168 }
169 fn get<'a>(map: &'a [(Value, Value)], lex: &Q42Lexicon, term: &str) -> Option<&'a Value> {
170 let k = key(lex, term);
171 map.iter().find_map(|(kk, vv)| match kk.as_integer() {
172 Some(i) if i128::from(i) as u64 == k => Some(vv),
173 _ => None,
174 })
175 }
176 fn u64v(v: &Value) -> Option<u64> {
177 v.as_integer().map(|i| i128::from(i) as u64)
178 }
179 fn header(lex: &Q42Lexicon, ty: &str) -> Vec<(Value, Value)> {
180 vec![
181 kv(lex, MAGIC_TERM, Value::from(VERSION)),
182 kv(lex, "@context", Value::from(crate::q_hash(CONTEXT_IRI))),
183 kv(lex, "type", Value::Text(ty.to_string())),
184 ]
185 }
186 fn is_cbor_ld(map: &[(Value, Value)], lex: &Q42Lexicon) -> bool {
187 get(map, lex, MAGIC_TERM).and_then(u64v) == Some(VERSION)
188 }
189 fn encode(entries: Vec<(Value, Value)>) -> Result<Vec<u8>, ()> {
190 let mut buf = Vec::new();
191 ciborium::into_writer(&Value::Map(entries), &mut buf).map_err(|_| ())?;
192 Ok(buf)
193 }
194
195 pub fn encode_request(lex: &Q42Lexicon, req: &QualiaRequest) -> Result<Vec<u8>, ()> {
197 let mut m = header(
198 lex,
199 match req {
200 QualiaRequest::Handshake { .. } => "Handshake",
201 QualiaRequest::Sync { .. } => "Sync",
202 },
203 );
204 match req {
205 QualiaRequest::Handshake {
206 did_q42,
207 semantic_context,
208 credentials,
209 ..
210 } => {
211 m.push(kv(lex, "did_q42", Value::from(*did_q42)));
212 m.push(kv(lex, "semantic_context", Value::from(*semantic_context)));
213 m.push(kv(lex, "credentials", Value::Bytes(credentials.clone())));
214 }
215 QualiaRequest::Sync {
216 did_q42,
217 hop_count,
218 gatekeeper_token,
219 target_shapes,
220 routing_constraints,
221 ..
222 } => {
223 m.push(kv(lex, "did_q42", Value::from(*did_q42)));
224 m.push(kv(lex, "hop_count", Value::from(*hop_count as u64)));
225 m.push(kv(
226 lex,
227 "gatekeeper_token",
228 match gatekeeper_token {
229 Some(t) => Value::Text(t.clone()),
230 None => Value::Null,
231 },
232 ));
233 m.push(kv(
234 lex,
235 "target_shapes",
236 Value::Array(
237 target_shapes
238 .iter()
239 .map(|s| Value::Text(s.clone()))
240 .collect(),
241 ),
242 ));
243 m.push(kv(
244 lex,
245 "routing_constraints",
246 Value::from(*routing_constraints as u64),
247 ));
248 }
249 }
250 encode(m)
251 }
252
253 pub fn decode_request(lex: &Q42Lexicon, data: &[u8]) -> Result<QualiaRequest, ()> {
256 let val: Value = ciborium::from_reader(data).map_err(|_| ())?;
257 let map = val.as_map().ok_or(())?;
258 if !is_cbor_ld(map, lex) {
259 return Err(());
260 }
261 let ty = get(map, lex, "type").and_then(|v| v.as_text()).ok_or(())?;
262 let did_q42 = get(map, lex, "did_q42").and_then(u64v).unwrap_or(0);
263 match ty {
264 "Handshake" => Ok(QualiaRequest::Handshake {
265 context: CONTEXT_IRI.to_string(),
266 request_type: "Handshake".to_string(),
267 did_q42,
268 semantic_context: get(map, lex, "semantic_context")
269 .and_then(u64v)
270 .unwrap_or(0),
271 credentials: get(map, lex, "credentials")
272 .and_then(|v| v.as_bytes())
273 .cloned()
274 .unwrap_or_default(),
275 }),
276 "Sync" => Ok(QualiaRequest::Sync {
277 context: CONTEXT_IRI.to_string(),
278 request_type: "Sync".to_string(),
279 did_q42,
280 hop_count: get(map, lex, "hop_count").and_then(u64v).unwrap_or(0) as u8,
281 gatekeeper_token: get(map, lex, "gatekeeper_token")
282 .and_then(|v| v.as_text().map(|s| s.to_string())),
283 target_shapes: get(map, lex, "target_shapes")
284 .and_then(|v| v.as_array())
285 .map(|a| {
286 a.iter()
287 .filter_map(|x| x.as_text().map(|s| s.to_string()))
288 .collect()
289 })
290 .unwrap_or_default(),
291 routing_constraints: get(map, lex, "routing_constraints")
292 .and_then(u64v)
293 .unwrap_or(0) as u8,
294 }),
295 _ => Err(()),
296 }
297 }
298
299 pub fn encode_response(lex: &Q42Lexicon, res: &QualiaResponse) -> Result<Vec<u8>, ()> {
301 let mut m = header(
302 lex,
303 match res {
304 QualiaResponse::HandshakeAck { .. } => "HandshakeAck",
305 QualiaResponse::SyncAck { .. } => "SyncAck",
306 },
307 );
308 match res {
309 QualiaResponse::HandshakeAck {
310 success,
311 did_q42,
312 semantic_context,
313 ..
314 } => {
315 m.push(kv(lex, "success", Value::Bool(*success)));
316 m.push(kv(lex, "did_q42", Value::from(*did_q42)));
317 m.push(kv(lex, "semantic_context", Value::from(*semantic_context)));
318 }
319 QualiaResponse::SyncAck {
320 success,
321 message,
322 blocks_sent,
323 did_q42,
324 routing_constraints,
325 ..
326 } => {
327 m.push(kv(lex, "success", Value::Bool(*success)));
328 m.push(kv(lex, "message", Value::Text(message.clone())));
329 m.push(kv(lex, "blocks_sent", Value::from(*blocks_sent)));
330 m.push(kv(lex, "did_q42", Value::from(*did_q42)));
331 m.push(kv(
332 lex,
333 "routing_constraints",
334 Value::from(*routing_constraints as u64),
335 ));
336 }
337 }
338 encode(m)
339 }
340
341 pub fn decode_response(lex: &Q42Lexicon, data: &[u8]) -> Result<QualiaResponse, ()> {
343 let val: Value = ciborium::from_reader(data).map_err(|_| ())?;
344 let map = val.as_map().ok_or(())?;
345 if !is_cbor_ld(map, lex) {
346 return Err(());
347 }
348 let ty = get(map, lex, "type").and_then(|v| v.as_text()).ok_or(())?;
349 let success = get(map, lex, "success")
350 .and_then(|v| v.as_bool())
351 .unwrap_or(false);
352 let did_q42 = get(map, lex, "did_q42").and_then(u64v).unwrap_or(0);
353 match ty {
354 "HandshakeAck" => Ok(QualiaResponse::HandshakeAck {
355 context: CONTEXT_IRI.to_string(),
356 response_type: "HandshakeAck".to_string(),
357 success,
358 did_q42,
359 semantic_context: get(map, lex, "semantic_context")
360 .and_then(u64v)
361 .unwrap_or(0),
362 }),
363 "SyncAck" => Ok(QualiaResponse::SyncAck {
364 context: CONTEXT_IRI.to_string(),
365 response_type: "SyncAck".to_string(),
366 success,
367 message: get(map, lex, "message")
368 .and_then(|v| v.as_text().map(|s| s.to_string()))
369 .unwrap_or_default(),
370 blocks_sent: get(map, lex, "blocks_sent").and_then(u64v).unwrap_or(0),
371 did_q42,
372 routing_constraints: get(map, lex, "routing_constraints")
373 .and_then(u64v)
374 .unwrap_or(0) as u8,
375 }),
376 _ => Err(()),
377 }
378 }
379}
380
381#[derive(Clone)]
382pub struct QualiaSyncCodec {
383 #[cfg(not(target_arch = "wasm32"))]
384 q42_context: Option<Arc<Q42Context>>,
385 #[cfg(not(target_arch = "wasm32"))]
386 cbor_ld_parser: Option<Arc<Q42CborLdParser>>,
387}
388
389impl Default for QualiaSyncCodec {
390 fn default() -> Self {
391 Self {
392 #[cfg(not(target_arch = "wasm32"))]
393 q42_context: None,
394 #[cfg(not(target_arch = "wasm32"))]
395 cbor_ld_parser: None,
396 }
397 }
398}
399
400#[cfg(not(target_arch = "wasm32"))]
401impl QualiaSyncCodec {
402 pub fn with_q42_volume(volume: &Q42Volume) -> Result<Self, CborLdError> {
404 let context =
405 Arc::new(Q42Context::from_volume(volume).map_err(|_| CborLdError::InvalidOffset)?);
406 let parser =
407 Arc::new(Q42CborLdParser::from_volume(volume).map_err(|_| CborLdError::InvalidOffset)?);
408
409 Ok(Self {
410 q42_context: Some(context),
411 cbor_ld_parser: Some(parser),
412 })
413 }
414
415 pub fn q42_context(&self) -> Option<&Arc<Q42Context>> {
417 self.q42_context.as_ref()
418 }
419
420 pub fn cbor_ld_parser(&self) -> Option<&Arc<Q42CborLdParser>> {
422 self.cbor_ld_parser.as_ref()
423 }
424}
425
426#[async_trait]
427impl Codec for QualiaSyncCodec {
428 type Protocol = StreamProtocol;
429 type Request = QualiaRequest;
430 type Response = QualiaResponse;
431
432 async fn read_request<T>(&mut self, _: &Self::Protocol, io: &mut T) -> io::Result<Self::Request>
433 where
434 T: AsyncRead + Unpin + Send,
435 {
436 let mut len_buf = [0u8; 4];
437 io.read_exact(&mut len_buf).await?;
438 let len = u32::from_be_bytes(len_buf) as usize;
439
440 let mut buf = vec![0u8; len];
441 io.read_exact(&mut buf).await?;
442
443 #[cfg(not(target_arch = "wasm32"))]
446 if let Some(ref parser) = self.cbor_ld_parser {
447 if let Ok(req) = qcborld::decode_request(parser.lexicon(), &buf) {
448 return Ok(req);
449 }
450 }
451
452 ciborium::from_reader(&buf[..])
454 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
455 }
456
457 async fn read_response<T>(
458 &mut self,
459 _: &Self::Protocol,
460 io: &mut T,
461 ) -> io::Result<Self::Response>
462 where
463 T: AsyncRead + Unpin + Send,
464 {
465 let mut len_buf = [0u8; 4];
466 io.read_exact(&mut len_buf).await?;
467 let len = u32::from_be_bytes(len_buf) as usize;
468
469 let mut buf = vec![0u8; len];
470 io.read_exact(&mut buf).await?;
471
472 #[cfg(not(target_arch = "wasm32"))]
475 if let Some(ref parser) = self.cbor_ld_parser {
476 if let Ok(res) = qcborld::decode_response(parser.lexicon(), &buf) {
477 return Ok(res);
478 }
479 }
480
481 ciborium::from_reader(&buf[..])
483 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
484 }
485
486 async fn write_request<T>(
487 &mut self,
488 _: &Self::Protocol,
489 io: &mut T,
490 req: Self::Request,
491 ) -> io::Result<()>
492 where
493 T: AsyncWrite + Unpin + Send,
494 {
495 #[cfg(not(target_arch = "wasm32"))]
498 if let Some(ref parser) = self.cbor_ld_parser {
499 if let Ok(bytes) = qcborld::encode_request(parser.lexicon(), &req) {
500 io.write_all(&(bytes.len() as u32).to_be_bytes()).await?;
501 io.write_all(&bytes).await?;
502 return Ok(());
503 }
504 }
505
506 let mut buf = Vec::new();
507 ciborium::into_writer(&req, &mut buf)
508 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
509
510 let len = buf.len() as u32;
511 io.write_all(&len.to_be_bytes()).await?;
512 io.write_all(&buf).await?;
513 Ok(())
514 }
515
516 async fn write_response<T>(
517 &mut self,
518 _: &Self::Protocol,
519 io: &mut T,
520 res: Self::Response,
521 ) -> io::Result<()>
522 where
523 T: AsyncWrite + Unpin + Send,
524 {
525 #[cfg(not(target_arch = "wasm32"))]
528 if let Some(ref parser) = self.cbor_ld_parser {
529 if let Ok(bytes) = qcborld::encode_response(parser.lexicon(), &res) {
530 io.write_all(&(bytes.len() as u32).to_be_bytes()).await?;
531 io.write_all(&bytes).await?;
532 return Ok(());
533 }
534 }
535
536 let mut buf = Vec::new();
537 ciborium::into_writer(&res, &mut buf)
538 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
539
540 let len = buf.len() as u32;
541 io.write_all(&len.to_be_bytes()).await?;
542 io.write_all(&buf).await?;
543 Ok(())
544 }
545}
546
547#[cfg(all(test, not(target_arch = "wasm32")))]
548mod cbor_ld_tests {
549 use super::*;
550 use crate::q42::q42_lexicon::Q42Lexicon;
551
552 #[test]
556 fn cbor_ld_request_roundtrip_is_lossless() {
557 let lex = Q42Lexicon::new();
558 let hs = QualiaRequest::Handshake {
559 context: qcborld::CONTEXT_IRI.to_string(),
560 request_type: "Handshake".to_string(),
561 did_q42: 0xDEAD_BEEF,
562 semantic_context: 0x1234_5678,
563 credentials: vec![1, 2, 3, 4, 5, 250, 251, 252],
564 };
565 let bytes = qcborld::encode_request(&lex, &hs).unwrap();
566 assert!(ciborium::from_reader::<QualiaRequest, _>(&bytes[..]).is_err());
568 let back = qcborld::decode_request(&lex, &bytes).unwrap();
569 match back {
570 QualiaRequest::Handshake {
571 did_q42,
572 semantic_context,
573 credentials,
574 ..
575 } => {
576 assert_eq!(did_q42, 0xDEAD_BEEF);
577 assert_eq!(semantic_context, 0x1234_5678);
578 assert_eq!(credentials, vec![1, 2, 3, 4, 5, 250, 251, 252]);
579 }
580 _ => panic!("variant changed across round-trip"),
581 }
582
583 let sync = QualiaRequest::Sync {
584 context: qcborld::CONTEXT_IRI.to_string(),
585 request_type: "Sync".to_string(),
586 did_q42: 42,
587 hop_count: 2,
588 gatekeeper_token: Some("tok-abc".to_string()),
589 target_shapes: vec!["foaf:Person".to_string(), "qualia:Vault".to_string()],
590 routing_constraints: 7,
591 };
592 let back =
593 qcborld::decode_request(&lex, &qcborld::encode_request(&lex, &sync).unwrap()).unwrap();
594 match back {
595 QualiaRequest::Sync {
596 did_q42,
597 hop_count,
598 gatekeeper_token,
599 target_shapes,
600 routing_constraints,
601 ..
602 } => {
603 assert_eq!(did_q42, 42);
604 assert_eq!(hop_count, 2);
605 assert_eq!(gatekeeper_token.as_deref(), Some("tok-abc"));
606 assert_eq!(
607 target_shapes,
608 vec!["foaf:Person".to_string(), "qualia:Vault".to_string()]
609 );
610 assert_eq!(routing_constraints, 7);
611 }
612 _ => panic!("variant changed across round-trip"),
613 }
614 }
615
616 #[test]
617 fn cbor_ld_response_roundtrip_is_lossless() {
618 let lex = Q42Lexicon::new();
619 let ack = QualiaResponse::SyncAck {
620 context: qcborld::CONTEXT_IRI.to_string(),
621 response_type: "SyncAck".to_string(),
622 success: true,
623 message: "synced".to_string(),
624 blocks_sent: 1234,
625 did_q42: 99,
626 routing_constraints: 3,
627 };
628 let back =
629 qcborld::decode_response(&lex, &qcborld::encode_response(&lex, &ack).unwrap()).unwrap();
630 match back {
631 QualiaResponse::SyncAck {
632 success,
633 message,
634 blocks_sent,
635 did_q42,
636 routing_constraints,
637 ..
638 } => {
639 assert!(success);
640 assert_eq!(message, "synced");
641 assert_eq!(blocks_sent, 1234);
642 assert_eq!(did_q42, 99);
643 assert_eq!(routing_constraints, 3);
644 }
645 _ => panic!("variant changed across round-trip"),
646 }
647 }
648
649 #[test]
650 fn plain_ciborium_is_not_mistaken_for_cbor_ld() {
651 let lex = Q42Lexicon::new();
654 let hs = QualiaRequest::Handshake {
655 context: qcborld::CONTEXT_IRI.to_string(),
656 request_type: "Handshake".to_string(),
657 did_q42: 1,
658 semantic_context: 2,
659 credentials: vec![],
660 };
661 let mut plain = Vec::new();
662 ciborium::into_writer(&hs, &mut plain).unwrap();
663 assert!(qcborld::decode_request(&lex, &plain).is_err());
664 }
665}