1use itertools::Itertools;
2use luhn::Luhn;
3use std::fmt;
4
5#[derive(Clone)]
7pub struct Uvci {
8 pub version: u8,
10 pub country: String,
12 pub schema_option_number: u8,
14 pub schema_option_desc: String,
16 pub issuing_entity: String,
18 pub vaccine_id: String,
20 pub opaque_unique_string: String,
22 pub opaque_id: String,
24 pub opaque_issuance: String,
26 pub opaque_vaccination_month: u8,
28 pub opaque_vaccination_year: u16,
30 pub checksum: String,
32 pub checksum_verification: bool,
34}
35
36impl fmt::Display for Uvci {
38 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39 write!(
40 f,
41 "version : {}\n\
42 country : {}\n\
43 schema_option_number : {}\n\
44 schema_option_desc : {}\n\
45 issuing_entity : {}\n\
46 vaccine_id : {}\n\
47 opaque_unique_string : {}\n\
48 opaque_id : {}\n\
49 opaque_issuance : {}\n\
50 opaque_vaccination_month : {}\n\
51 opaque_vaccination_year : {}\n\
52 checksum : {}\n\
53 checksum_verification : {}\n",
54 &self.version.to_string(),
55 &self.country,
56 &self.schema_option_number.to_string(),
57 &self.schema_option_desc,
58 &self.issuing_entity,
59 &self.vaccine_id,
60 &self.opaque_unique_string,
61 &self.opaque_id,
62 &self.opaque_issuance,
63 &self.opaque_vaccination_month,
64 &self.opaque_vaccination_year,
65 &self.checksum,
66 &self.checksum_verification
67 )
68 }
69}
70
71pub fn uvci_to_csv(cert_id: &str) -> String {
76 return to_csv(parse(cert_id));
77}
78
79fn to_csv(uvci: Uvci) -> String {
81 let mut output = "".to_string();
82 output.push_str(&uvci.version.to_string());
83 output.push_str(",");
84 output.push_str(&uvci.country);
85 output.push_str(",");
86 output.push_str(&uvci.schema_option_number.to_string());
87 output.push_str(",");
88 output.push_str(&uvci.schema_option_desc);
89 output.push_str(",");
90 output.push_str(&uvci.issuing_entity);
91 output.push_str(",");
92 output.push_str(&uvci.vaccine_id);
93 output.push_str(",");
94 output.push_str(&uvci.opaque_unique_string);
95 output.push_str(",");
96 output.push_str(&uvci.opaque_id);
97 output.push_str(",");
98 output.push_str(&uvci.opaque_issuance);
99 output.push_str(",");
100 output.push_str(&uvci.opaque_vaccination_month.to_string());
101 output.push_str(",");
102 output.push_str(&uvci.opaque_vaccination_year.to_string());
103 output.push_str(",");
104 output.push_str(&uvci.checksum);
105 output.push_str(",");
106 output.push_str(&uvci.checksum_verification.to_string());
107 return output.to_string();
108}
109
110pub fn uvcis_to_graph(cert_ids: &Vec<String>) -> String {
117 let mut cypher_cmd = "".to_string();
118 for cert_id in cert_ids {
119 cypher_cmd.push_str(&uvci_to_graph(cert_id));
120 }
121 let values: Vec<_> = cypher_cmd.split('\n').collect();
123 let values: Vec<_> = values.into_iter().unique().collect();
124 let cypher_output: String = values.into_iter().collect();
125 let cypher_output = cypher_output.replace("CREATE", "\nCREATE");
126 return cypher_output;
127}
128
129pub fn uvci_to_graph(cert_id: &str) -> String {
136 return to_graph(parse(cert_id));
137}
138
139fn to_graph(uvci_data: Uvci) -> String {
145 if !((uvci_data.version == 1)
147 && (uvci_data.country == "SE")
148 && (uvci_data.issuing_entity == "EHM")
149 && (uvci_data.schema_option_number == 3))
150 {
151 return "".to_string();
152 }
153
154 let mut cypher_cmd = "".to_string();
156 let var_country = "Sweden";
157 let var_issuer = "E-Hälso Myndigheten";
158
159 cypher_cmd.push_str("CREATE (");
161 cypher_cmd.push_str(&uvci_data.country);
162 cypher_cmd.push_str(":country {name:'");
163 cypher_cmd.push_str(var_country);
164 cypher_cmd.push_str("'})-[:COUNTRY_OF {}]->(");
165 cypher_cmd.push_str(&uvci_data.issuing_entity);
166 cypher_cmd.push_str(":issuing_entity {name:'");
167 cypher_cmd.push_str(var_issuer);
168 cypher_cmd.push_str("'})\n");
169
170 cypher_cmd.push_str("CREATE (");
172 cypher_cmd.push_str(&uvci_data.issuing_entity);
173 cypher_cmd.push_str(")-[:ISSUER_OF {}]->(");
174 cypher_cmd.push_str(&uvci_data.opaque_id);
175 cypher_cmd.push_str(":opaque_id {name:'");
176 cypher_cmd.push_str(&uvci_data.opaque_id);
177 cypher_cmd.push_str("'})\n");
178
179 let mut var_date_name = "d".to_string();
181 var_date_name.push_str(&uvci_data.opaque_vaccination_year.to_string());
182 var_date_name.push_str(&uvci_data.opaque_vaccination_month.to_string());
183
184 let var_month_name;
185 match uvci_data.opaque_vaccination_month {
186 1 => var_month_name = "Jan".to_string(),
187 2 => var_month_name = "Feb".to_string(),
188 3 => var_month_name = "Mar".to_string(),
189 4 => var_month_name = "Apr".to_string(),
190 5 => var_month_name = "May".to_string(),
191 6 => var_month_name = "Jun".to_string(),
192 7 => var_month_name = "Jul".to_string(),
193 8 => var_month_name = "Aug".to_string(),
194 9 => var_month_name = "Sep".to_string(),
195 10 => var_month_name = "Oct".to_string(),
196 11 => var_month_name = "Nov".to_string(),
197 12 => var_month_name = "Dec".to_string(),
198 _ => var_month_name = "Unknown".to_string(),
199 }
200 let mut var_date_data = "".to_string();
201 var_date_data.push_str(&var_month_name);
202 var_date_data.push_str(" ");
203 var_date_data.push_str(&uvci_data.opaque_vaccination_year.to_string());
204
205 cypher_cmd.push_str("CREATE (");
207 cypher_cmd.push_str(&var_date_name);
208 cypher_cmd.push_str(":vac_date {name:'");
209 cypher_cmd.push_str(&var_date_data);
210 cypher_cmd.push_str("'})\n");
211
212 cypher_cmd.push_str("CREATE (");
214 cypher_cmd.push_str(&var_date_name);
215 cypher_cmd.push_str(")-[:VAC_DATE_OF {}]->(");
216 cypher_cmd.push_str(&uvci_data.opaque_id);
217 cypher_cmd.push_str(")\n");
218
219 cypher_cmd.push_str("CREATE (");
221 cypher_cmd.push_str(&uvci_data.opaque_unique_string);
222 cypher_cmd.push_str(":reissue_id {name:'");
223 cypher_cmd.push_str(&uvci_data.opaque_issuance);
224 cypher_cmd.push_str("'})-[:REISSUE_OF {}]->(");
225 cypher_cmd.push_str(&uvci_data.opaque_id);
226 cypher_cmd.push_str(")\n");
227
228 return cypher_cmd;
230}
231
232pub fn parse(cert_id: &str) -> Uvci {
284 let mut uvci_data = Uvci {
285 version: 0,
286 country: "".to_string(),
287 schema_option_number: 0,
288 schema_option_desc: "".to_string(),
289 issuing_entity: "".to_string(),
290 vaccine_id: "".to_string(),
291 opaque_unique_string: "".to_string(),
292 opaque_id: "".to_string(),
293 opaque_issuance: "".to_string(),
294 opaque_vaccination_month: 0,
295 opaque_vaccination_year: 0,
296 checksum: "".to_string(),
297 checksum_verification: false,
298 };
299
300 if cert_id.is_empty() {
302 return uvci_data;
303 }
304
305 if cert_id.len() > 72 {
307 return uvci_data;
308 }
309
310 let cert_id = cert_id.to_uppercase();
312
313 let mut cert_id2 = cert_id.clone();
315 if !cert_id.starts_with("URN:UVCI:") {
316 cert_id2 = "URN:UVCI:".to_owned() + &cert_id2;
317 }
318 let cert_id = cert_id2;
319
320 let l = Luhn::new("/0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ").expect("invalid alphabet given");
322 uvci_data.checksum_verification = l.validate(rearrange(cert_id.to_string())).unwrap();
323
324 let split_checksum = cert_id.split("#");
326 let vec: Vec<&str> = split_checksum.collect();
327 if vec.len() > 1 {
328 uvci_data.checksum = vec[1].to_string();
329 }
330
331 let split_blocks = vec[0].split(":");
333 let vec: Vec<&str> = split_blocks.collect();
334 if vec[0] != "URN" && vec[1] != "UVCI" {
335 return uvci_data;
336 }
337
338 if vec.len() < 4 {
340 return uvci_data;
341 }
342
343 let temp = vec[2].to_string();
345 if temp.parse::<u8>().is_ok() {
346 uvci_data.version = temp.parse::<u8>().unwrap();
347 }
348
349 uvci_data.country = vec[3].to_string();
351
352 if vec.len() < 5 {
354 return uvci_data;
355 }
356 let split_options = vec[4].split("/");
357 let vec: Vec<&str> = split_options.collect();
358 match vec.len() {
359 3 => {
360 uvci_data.schema_option_number = 1;
361 uvci_data.schema_option_desc = "identifier with semantics".to_string();
362 uvci_data.issuing_entity = vec[0].to_string();
363 uvci_data.vaccine_id = vec[1].to_string();
364 uvci_data.opaque_unique_string = vec[2].to_string();
365 }
366 1 => {
367 uvci_data.schema_option_number = 2;
368 uvci_data.schema_option_desc = "opaque identifier - no structure".to_string();
369 uvci_data.opaque_unique_string = vec[0].to_string();
370 }
371 2 => {
372 uvci_data.schema_option_number = 3;
373 uvci_data.schema_option_desc = "some semantics".to_string();
374 uvci_data.issuing_entity = vec[0].to_string();
375 uvci_data.opaque_unique_string = vec[1].to_string();
376 }
377 _ => (),
378 }
379
380 if (uvci_data.version == 1)
382 && (uvci_data.country == "SE")
383 && (uvci_data.issuing_entity == "EHM")
384 && (uvci_data.schema_option_number == 3)
385 {
386 if uvci_data.opaque_unique_string.len() == 13 {
387 uvci_data.opaque_id = (&uvci_data.opaque_unique_string[0..9]).to_string();
388 uvci_data.opaque_issuance = (&uvci_data.opaque_unique_string[9..13]).to_string();
389
390 let vaccination_date = get_vaccination_date_tan(uvci_data.opaque_id.clone());
391 uvci_data.opaque_vaccination_month = vaccination_date.0;
392 uvci_data.opaque_vaccination_year = vaccination_date.1;
393 }
394 }
395
396 return uvci_data;
397}
398
399fn rearrange(cert_id: String) -> String {
407 let cert_id = cert_id.to_uppercase();
408 let cert_id = cert_id.replace("#", "");
409 let cert_id = cert_id.replace("M", "a");
410 let cert_id = cert_id.replace("N", "b");
411 let cert_id = cert_id.replace("O", "c");
412 let cert_id = cert_id.replace("P", "d");
413 let cert_id = cert_id.replace("Q", "e");
414 let cert_id = cert_id.replace("R", "f");
415 let cert_id = cert_id.replace("S", "g");
416 let cert_id = cert_id.replace("T", "h");
417 let cert_id = cert_id.replace("U", "i");
418 let cert_id = cert_id.replace("V", "j");
419 let cert_id = cert_id.replace("W", "k");
420 let cert_id = cert_id.replace("X", "l");
421 let cert_id = cert_id.replace("Y", "m");
422 let cert_id = cert_id.replace("Z", "m");
423 let cert_id = cert_id.replace("0", "o");
424 let cert_id = cert_id.replace("1", "p");
425 let cert_id = cert_id.replace("2", "q");
426 let cert_id = cert_id.replace("3", "r");
427 let cert_id = cert_id.replace("4", "s");
428 let cert_id = cert_id.replace("5", "t");
429 let cert_id = cert_id.replace("6", "u");
430 let cert_id = cert_id.replace("7", "v");
431 let cert_id = cert_id.replace("8", "w");
432 let cert_id = cert_id.replace("9", "x");
433 let cert_id = cert_id.replace("/", "y");
434 let cert_id = cert_id.replace(":", "z");
435 let cert_id = cert_id.replace("A", "/");
436 let cert_id = cert_id.replace("B", "0");
437 let cert_id = cert_id.replace("C", "1");
438 let cert_id = cert_id.replace("D", "2");
439 let cert_id = cert_id.replace("E", "3");
440 let cert_id = cert_id.replace("F", "4");
441 let cert_id = cert_id.replace("G", "5");
442 let cert_id = cert_id.replace("H", "6");
443 let cert_id = cert_id.replace("I", "7");
444 let cert_id = cert_id.replace("J", "8");
445 let cert_id = cert_id.replace("K", "9");
446 let cert_id = cert_id.replace("L", ":");
447 return cert_id.to_uppercase();
448}
449
450fn get_vaccination_date_tan(opaque_id: String) -> (u8, u16) {
456 let opaque_id = opaque_id.replace("V", "");
458 if !opaque_id.parse::<f32>().is_ok() {
459 return (0, 0);
460 }
461 let mut vaccination_doses = opaque_id.parse::<f32>().unwrap();
462
463 if vaccination_doses < 0.0 {
465 return (0, 0);
466 }
467
468 let mut vaccination_month;
469 if vaccination_doses <= 13983264.0 {
470 vaccination_doses = (6991632.0 - vaccination_doses) / 5536858.0;
472 let mth_f = 5.03 + ((-vaccination_doses.tan()) * 1.6);
473 let mth_u8 = mth_f.round() as u16;
474 vaccination_month = mth_u8;
475 } else {
476 vaccination_month = (vaccination_doses / 1552008.0) as u16;
478 }
479
480 let vaccination_year;
482 if vaccination_month == 0 {
483 vaccination_year = 2020;
484 } else {
485 vaccination_year = ((vaccination_month - 1) / 12) + 2021;
486 }
487
488 if vaccination_month == 0 {
490 vaccination_month = 12;
491 }
492 while vaccination_month > 12 {
493 vaccination_month = vaccination_month - 12;
494 }
495
496 return (vaccination_month as u8, vaccination_year as u16);
498}
499
500#[cfg(test)]
501mod tests {
502 use super::get_vaccination_date_tan;
503 use super::parse;
504 use super::uvci_to_csv;
505
506 #[test]
507 fn uvci_csv() {
508 assert!(
509 uvci_to_csv("URN:UVCI:01:SE:EHM/V00016227TFJJ#Q")
510 == "1,SE,3,some semantics,EHM,,V00016227TFJJ,V00016227,TFJJ,12,2020,Q,false"
511 );
512 }
513
514 #[test]
515 fn swedish_uvci_opaque_date() {
516 assert!(
517 get_vaccination_date_tan("0".to_string()) == (12, 2020),
518 "Dec, Wrong date"
519 );
520 assert!(
521 get_vaccination_date_tan("2014920".to_string()) == (3, 2021),
522 "March, Wrong date"
523 );
524 assert!(
525 get_vaccination_date_tan("6991632".to_string()) == (5, 2021),
526 "May, Wrong date"
527 );
528 assert!(
529 get_vaccination_date_tan("12916227".to_string()) == (8, 2021),
530 "Aug, Wrong date"
531 );
532 assert!(
533 get_vaccination_date_tan("13592955".to_string()) == (9, 2021),
534 "Sep, Wrong date"
535 );
536 assert!(
537 get_vaccination_date_tan("13983264".to_string()) == (10, 2021),
538 "Oct, Wrong date"
539 );
540 assert!(
541 get_vaccination_date_tan("99999999".to_string()) == (4, 2026),
542 "Max, wrong date"
543 );
544 assert!(
546 get_vaccination_date_tan("10427296".to_string()) == (6, 2021),
547 "Single dose, wrong date"
548 );
549 assert!(
550 get_vaccination_date_tan("20854592".to_string()) == (1, 2022),
551 "Double dose, wrong date"
552 );
553 assert!(
554 get_vaccination_date_tan("31281888".to_string()) == (8, 2022),
555 "Double dose + booster, wrong date"
556 );
557 }
558
559 #[test]
560 fn swedish_uvci_opaque_data() {
561 assert!(
562 parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_unique_string == "V12907267LAJW",
563 "wrong opaque_unique_string"
564 );
565 assert!(
566 parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_id == "V12907267",
567 "wrong opaque_id"
568 );
569 assert!(
570 parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_issuance == "LAJW",
571 "wrong opaque_issuance"
572 );
573 assert!(
574 parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_vaccination_month == 8,
575 "wrong opaque_vaccination_month"
576 );
577 assert!(
578 parse("URN:UVCI:01:SE:EHM/V12907267LAJW#E").opaque_vaccination_year == 2021,
579 "wrong opaque_vaccination_month"
580 );
581 }
582
583 #[test]
584 fn swedish_uvci_with_checksum_valid() {
585 let cert_ids_sweden: [&str; 15] = [
586 "URN:UVCI:01:SE:EHM/V12907267LAJW#E",
587 "URN:UVCI:01:SE:EHM/V12916227TFJJ#Q",
588 "URN:UVCI:01:SE:EHM/V12920064NYOH#4",
589 "URN:UVCI:01:SE:EHM/V12923931NNBY#T",
590 "URN:UVCI:01:SE:EHM/V12939008LSVR#F",
591 "URN:UVCI:01:SE:EHM/V12939037PXFJ#V",
592 "URN:UVCI:01:SE:EHM/V12940126MRXQ#N",
593 "URN:UVCI:01:SE:EHM/V12956472WRGE#7",
594 "URN:UVCI:01:SE:EHM/V12965046ALNM#I",
595 "URN:UVCI:01:SE:EHM/V12982924YQMV#T",
596 "URN:UVCI:01:SE:EHM/V12991074UCIC#4",
597 "URN:UVCI:01:SE:EHM/V12993686OVCX#R",
598 "URN:UVCI:01:SE:EHM/V12996544DVKM#M",
599 "URN:UVCI:01:SE:EHM/V12997980ASMG#1",
600 "URN:UVCI:01:SE:EHM/V12998404MNQF#6",
601 ];
602 for cert_id in &cert_ids_sweden {
603 println!("{}\n{}\n", cert_id, parse(cert_id));
604 assert!(
605 parse(cert_id).checksum_verification,
606 "checksum verification failed"
607 );
608 }
609 }
610
611 #[test]
612 fn swedish_uvci_with_checksum_invalid() {
613 let cert_ids_sweden: [&str; 15] = [
614 "URN:UVCI:01:SE:EHM/V12907267LAJW#A",
615 "URN:UVCI:01:SE:EHM/V12916227TFJJ#B",
616 "URN:UVCI:01:SE:EHM/V12920064NYOH#C",
617 "URN:UVCI:01:SE:EHM/V12923931NNBY#D",
618 "URN:UVCI:01:SE:EHM/V12939008LSVR#E",
619 "URN:UVCI:01:SE:EHM/V12939037PXFJ#F",
620 "URN:UVCI:01:SE:EHM/V12940126MRXQ#G",
621 "URN:UVCI:01:SE:EHM/V12956472WRGE#H",
622 "URN:UVCI:01:SE:EHM/V12965046ALNM#0",
623 "URN:UVCI:01:SE:EHM/V12982924YQMV#1",
624 "URN:UVCI:01:SE:EHM/V12991074UCIC#2",
625 "URN:UVCI:01:SE:EHM/V12993686OVCX#3",
626 "URN:UVCI:01:SE:EHM/V12996544DVKM#4",
627 "URN:UVCI:01:SE:EHM/V12997980ASMG#5",
628 "URN:UVCI:01:SE:EHM/V12998404MNQF#9",
629 ];
630 for cert_id in &cert_ids_sweden {
631 println!("{}\n{}\n", cert_id, parse(cert_id));
632 assert!(
633 !parse(cert_id).checksum_verification,
634 "checksum verification failed"
635 );
636 }
637 }
638
639 #[test]
640 fn assorted_uvci() {
641 let cert_ids_assorted: [&str; 18] = [
642 "",
643 "a",
644 "::::::::::",
645 "//////////",
646 "a:a:a:a:a:a:a:a:a:a:a",
647 "URN:UVCI:01:SE://////////",
648 "URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD8131080784F94AEE0E43C25D813#B",
649 "URN:UVCI:01:SE:EHM/C878/123456789ABC",
650 "URN:UVCI:01:SE:EHM/C878/123456789ABC#B",
651 "01:SE:EHM/C878/123456789ABC#B",
652 "URN:UVCI:01:SE:123456789ABC",
653 "URN:UVCI:01:AT:10807843F94AEE0EE5093FBC254BD813#B",
654 "URN:UVCI:01:SE:EHM/V12916227TFJJ#Q",
655 "URN:UVCI:01:NL:187/37512422923",
656 "urn:uvci:01:se:ehm/v12982924yqmv#t",
657 "urn:uvci:98:se:ehm/v12982924yqmv#t",
658 "URN:UVCI:01:IT:84A0F1A35F1D454C96939812CA55D571#F",
659 "01:IT:84A0F1A35F1D454C96939812CA55D571#F",
660 ];
661
662 for cert_id in &cert_ids_assorted {
663 println!("{}\n{}\n", cert_id, parse(cert_id));
664 assert!(
665 parse(cert_id).schema_option_number <= 3,
666 "schema_option_number larger than 3"
667 );
668 }
669 }
670}