1use std::fmt;
7
8use concept_graph::ordinal::Ordinal;
9
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum RecordError {
13 #[error("record truncated at byte {at}")]
15 Truncated {
16 at: usize,
18 },
19 #[error("string at byte {at} is not UTF-8")]
21 Utf8 {
22 at: usize,
24 #[source]
26 source: std::str::Utf8Error,
27 },
28 #[error("unknown property value tag {tag}")]
30 Tag {
31 tag: u8,
33 },
34 #[error("{remaining} trailing byte(s) after the record")]
36 Trailing {
37 remaining: usize,
39 },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Concept {
45 pub code: String,
47 pub active: bool,
49 pub effective_time: Option<String>,
51 pub module: Option<Ordinal>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Designation {
58 pub id: Option<String>,
60 pub term: String,
62 pub language: String,
64 pub use_ordinal: u32,
66 pub active: bool,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum PropertyValue {
73 Concept(Ordinal),
75 Code(String),
77 String(String),
79 Integer(i64),
81 Boolean(bool),
83 Decimal(String),
85 DateTime(String),
87}
88
89impl fmt::Display for PropertyValue {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::Concept(o) => write!(f, "{o}"),
93 Self::Code(s) | Self::String(s) | Self::Decimal(s) | Self::DateTime(s) => {
94 f.write_str(s)
95 }
96 Self::Integer(i) => write!(f, "{i}"),
97 Self::Boolean(b) => write!(f, "{b}"),
98 }
99 }
100}
101
102struct Writer(Vec<u8>);
103
104impl Writer {
105 fn u8(&mut self, v: u8) {
106 self.0.push(v);
107 }
108 fn u32(&mut self, v: u32) {
109 self.0.extend_from_slice(&v.to_le_bytes());
110 }
111 fn i64(&mut self, v: i64) {
112 self.0.extend_from_slice(&v.to_le_bytes());
113 }
114 fn str(&mut self, s: &str) {
115 self.u32(u32::try_from(s.len()).unwrap_or(u32::MAX));
116 self.0.extend_from_slice(s.as_bytes());
117 }
118 fn opt_str(&mut self, s: Option<&str>) {
119 match s {
120 Some(s) => {
121 self.u8(1);
122 self.str(s);
123 }
124 None => self.u8(0),
125 }
126 }
127}
128
129struct Reader<'a> {
130 bytes: &'a [u8],
131 at: usize,
132}
133
134impl<'a> Reader<'a> {
135 fn take(&mut self, n: usize) -> Result<&'a [u8], RecordError> {
136 let end = self
137 .at
138 .checked_add(n)
139 .ok_or(RecordError::Truncated { at: self.at })?;
140 let slice = self
141 .bytes
142 .get(self.at..end)
143 .ok_or(RecordError::Truncated { at: self.at })?;
144 self.at = end;
145 Ok(slice)
146 }
147 fn u8(&mut self) -> Result<u8, RecordError> {
148 self.take(1)?
149 .first()
150 .copied()
151 .ok_or(RecordError::Truncated { at: self.at })
152 }
153 fn u32(&mut self) -> Result<u32, RecordError> {
154 let Ok(bytes) = <[u8; 4]>::try_from(self.take(4)?) else {
156 return Err(RecordError::Truncated { at: self.at });
157 };
158 Ok(u32::from_le_bytes(bytes))
159 }
160 fn i64(&mut self) -> Result<i64, RecordError> {
161 let Ok(bytes) = <[u8; 8]>::try_from(self.take(8)?) else {
162 return Err(RecordError::Truncated { at: self.at });
163 };
164 Ok(i64::from_le_bytes(bytes))
165 }
166 fn str(&mut self) -> Result<String, RecordError> {
167 let Ok(len) = usize::try_from(self.u32()?) else {
168 return Err(RecordError::Truncated { at: self.at });
169 };
170 let at = self.at;
171 let bytes = self.take(len)?;
172 std::str::from_utf8(bytes)
173 .map(str::to_owned)
174 .map_err(|source| RecordError::Utf8 { at, source })
175 }
176 fn opt_str(&mut self) -> Result<Option<String>, RecordError> {
177 match self.u8()? {
178 0 => Ok(None),
179 _ => self.str().map(Some),
180 }
181 }
182 fn finish(self) -> Result<(), RecordError> {
183 let remaining = self.bytes.len().saturating_sub(self.at);
184 if remaining == 0 {
185 Ok(())
186 } else {
187 Err(RecordError::Trailing { remaining })
188 }
189 }
190}
191
192impl Concept {
193 #[must_use]
195 pub fn encode(&self) -> Vec<u8> {
196 let mut w = Writer(Vec::new());
197 w.str(&self.code);
198 w.u8(u8::from(self.active));
199 w.opt_str(self.effective_time.as_deref());
200 match self.module {
201 Some(m) => {
202 w.u8(1);
203 w.u32(m.index());
204 }
205 None => w.u8(0),
206 }
207 w.0
208 }
209
210 pub fn decode(bytes: &[u8]) -> Result<Self, RecordError> {
216 let mut r = Reader { bytes, at: 0 };
217 let code = r.str()?;
218 let active = r.u8()? != 0;
219 let effective_time = r.opt_str()?;
220 let module = match r.u8()? {
221 0 => None,
222 _ => Some(Ordinal::new(r.u32()?)),
223 };
224 r.finish()?;
225 Ok(Self {
226 code,
227 active,
228 effective_time,
229 module,
230 })
231 }
232
233 pub fn decode_code(bytes: &[u8]) -> Result<String, RecordError> {
242 code(bytes).map(str::to_owned)
243 }
244}
245
246#[derive(Debug)]
254pub struct Displays;
255
256impl Displays {
257 #[must_use]
263 pub fn encode(entries: &[(u32, &str, &str)]) -> Vec<u8> {
264 let mut w = Writer(Vec::new());
265 w.u32(u32::try_from(entries.len()).unwrap_or(u32::MAX));
266 for (refset, language, term) in entries {
267 w.u32(*refset);
268 w.str(language);
269 w.str(term);
270 }
271 w.0
272 }
273
274 pub fn find(bytes: &[u8], refset: u32) -> Result<Option<(String, String)>, RecordError> {
280 let mut r = Reader { bytes, at: 0 };
281 let count = r.u32()?;
282 for _ in 0..count {
283 let entry_refset = r.u32()?;
284 let language = r.str()?;
285 let term = r.str()?;
286 if entry_refset == refset {
287 return Ok(Some((language, term)));
288 }
289 }
290 Ok(None)
291 }
292}
293
294#[derive(Debug)]
299pub struct Designations;
300
301impl Designations {
302 #[must_use]
304 pub fn encode(entries: &[(u32, &[u8])]) -> Vec<u8> {
305 let mut w = Writer(Vec::new());
306 w.u32(u32::try_from(entries.len()).unwrap_or(u32::MAX));
307 for (index, bytes) in entries {
308 w.u32(*index);
309 w.u32(u32::try_from(bytes.len()).unwrap_or(u32::MAX));
310 w.0.extend_from_slice(bytes);
311 }
312 w.0
313 }
314
315 pub fn decode(bytes: &[u8]) -> Result<Vec<Designation>, RecordError> {
322 let mut out = Vec::new();
323 Self::walk(bytes, |_, record| {
324 out.push(Designation::decode(record)?);
325 Ok(true)
326 })?;
327 Ok(out)
328 }
329
330 pub fn at(bytes: &[u8], index: u32) -> Result<Option<Designation>, RecordError> {
337 let mut found = None;
338 Self::walk(bytes, |at, record| {
339 if at != index {
340 return Ok(true);
341 }
342 found = Some(Designation::decode(record)?);
343 Ok(false)
344 })?;
345 Ok(found)
346 }
347
348 fn walk(
350 bytes: &[u8],
351 mut visit: impl FnMut(u32, &[u8]) -> Result<bool, RecordError>,
352 ) -> Result<(), RecordError> {
353 let mut r = Reader { bytes, at: 0 };
354 let count = r.u32()?;
355 for _ in 0..count {
356 let index = r.u32()?;
357 let Ok(len) = usize::try_from(r.u32()?) else {
358 return Err(RecordError::Truncated { at: r.at });
359 };
360 let record = r.take(len)?;
361 if !visit(index, record)? {
362 return Ok(());
363 }
364 }
365 r.finish()
366 }
367}
368
369#[derive(Debug)]
371pub struct Properties;
372
373impl Properties {
374 #[must_use]
376 pub fn encode(entries: &[(u32, &[u8])]) -> Vec<u8> {
377 let mut w = Writer(Vec::new());
378 w.u32(u32::try_from(entries.len()).unwrap_or(u32::MAX));
379 for (key, values) in entries {
380 w.u32(*key);
381 w.u32(u32::try_from(values.len()).unwrap_or(u32::MAX));
382 w.0.extend_from_slice(values);
383 }
384 w.0
385 }
386
387 pub fn decode(bytes: &[u8]) -> Result<Vec<(u32, Vec<PropertyValue>)>, RecordError> {
394 let mut r = Reader { bytes, at: 0 };
395 let count = r.u32()?;
396 let mut out = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
397 for _ in 0..count {
398 let key = r.u32()?;
399 let Ok(len) = usize::try_from(r.u32()?) else {
400 return Err(RecordError::Truncated { at: r.at });
401 };
402 let values = PropertyValue::decode_list(r.take(len)?)?;
403 out.push((key, values));
404 }
405 r.finish()?;
406 Ok(out)
407 }
408}
409
410#[derive(Debug)]
412pub struct Acceptability;
413
414impl Acceptability {
415 #[must_use]
418 pub fn encode(entries: &[(u32, u32, u32)]) -> Vec<u8> {
419 let mut w = Writer(Vec::new());
420 w.u32(u32::try_from(entries.len()).unwrap_or(u32::MAX));
421 for (index, refset, acceptability) in entries {
422 w.u32(*index);
423 w.u32(*refset);
424 w.u32(*acceptability);
425 }
426 w.0
427 }
428
429 pub fn find(bytes: &[u8], index: u32, refset: u32) -> Result<Option<u32>, RecordError> {
436 for (at, in_refset, acceptability) in Self::decode(bytes)? {
437 if at == index && in_refset == refset {
438 return Ok(Some(acceptability));
439 }
440 }
441 Ok(None)
442 }
443
444 pub fn decode(bytes: &[u8]) -> Result<Vec<(u32, u32, u32)>, RecordError> {
450 let mut r = Reader { bytes, at: 0 };
451 let count = r.u32()?;
452 let mut out = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
453 for _ in 0..count {
454 out.push((r.u32()?, r.u32()?, r.u32()?));
455 }
456 r.finish()?;
457 Ok(out)
458 }
459}
460
461pub fn code(bytes: &[u8]) -> Result<&str, RecordError> {
467 let mut reader = Reader { bytes, at: 0 };
468 let Ok(len) = usize::try_from(reader.u32()?) else {
469 return Err(RecordError::Truncated { at: reader.at });
470 };
471 let at = reader.at;
472 let taken = reader.take(len)?;
473 std::str::from_utf8(taken).map_err(|source| RecordError::Utf8 { at, source })
474}
475
476impl Designation {
477 #[must_use]
479 pub fn encode(&self) -> Vec<u8> {
480 let mut w = Writer(Vec::new());
481 w.opt_str(self.id.as_deref());
482 w.str(&self.term);
483 w.str(&self.language);
484 w.u32(self.use_ordinal);
485 w.u8(u8::from(self.active));
486 w.0
487 }
488
489 pub fn decode(bytes: &[u8]) -> Result<Self, RecordError> {
495 let mut r = Reader { bytes, at: 0 };
496 let designation = Self {
497 id: r.opt_str()?,
498 term: r.str()?,
499 language: r.str()?,
500 use_ordinal: r.u32()?,
501 active: r.u8()? != 0,
502 };
503 r.finish()?;
504 Ok(designation)
505 }
506}
507
508impl PropertyValue {
509 fn encode_into(&self, w: &mut Writer) {
510 match self {
511 Self::Concept(o) => {
512 w.u8(0);
513 w.u32(o.index());
514 }
515 Self::Code(s) => {
516 w.u8(1);
517 w.str(s);
518 }
519 Self::String(s) => {
520 w.u8(2);
521 w.str(s);
522 }
523 Self::Integer(i) => {
524 w.u8(3);
525 w.i64(*i);
526 }
527 Self::Boolean(b) => {
528 w.u8(4);
529 w.u8(u8::from(*b));
530 }
531 Self::Decimal(s) => {
532 w.u8(5);
533 w.str(s);
534 }
535 Self::DateTime(s) => {
536 w.u8(6);
537 w.str(s);
538 }
539 }
540 }
541
542 fn decode_from(r: &mut Reader<'_>) -> Result<Self, RecordError> {
543 Ok(match r.u8()? {
544 0 => Self::Concept(Ordinal::new(r.u32()?)),
545 1 => Self::Code(r.str()?),
546 2 => Self::String(r.str()?),
547 3 => Self::Integer(r.i64()?),
548 4 => Self::Boolean(r.u8()? != 0),
549 5 => Self::Decimal(r.str()?),
550 6 => Self::DateTime(r.str()?),
551 tag => return Err(RecordError::Tag { tag }),
552 })
553 }
554
555 #[must_use]
557 pub fn encode_list(values: &[Self]) -> Vec<u8> {
558 let mut w = Writer(Vec::new());
559 w.u32(u32::try_from(values.len()).unwrap_or(u32::MAX));
560 for value in values {
561 value.encode_into(&mut w);
562 }
563 w.0
564 }
565
566 pub fn decode_list(bytes: &[u8]) -> Result<Vec<Self>, RecordError> {
572 let mut r = Reader { bytes, at: 0 };
573 let count = r.u32()?;
574 let mut values = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
575 for _ in 0..count {
576 values.push(Self::decode_from(&mut r)?);
577 }
578 r.finish()?;
579 Ok(values)
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::{Concept, Designation, PropertyValue, RecordError};
586 use concept_graph::ordinal::Ordinal;
587
588 #[test]
589 fn records_round_trip() {
590 let concept = Concept {
591 code: "123456789".to_owned(),
592 active: true,
593 effective_time: Some("20260101".to_owned()),
594 module: Some(Ordinal::new(7)),
595 };
596 assert_eq!(Concept::decode(&concept.encode()), Ok(concept));
597 let designation = Designation {
598 id: None,
599 term: "Synthetisch kind".to_owned(),
600 language: "nl".to_owned(),
601 use_ordinal: 2,
602 active: false,
603 };
604 assert_eq!(Designation::decode(&designation.encode()), Ok(designation));
605 let values = vec![
606 PropertyValue::Concept(Ordinal::new(3)),
607 PropertyValue::Code("x".to_owned()),
608 PropertyValue::String("s".to_owned()),
609 PropertyValue::Integer(-42),
610 PropertyValue::Boolean(true),
611 PropertyValue::Decimal("2.50".to_owned()),
612 PropertyValue::DateTime("2026-01-01".to_owned()),
613 ];
614 assert_eq!(
615 PropertyValue::decode_list(&PropertyValue::encode_list(&values)),
616 Ok(values)
617 );
618 }
619
620 #[test]
621 fn the_per_concept_records_round_trip() {
622 let first = Designation {
623 id: Some("1".to_owned()),
624 term: "Kat".to_owned(),
625 language: "nl".to_owned(),
626 use_ordinal: 1,
627 active: true,
628 };
629 let second = Designation {
630 id: None,
631 term: "Cat".to_owned(),
632 language: "en".to_owned(),
633 use_ordinal: 0,
634 active: false,
635 };
636 let (a, b) = (first.encode(), second.encode());
637 let packed = super::Designations::encode(&[(0, a.as_slice()), (3, b.as_slice())]);
640 assert_eq!(
641 super::Designations::decode(&packed),
642 Ok(vec![first.clone(), second.clone()])
643 );
644 assert_eq!(super::Designations::at(&packed, 0), Ok(Some(first)));
645 assert_eq!(super::Designations::at(&packed, 3), Ok(Some(second)));
646 assert_eq!(super::Designations::at(&packed, 2), Ok(None));
647
648 let values = PropertyValue::encode_list(&[PropertyValue::Concept(Ordinal::new(4))]);
649 let packed = super::Properties::encode(&[(7, values.as_slice()), (9, &[0, 0, 0, 0])]);
650 assert_eq!(
651 super::Properties::decode(&packed),
652 Ok(vec![
653 (7, vec![PropertyValue::Concept(Ordinal::new(4))]),
654 (9, Vec::new()),
655 ])
656 );
657
658 let packed = super::Acceptability::encode(&[(0, 1, 0), (0, 2, 1)]);
659 assert_eq!(
660 super::Acceptability::decode(&packed),
661 Ok(vec![(0, 1, 0), (0, 2, 1)])
662 );
663 assert_eq!(super::Acceptability::find(&packed, 0, 2), Ok(Some(1)));
664 assert_eq!(super::Acceptability::find(&packed, 1, 2), Ok(None));
665 }
666
667 #[test]
668 fn damaged_per_concept_records_are_refused() {
669 let packed = super::Designations::encode(&[(0, b"x".as_slice())]);
670 assert!(matches!(
671 super::Designations::decode(&packed[..6]),
672 Err(RecordError::Truncated { .. })
673 ));
674 let packed = super::Properties::encode(&[(0, &[1, 0, 0, 0, 9])]);
675 assert!(matches!(
676 super::Properties::decode(&packed),
677 Err(RecordError::Tag { tag: 9 })
678 ));
679 assert!(matches!(
680 super::Acceptability::decode(&[2, 0, 0, 0, 0, 0, 0, 0]),
681 Err(RecordError::Truncated { .. })
682 ));
683 }
684
685 #[test]
686 fn damaged_records_are_refused() {
687 let bytes = Concept {
688 code: "1".to_owned(),
689 active: true,
690 effective_time: None,
691 module: None,
692 }
693 .encode();
694 assert!(matches!(
695 Concept::decode(&bytes[..3]),
696 Err(RecordError::Truncated { .. })
697 ));
698 let mut trailing = bytes.clone();
699 trailing.push(0);
700 assert!(matches!(
701 Concept::decode(&trailing),
702 Err(RecordError::Trailing { remaining: 1 })
703 ));
704 assert!(matches!(
705 PropertyValue::decode_list(&[1, 0, 0, 0, 9]),
706 Err(RecordError::Tag { tag: 9 })
707 ));
708 assert!(matches!(
709 Designation::decode(&[0, 1, 0, 0, 0, 0xff]),
710 Err(RecordError::Utf8 { .. })
711 ));
712 }
713}