qualia_core_db/identity/credentials/
codecs.rs1use super::Credential;
6
7#[derive(Debug)]
8pub enum CodecError {
9 SerializationError(String),
10}
11
12pub trait CredentialCodec {
14 fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError>;
15 fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError>;
16}
17
18pub struct OpenBadgeCodec;
20
21const OB_CONTEXT: &str = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json";
22
23impl CredentialCodec for OpenBadgeCodec {
24 fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError> {
25 let mut c = credential.clone();
26 if !c.context.iter().any(|s| s == OB_CONTEXT) {
27 c.context.push(OB_CONTEXT.to_string());
28 }
29 if !c.types.iter().any(|s| s == "OpenBadgeCredential") {
30 c.types.push("OpenBadgeCredential".to_string());
31 }
32 serde_json::to_vec(&c)
33 .map_err(|e| CodecError::SerializationError(format!("OpenBadge encoding failed: {e}")))
34 }
35
36 fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError> {
37 let credential: Credential = serde_json::from_slice(payload).map_err(|e| {
38 CodecError::SerializationError(format!("OpenBadge decoding failed: {e}"))
39 })?;
40 if !credential.types.iter().any(|s| s == "OpenBadgeCredential") {
41 return Err(CodecError::SerializationError(
42 "Not a valid OpenBadgeCredential".to_string(),
43 ));
44 }
45 Ok(credential)
46 }
47}
48
49pub struct PdfCodec;
51
52impl CredentialCodec for PdfCodec {
53 fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError> {
54 let json = serde_json::to_string(credential)
55 .map_err(|e| CodecError::SerializationError(format!("PDF encoding failed: {e}")))?;
56
57 let mut pdf = Vec::new();
58 pdf.extend_from_slice(b"%PDF-1.7\n");
59 pdf.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
60 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
61 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
62 pdf.extend_from_slice(b"3 0 obj\n");
63 pdf.extend_from_slice(
64 format!(
65 "<< /Type /Metadata /Subtype /XML /Length {} >>\n",
66 json.len()
67 )
68 .as_bytes(),
69 );
70 pdf.extend_from_slice(b"stream\n");
71 pdf.extend_from_slice(json.as_bytes());
72 pdf.extend_from_slice(b"\nendstream\nendobj\n");
73 pdf.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n0000000015 00000 n \n0000000066 00000 n \n0000000125 00000 n \n");
74 pdf.extend_from_slice(b"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n250\n%%EOF\n");
75 Ok(pdf)
76 }
77
78 fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError> {
79 let s = String::from_utf8_lossy(payload);
80 if let Some(start) = s.find("stream\n{") {
81 let json_start = start + 7;
82 if let Some(end) = s[json_start..].find("\nendstream") {
83 return serde_json::from_str(&s[json_start..json_start + end]).map_err(|e| {
84 CodecError::SerializationError(format!("PDF decoding failed: {e}"))
85 });
86 }
87 }
88 Err(CodecError::SerializationError(
89 "Could not locate VC in PDF payload".to_string(),
90 ))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use std::collections::HashMap;
98
99 fn test_credential() -> Credential {
100 let mut subject = HashMap::new();
101 subject.insert("id".to_string(), "did:example:123".to_string());
102 subject.insert("achievement".to_string(), "Course Completion".to_string());
103 Credential {
104 context: vec!["https://www.w3.org/2018/credentials/v1".to_string()],
105 id: "http://example.edu/credentials/3732".to_string(),
106 types: vec!["VerifiableCredential".to_string()],
107 issuer: "https://example.edu/issuers/565049".to_string(),
108 issuance_date: "2010-01-01T19:23:24Z".to_string(),
109 credential_subject: subject,
110 proof: None,
111 }
112 }
113
114 #[test]
115 fn openbadge_roundtrip() {
116 let codec = OpenBadgeCodec;
117 let cred = test_credential();
118 let encoded = codec.encode(&cred).expect("encode");
119 let decoded = codec.decode(&encoded).expect("decode");
120 assert!(decoded.types.iter().any(|s| s == "OpenBadgeCredential"));
121 assert!(decoded.context.iter().any(|s| s == OB_CONTEXT));
122 assert_eq!(decoded.id, cred.id);
123 }
124
125 #[test]
126 fn pdf_roundtrip() {
127 let codec = PdfCodec;
128 let cred = test_credential();
129 let encoded = codec.encode(&cred).expect("encode");
130 assert!(encoded.starts_with(b"%PDF-1.7"));
131 assert!(encoded.ends_with(b"%%EOF\n"));
132 let decoded = codec.decode(&encoded).expect("decode");
133 assert_eq!(decoded.id, cred.id);
134 assert_eq!(decoded.issuer, cred.issuer);
135 }
136}