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