1use thiserror::Error;
2
3#[derive(Debug)]
7pub struct IoError(pub(crate) std::io::Error);
8
9impl IoError {
10 pub fn inner(&self) -> &std::io::Error {
12 &self.0
13 }
14}
15
16impl PartialEq for IoError {
17 fn eq(&self, other: &Self) -> bool {
24 self.0.kind() == other.0.kind()
25 }
26}
27
28impl std::fmt::Display for IoError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 self.0.fmt(f)
31 }
32}
33
34impl std::error::Error for IoError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 self.0.source()
37 }
38}
39
40impl From<std::io::Error> for IoError {
41 fn from(e: std::io::Error) -> Self {
42 Self(e)
43 }
44}
45
46#[derive(Debug, Error, PartialEq)]
53#[non_exhaustive]
54pub enum EdifactError {
55 #[error("unexpected end of input at byte offset {offset}")]
60 UnexpectedEof {
61 offset: usize,
63 },
64
65 #[error("invalid delimiter byte 0x{byte:02X} at offset {offset}")]
70 InvalidDelimiter {
71 byte: u8,
73 offset: usize,
75 },
76
77 #[error("invalid EDIFACT text at byte offset {offset}")]
82 InvalidText {
83 offset: usize,
85 },
86
87 #[error("invalid release sequence at byte offset {offset}: dangling release character")]
92 InvalidReleaseSequence {
93 offset: usize,
95 },
96
97 #[error("interchange message count mismatch: UNZ declared {expected}, found {actual}")]
102 MessageCountMismatch {
103 expected: u32,
105 actual: u32,
107 },
108
109 #[error(
114 "segment count mismatch in message {message_ref}: UNT declared {expected}, found {actual}"
115 )]
116 SegmentCountMismatch {
117 expected: u32,
119 actual: u32,
121 message_ref: String,
123 },
124
125 #[error("invalid segment tag {0:?}")]
129 InvalidSegmentTag(String),
130
131 #[error("invalid UNA service string advice")]
140 InvalidUna,
141
142 #[error("missing required element {element_index} in segment {tag}")]
147 MissingRequiredElement {
148 tag: String,
150 element_index: usize,
152 },
153
154 #[error(
158 "missing required component {component_index} in element {element_index} of segment {tag}"
159 )]
160 MissingRequiredComponent {
161 tag: String,
163 element_index: usize,
165 component_index: usize,
167 },
168
169 #[error("serialized output contains invalid UTF-8")]
174 InvalidUtf8,
175
176 #[error(transparent)]
178 Io(#[from] IoError),
179
180 #[error("segment {tag} is not valid for message type {message_type}")]
185 InvalidSegmentForMessage {
186 tag: String,
188 message_type: String,
190 offset: usize,
192 },
193
194 #[error("segment {tag} has {actual} elements, expected between {min} and {max}")]
198 InvalidElementCount {
199 tag: String,
201 min: usize,
203 max: usize,
205 actual: usize,
207 offset: usize,
209 },
210
211 #[error("segment {tag} element {element_index} has {actual} components, expected {expected}")]
215 InvalidComponentCount {
216 tag: String,
218 element_index: usize,
220 expected: u8,
222 actual: u8,
224 offset: usize,
226 },
227
228 #[error(
233 "segment {tag} element {element_index}: '{value}' is not a valid code (code list {code_list})"
234 )]
235 InvalidCodeValue {
236 tag: String,
238 element_index: usize,
240 value: String,
242 code_list: String,
244 offset: usize,
246 suggestion: Option<&'static str>,
248 },
249
250 #[error("required segment {tag} is missing from message (position {expected_position})")]
254 MissingSegment {
255 tag: String,
257 expected_position: String,
259 },
260
261 #[error("segment {tag} has qualifier '{actual}', expected '{expected}'")]
265 QualifierMismatch {
266 tag: String,
268 actual: String,
270 expected: String,
272 offset: usize,
274 },
275
276 #[error("segment {tag} element {element_index}: conditional requirement not met ({condition})")]
281 ConditionalRequirementNotMet {
282 tag: String,
284 element_index: usize,
286 condition: String,
288 offset: usize,
290 },
291
292 #[error("validation failed with {error_count} error(s)")]
310 ValidationErrors {
311 error_count: usize,
313 report: Box<ValidationReport>,
315 },
316
317 #[error("segment starting at byte offset {offset} exceeded maximum length of {limit} bytes")]
326 SegmentTooLong {
327 offset: usize,
329 limit: usize,
331 },
332
333 #[error("no handler registered for message type {message_type}")]
339 UnexpectedMessageType {
340 message_type: String,
342 },
343
344 #[error("interchange too large: count {count} exceeds u32::MAX")]
351 InterchangeTooLarge {
352 count: u64,
354 },
355
356 #[error("invalid event sequence: {message}")]
364 InvalidEventSequence {
365 message: &'static str,
367 },
368
369 #[error("element definition contains invalid position 0; positions must be >= 1 (one-based)")]
375 InvalidElementPosition,
376
377 #[error("incompatible release scopes: cannot compose {current:?} with {incoming:?}")]
383 IncompatibleReleaseScopes {
384 current: String,
386 incoming: String,
388 },
389
390 #[error("segment {tag} element {element_index}: invalid field value {value:?}")]
396 InvalidFieldValue {
397 tag: String,
399 element_index: usize,
401 value: String,
403 },
404
405 #[error("unexpected data token at byte offset {offset}: data element before segment tag")]
414 UnexpectedDataToken {
415 offset: usize,
417 },
418
419 #[error(
425 "unrecognised syntax identifier '{0}': expected UNOA/UNOB/UNOC/UNOD/UNOE/UNOF (or KECA)"
426 )]
427 UnrecognisedSyntaxIdentifier(String),
428
429 #[error("duplicate {tag} reference '{reference}' at byte offset {offset}")]
437 DuplicateReference {
438 tag: String,
440 reference: String,
442 offset: usize,
444 },
445}
446
447impl From<std::io::Error> for EdifactError {
448 fn from(e: std::io::Error) -> Self {
449 Self::Io(IoError(e))
450 }
451}
452
453impl EdifactError {
454 #[must_use]
456 pub const fn stable_code(&self) -> &'static str {
457 match self {
458 Self::UnexpectedEof { .. } => "E001",
459 Self::InvalidDelimiter { .. } => "E002",
460 Self::InvalidText { .. } => "E003",
461 Self::MessageCountMismatch { .. } => "E004",
462 Self::SegmentCountMismatch { .. } => "E005",
463 Self::InvalidSegmentTag(_) => "E006",
464 Self::InvalidUna => "E007",
465 Self::MissingRequiredElement { .. } => "E008",
466 Self::InvalidUtf8 => "E009",
467 Self::Io(_) => "E010",
468 Self::InvalidSegmentForMessage { .. } => "E011",
469 Self::InvalidElementCount { .. } => "E012",
470 Self::InvalidComponentCount { .. } => "E013",
471 Self::InvalidCodeValue { .. } => "E014",
472 Self::MissingSegment { .. } => "E015",
473 Self::QualifierMismatch { .. } => "E016",
474 Self::ConditionalRequirementNotMet { .. } => "E017",
475 Self::InvalidReleaseSequence { .. } => "E019",
477 Self::SegmentTooLong { .. } => "E020",
478 Self::MissingRequiredComponent { .. } => "E021",
479 Self::UnexpectedMessageType { .. } => "E022",
480 Self::InterchangeTooLarge { .. } => "E023",
481 Self::InvalidEventSequence { .. } => "E024",
482 Self::InvalidElementPosition => "E025",
483 Self::IncompatibleReleaseScopes { .. } => "E026",
484 Self::InvalidFieldValue { .. } => "E027",
485 Self::UnexpectedDataToken { .. } => "E028",
486 Self::ValidationErrors { .. } => "E030",
489 Self::UnrecognisedSyntaxIdentifier(_) => "E031",
490 Self::DuplicateReference { .. } => "E032",
491 }
492 }
493
494 #[must_use]
496 pub fn recovery_hint(&self) -> Option<&'static str> {
497 match self {
498 Self::UnexpectedEof { .. } => {
499 Some("Ensure every segment ends with the configured segment terminator")
500 }
501 Self::InvalidDelimiter { .. } => {
502 Some("Check UNA service string advice and delimiter bytes in the payload")
503 }
504 Self::InvalidText { .. } => {
505 Some("Input must be valid UTF-8 text for segment and element values")
506 }
507 Self::InvalidReleaseSequence { .. } => {
508 Some("Release character must escape one following byte; trailing '?' is invalid")
509 }
510 Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
511 Self::InvalidUna => Some(
512 "UNA must be exactly 9 bytes: 'UNA' followed by 6 distinct, non-whitespace service characters",
513 ),
514 Self::MissingRequiredElement { .. } => {
515 Some("Provide all mandatory elements for the segment per directory rules")
516 }
517 Self::MissingRequiredComponent { .. } => Some(
518 "Provide all mandatory components for the composite element per directory rules",
519 ),
520 Self::InvalidSegmentForMessage { .. } => {
521 Some("Remove unsupported segment or switch to the correct message type")
522 }
523 Self::InvalidElementCount { .. } => {
524 Some("Adjust the segment element count to the allowed min/max range")
525 }
526 Self::InvalidComponentCount { .. } => {
527 Some("Fix composite element arity to match the expected component count")
528 }
529 Self::InvalidCodeValue { .. } => {
530 Some("Use a value from the referenced code list for this element")
531 }
532 Self::MissingSegment { .. } => {
533 Some("Insert the required segment at the expected position")
534 }
535 Self::QualifierMismatch { .. } => {
536 Some("Set the segment qualifier to the expected value")
537 }
538 Self::ConditionalRequirementNotMet { .. } => {
539 Some("When the condition is met, include the conditionally required element")
540 }
541 Self::SegmentTooLong { limit, .. } => {
542 let _ = limit; Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
544 }
545 Self::InvalidEventSequence { .. } => {
546 Some("Emit StartSegment before Element, and Element before ComponentElement")
547 }
548 Self::InvalidElementPosition => Some(
549 "Set element position to a value >= 1; positions are one-based (1 = first element slot)",
550 ),
551 Self::IncompatibleReleaseScopes { .. } => Some(
552 "Only compose ProfileRulePack values that share the same release scope, or where at most one has a release scope set",
553 ),
554 Self::InvalidFieldValue { .. } => Some(
555 "Correct the field value to match the expected format or range for this element",
556 ),
557 Self::UnexpectedDataToken { .. } => Some(
558 "A data element appeared before any segment tag; check for partial writes or encoding corruption",
559 ),
560 Self::DuplicateReference { .. } => Some(
561 "Assign a unique control reference to every UNH (DE 0062) and UNG (DE 0048) within an interchange",
562 ),
563 Self::ValidationErrors { .. }
564 | Self::MessageCountMismatch { .. }
565 | Self::SegmentCountMismatch { .. }
566 | Self::UnexpectedMessageType { .. }
567 | Self::InterchangeTooLarge { .. }
568 | Self::UnrecognisedSyntaxIdentifier(_)
569 | Self::InvalidUtf8
570 | Self::Io(_) => None,
571 }
572 }
573}
574
575#[cfg(feature = "diagnostics")]
576#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
577impl miette::Diagnostic for EdifactError {
578 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
579 Some(Box::new(self.stable_code()))
580 }
581
582 fn severity(&self) -> Option<miette::Severity> {
583 match self {
584 Self::InvalidCodeValue { .. }
585 | Self::InvalidComponentCount { .. }
586 | Self::QualifierMismatch { .. } => Some(miette::Severity::Warning),
587 _ => Some(miette::Severity::Error),
588 }
589 }
590
591 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
592 match self {
593 Self::InvalidUna => Some(Box::new(
595 "UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
596 )),
597 Self::InvalidUtf8 => Some(Box::new(
598 "Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
599 )),
600 Self::UnexpectedEof { offset } => Some(Box::new(format!(
602 "Check that all segments are terminated with the segment terminator (usually '). \
603 Reached end at offset {offset}",
604 ))),
605 Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
606 "The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
607 Check UNA configuration",
608 ))),
609 Self::InvalidText { offset } => Some(Box::new(format!(
610 "The byte sequence at offset {offset} contains invalid UTF-8. \
611 Ensure input is valid UTF-8",
612 ))),
613 Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
614 "Release character at offset {offset} is dangling. \
615 Ensure '?' is followed by an escaped byte",
616 ))),
617 Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
618 "UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
619 Check the UNZ message count",
620 ))),
621 Self::SegmentCountMismatch {
622 expected,
623 actual,
624 message_ref,
625 } => Some(Box::new(format!(
626 "UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
627 Check the UNT segment count",
628 ))),
629 Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
630 "Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
631 ))),
632 Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
633 "Segment {tag} requires element at index {element_index}",
634 ))),
635 Self::MissingRequiredComponent {
636 tag,
637 element_index,
638 component_index,
639 } => Some(Box::new(format!(
640 "Segment {tag} element {element_index} requires component at index {component_index}",
641 ))),
642 Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
643 Self::InvalidSegmentForMessage {
644 tag, message_type, ..
645 } => Some(Box::new(format!(
646 "Segment {tag} should not appear in a {message_type} message. \
647 Check the directory definition",
648 ))),
649 Self::InvalidElementCount {
650 tag,
651 min,
652 max,
653 actual,
654 ..
655 } => Some(Box::new(format!(
656 "Segment {tag} should have between {min} and {max} elements, but has {actual}. \
657 Check segment structure",
658 ))),
659 Self::InvalidComponentCount {
660 tag,
661 element_index,
662 expected,
663 actual,
664 ..
665 } => Some(Box::new(format!(
666 "In segment {tag}, element {element_index} should have {expected} components \
667 but has {actual}. Check element structure",
668 ))),
669 Self::InvalidCodeValue {
670 tag,
671 element_index,
672 value,
673 code_list,
674 ..
675 } => Some(Box::new(format!(
676 "Value '{value}' in segment {tag} element {element_index} is not in the \
677 {code_list} code list. Check the directory for valid codes",
678 ))),
679 Self::MissingSegment {
680 tag,
681 expected_position,
682 } => Some(Box::new(format!(
683 "Segment {tag} is required at position {expected_position} but is missing. \
684 Add this segment to the message",
685 ))),
686 Self::QualifierMismatch {
687 tag,
688 actual,
689 expected,
690 ..
691 } => Some(Box::new(format!(
692 "Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
693 Check the segment's first component",
694 ))),
695 Self::ConditionalRequirementNotMet {
696 tag,
697 element_index,
698 condition,
699 ..
700 } => Some(Box::new(format!(
701 "In segment {tag}, element {element_index} is conditionally required when: \
702 {condition}. Check if the condition is met",
703 ))),
704 Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
705 "Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
706 Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
707 or verify the input for a missing segment terminator",
708 ))),
709 Self::UnexpectedMessageType { message_type } => Some(Box::new(format!(
710 "No handler was registered for message type '{message_type}'. \
711 Register a handler with MessageDispatch::on(\"{message_type}\", ...)",
712 ))),
713 Self::InterchangeTooLarge { count } => Some(Box::new(format!(
714 "Interchange contains {count} items which exceeds the u32::MAX limit. \
715 This is an extremely unusual input; verify the message is not corrupted.",
716 ))),
717 Self::InvalidEventSequence { message } => Some(Box::new(format!(
718 "Event sequence violation: {message}. \
719 Check that StartSegment is emitted before Element, and Element before ComponentElement.",
720 ))),
721 Self::InvalidElementPosition => Some(Box::new(
722 "Element positions must be >= 1 (one-based). \
723 Ensure no OwnedElementRef is constructed with position == 0",
724 )),
725 Self::IncompatibleReleaseScopes { current, incoming } => Some(Box::new(format!(
726 "Release scope {current:?} and {incoming:?} are incompatible. \
727 Only compose ProfileRulePack values that share the same release scope, \
728 or where at most one carries a release scope",
729 ))),
730 Self::InvalidFieldValue {
731 tag,
732 element_index,
733 value,
734 } => Some(Box::new(format!(
735 "Segment {tag} element {element_index} has invalid value '{value}'. \
736 Check the expected format or range for this field",
737 ))),
738 Self::UnexpectedDataToken { offset } => Some(Box::new(format!(
739 "Data element at offset {offset} appeared before any segment tag. \
740 Check for partial writes or encoding corruption",
741 ))),
742 Self::ValidationErrors { error_count, .. } => Some(Box::new(format!(
743 "Validation found {error_count} error(s). Inspect the ValidationReport for details",
744 ))),
745 Self::UnrecognisedSyntaxIdentifier(id) => Some(Box::new(format!(
746 "Syntax identifier '{id}' is not defined in ISO 9735-1. \
747 Valid values are UNOA, UNOB, UNOC, UNOD, UNOE, UNOF (or KECA for KEC-A profile)",
748 ))),
749 Self::DuplicateReference { tag, reference, .. } => Some(Box::new(format!(
750 "Reference '{reference}' is used by more than one {tag} in this interchange; \
751 each must be unique so receivers can address messages unambiguously",
752 ))),
753 }
754 }
755}
756
757pub use crate::report::ValidationReport;
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn recovery_hint_exists_for_common_malformed_cases() {
767 let err = EdifactError::InvalidReleaseSequence { offset: 10 };
768 assert!(err.recovery_hint().is_some());
769
770 let err = EdifactError::InvalidCodeValue {
771 tag: "BGM".to_owned(),
772 element_index: 0,
773 value: "X".to_owned(),
774 code_list: "1001".to_owned(),
775 offset: 0,
776 suggestion: None,
777 };
778 assert!(err.recovery_hint().is_some());
779 }
780}