identity_credential/credential/
revocation_bitmap_status.rs1use std::str::FromStr;
5
6use identity_core::common::Object;
7use identity_core::common::Url;
8use identity_core::common::Value;
9use identity_did::DIDUrl;
10
11use crate::credential::Status;
12use crate::error::Error;
13use crate::error::Result;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct RevocationBitmapStatus(Status);
19
20impl RevocationBitmapStatus {
21 const INDEX_PROPERTY: &'static str = "revocationBitmapIndex";
22 pub const TYPE: &'static str = "RevocationBitmap2022";
24
25 pub fn new(mut id: DIDUrl, index: u32) -> Self {
44 id.set_query(Some(&format!("index={index}")))
45 .expect("the string should be non-empty and a valid URL query");
46
47 let mut object = Object::new();
48 object.insert(Self::INDEX_PROPERTY.to_owned(), Value::String(index.to_string()));
49 RevocationBitmapStatus(Status::new_with_properties(
50 Url::from(id),
51 Self::TYPE.to_owned(),
52 object,
53 ))
54 }
55
56 pub fn id(&self) -> Result<DIDUrl> {
59 DIDUrl::parse(self.0.id.as_str())
60 .map_err(|err| Error::InvalidStatus(format!("invalid DID Url '{}': {:?}", self.0.id, err)))
61 }
62
63 pub fn index(&self) -> Result<u32> {
65 if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY) {
66 try_index_to_u32(index, Self::INDEX_PROPERTY)
67 } else {
68 Err(Error::InvalidStatus(format!(
69 "expected {} to be an unsigned 32-bit integer expressed as a string",
70 Self::INDEX_PROPERTY
71 )))
72 }
73 }
74}
75
76impl TryFrom<Status> for RevocationBitmapStatus {
77 type Error = Error;
78
79 fn try_from(status: Status) -> Result<Self> {
80 if status.type_ != Self::TYPE {
81 return Err(Error::InvalidStatus(format!(
82 "expected type '{}', got '{}'",
83 Self::TYPE,
84 status.type_
85 )));
86 }
87
88 let revocation_bitmap_index: &Value =
89 if let Some(revocation_bitmap_index) = status.properties.get(Self::INDEX_PROPERTY) {
90 revocation_bitmap_index
91 } else {
92 return Err(Error::InvalidStatus(format!(
93 "missing required property '{}'",
94 Self::INDEX_PROPERTY
95 )));
96 };
97
98 let revocation_bitmap_index: u32 = if let Value::String(index) = revocation_bitmap_index {
99 try_index_to_u32(index, Self::INDEX_PROPERTY)?
100 } else {
101 return Err(Error::InvalidStatus(format!(
102 "property '{}' is not a string",
103 Self::INDEX_PROPERTY
104 )));
105 };
106
107 for pair in status.id.query_pairs() {
111 if pair.0 == "index" {
112 let index: u32 = try_index_to_u32(pair.1.as_ref(), "value of index query")?;
113 if index != revocation_bitmap_index {
114 return Err(Error::InvalidStatus(format!(
115 "value of index query `{index}` does not match revocationBitmapIndex `{revocation_bitmap_index}`"
116 )));
117 }
118 }
119 }
120
121 Ok(Self(status))
122 }
123}
124
125impl From<RevocationBitmapStatus> for Status {
126 fn from(status: RevocationBitmapStatus) -> Self {
127 status.0
128 }
129}
130
131pub fn try_index_to_u32(index: &str, name: &str) -> Result<u32> {
133 u32::from_str(index).map_err(|err| {
134 Error::InvalidStatus(format!(
135 "{name} cannot be converted to an unsigned, 32-bit integer: {err}",
136 ))
137 })
138}
139
140#[cfg(test)]
141mod tests {
142 use identity_core::common::Object;
143 use identity_core::common::Url;
144 use identity_core::common::Value;
145 use identity_core::convert::FromJson;
146 use identity_did::DIDUrl;
147
148 use crate::Error;
149
150 use super::RevocationBitmapStatus;
151 use super::Status;
152
153 #[test]
154 fn test_embedded_status_invariants() {
155 let url: Url = Url::parse("did:method:0xabcd?index=0#revocation").unwrap();
156 let did_url: DIDUrl = DIDUrl::parse(url.clone().into_string()).unwrap();
157 let revocation_list_index: u32 = 0;
158 let embedded_revocation_status: RevocationBitmapStatus =
159 RevocationBitmapStatus::new(did_url, revocation_list_index);
160
161 let object: Object = Object::from([(
162 RevocationBitmapStatus::INDEX_PROPERTY.to_owned(),
163 Value::String(revocation_list_index.to_string()),
164 )]);
165 let status: Status =
166 Status::new_with_properties(url.clone(), RevocationBitmapStatus::TYPE.to_owned(), object.clone());
167 assert_eq!(embedded_revocation_status, status.try_into().unwrap());
168
169 let status_missing_property: Status =
170 Status::new_with_properties(url.clone(), RevocationBitmapStatus::TYPE.to_owned(), Object::new());
171 assert!(RevocationBitmapStatus::try_from(status_missing_property).is_err());
172
173 let status_wrong_type: Status = Status::new_with_properties(url, "DifferentType".to_owned(), object);
174 assert!(RevocationBitmapStatus::try_from(status_wrong_type).is_err());
175 }
176
177 #[test]
178 fn test_revocation_bitmap_status_index_query() {
179 let did_url: DIDUrl = DIDUrl::parse("did:method:0xffff#rev-0").unwrap();
181 let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
182 assert_eq!(revocation_status.id().unwrap().query().unwrap(), "index=250");
183
184 let did_url: DIDUrl = DIDUrl::parse("did:method:0xffff?index=300#rev-0").unwrap();
186 let revocation_status: RevocationBitmapStatus = RevocationBitmapStatus::new(did_url, 250);
187 assert_eq!(revocation_status.id().unwrap().query().unwrap(), "index=250");
188 }
189
190 #[test]
191 fn test_revocation_bitmap_status_index_requirements() {
192 let status: Status = Status::from_json_value(serde_json::json!({
194 "id": "did:method:0xffff?index=10#rev-0",
195 "type": RevocationBitmapStatus::TYPE,
196 RevocationBitmapStatus::INDEX_PROPERTY: "5",
197 }))
198 .unwrap();
199
200 assert!(matches!(
201 RevocationBitmapStatus::try_from(status).unwrap_err(),
202 Error::InvalidStatus(_)
203 ));
204
205 let status: Status = Status::from_json_value(serde_json::json!({
207 "id": "did:method:0xffff?index=5#rev-0",
208 "type": RevocationBitmapStatus::TYPE,
209 RevocationBitmapStatus::INDEX_PROPERTY: "5",
210 }))
211 .unwrap();
212 assert!(RevocationBitmapStatus::try_from(status).is_ok());
213
214 let status: Status = Status::from_json_value(serde_json::json!({
216 "id": "did:method:0xffff#rev-0",
217 "type": RevocationBitmapStatus::TYPE,
218 RevocationBitmapStatus::INDEX_PROPERTY: "5",
219 }))
220 .unwrap();
221 assert!(RevocationBitmapStatus::try_from(status).is_ok());
222 }
223}