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")]
139 InvalidUna,
140
141 #[error("missing required element {element_index} in segment {tag}")]
146 MissingRequiredElement {
147 tag: String,
149 element_index: usize,
151 },
152
153 #[error(
157 "missing required component {component_index} in element {element_index} of segment {tag}"
158 )]
159 MissingRequiredComponent {
160 tag: String,
162 element_index: usize,
164 component_index: usize,
166 },
167
168 #[error("serialized output contains invalid UTF-8")]
173 InvalidUtf8,
174
175 #[error(transparent)]
177 Io(#[from] IoError),
178
179 #[error("segment {tag} is not valid for message type {message_type}")]
184 InvalidSegmentForMessage {
185 tag: String,
187 message_type: String,
189 offset: usize,
191 },
192
193 #[error("segment {tag} has {actual} elements, expected between {min} and {max}")]
197 InvalidElementCount {
198 tag: String,
200 min: usize,
202 max: usize,
204 actual: usize,
206 offset: usize,
208 },
209
210 #[error("segment {tag} element {element_index} has {actual} components, expected {expected}")]
214 InvalidComponentCount {
215 tag: String,
217 element_index: usize,
219 expected: u8,
221 actual: u8,
223 offset: usize,
225 },
226
227 #[error(
232 "segment {tag} element {element_index}: '{value}' is not a valid code (code list {code_list})"
233 )]
234 InvalidCodeValue {
235 tag: String,
237 element_index: usize,
239 value: String,
241 code_list: String,
243 offset: usize,
245 suggestion: Option<&'static str>,
247 },
248
249 #[error("required segment {tag} is missing from message (position {expected_position})")]
253 MissingSegment {
254 tag: String,
256 expected_position: String,
258 },
259
260 #[error("segment {tag} has qualifier '{actual}', expected '{expected}'")]
264 QualifierMismatch {
265 tag: String,
267 actual: String,
269 expected: String,
271 offset: usize,
273 },
274
275 #[error("segment {tag} element {element_index}: conditional requirement not met ({condition})")]
280 ConditionalRequirementNotMet {
281 tag: String,
283 element_index: usize,
285 condition: String,
287 offset: usize,
289 },
290
291 #[error("validation failed with {error_count} error(s)")]
309 ValidationErrors {
310 error_count: usize,
312 report: Box<ValidationReport>,
314 },
315
316 #[error("segment starting at byte offset {offset} exceeded maximum length of {limit} bytes")]
325 SegmentTooLong {
326 offset: usize,
328 limit: usize,
330 },
331
332 #[error("no handler registered for message type {message_type}")]
338 UnexpectedMessageType {
339 message_type: String,
341 },
342
343 #[error("interchange too large: count {count} exceeds u32::MAX")]
350 InterchangeTooLarge {
351 count: u64,
353 },
354
355 #[error("invalid event sequence: {message}")]
363 InvalidEventSequence {
364 message: &'static str,
366 },
367
368 #[error("element definition contains invalid position 0; positions must be >= 1 (one-based)")]
374 InvalidElementPosition,
375
376 #[error("incompatible release scopes: cannot compose {current:?} with {incoming:?}")]
382 IncompatibleReleaseScopes {
383 current: String,
385 incoming: String,
387 },
388
389 #[error("segment {tag} element {element_index}: invalid field value {value:?}")]
395 InvalidFieldValue {
396 tag: String,
398 element_index: usize,
400 value: String,
402 },
403
404 #[error("unexpected data token at byte offset {offset}: data element before segment tag")]
413 UnexpectedDataToken {
414 offset: usize,
416 },
417
418 #[error(
424 "functional group segments (UNG/UNE) at byte offset {offset} are not supported; \
425 strip them before calling validate_envelope"
426 )]
427 FunctionalGroupNotSupported {
428 offset: usize,
430 },
431}
432
433impl From<std::io::Error> for EdifactError {
434 fn from(e: std::io::Error) -> Self {
435 Self::Io(IoError(e))
436 }
437}
438
439impl EdifactError {
440 #[must_use]
442 pub const fn stable_code(&self) -> &'static str {
443 match self {
444 Self::UnexpectedEof { .. } => "E001",
445 Self::InvalidDelimiter { .. } => "E002",
446 Self::InvalidText { .. } => "E003",
447 Self::MessageCountMismatch { .. } => "E004",
448 Self::SegmentCountMismatch { .. } => "E005",
449 Self::InvalidSegmentTag(_) => "E006",
450 Self::InvalidUna => "E007",
451 Self::MissingRequiredElement { .. } => "E008",
452 Self::InvalidUtf8 => "E009",
453 Self::Io(_) => "E010",
454 Self::InvalidSegmentForMessage { .. } => "E011",
455 Self::InvalidElementCount { .. } => "E012",
456 Self::InvalidComponentCount { .. } => "E013",
457 Self::InvalidCodeValue { .. } => "E014",
458 Self::MissingSegment { .. } => "E015",
459 Self::QualifierMismatch { .. } => "E016",
460 Self::ConditionalRequirementNotMet { .. } => "E017",
461 Self::InvalidReleaseSequence { .. } => "E019",
463 Self::SegmentTooLong { .. } => "E020",
464 Self::MissingRequiredComponent { .. } => "E021",
465 Self::UnexpectedMessageType { .. } => "E022",
466 Self::InterchangeTooLarge { .. } => "E023",
467 Self::InvalidEventSequence { .. } => "E024",
468 Self::InvalidElementPosition => "E025",
469 Self::IncompatibleReleaseScopes { .. } => "E026",
470 Self::InvalidFieldValue { .. } => "E027",
471 Self::UnexpectedDataToken { .. } => "E028",
472 Self::FunctionalGroupNotSupported { .. } => "E029",
473 Self::ValidationErrors { .. } => "E030",
474 }
475 }
476
477 #[must_use]
479 pub fn recovery_hint(&self) -> Option<&'static str> {
480 match self {
481 Self::UnexpectedEof { .. } => {
482 Some("Ensure every segment ends with the configured segment terminator")
483 }
484 Self::InvalidDelimiter { .. } => {
485 Some("Check UNA service string advice and delimiter bytes in the payload")
486 }
487 Self::InvalidText { .. } => {
488 Some("Input must be valid UTF-8 text for segment and element values")
489 }
490 Self::InvalidReleaseSequence { .. } => {
491 Some("Release character must escape one following byte; trailing '?' is invalid")
492 }
493 Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
494 Self::InvalidUna => Some(
495 "UNA must be exactly 9 bytes: 'UNA' followed by 6 distinct, non-whitespace service characters",
496 ),
497 Self::MissingRequiredElement { .. } => {
498 Some("Provide all mandatory elements for the segment per directory rules")
499 }
500 Self::MissingRequiredComponent { .. } => Some(
501 "Provide all mandatory components for the composite element per directory rules",
502 ),
503 Self::InvalidSegmentForMessage { .. } => {
504 Some("Remove unsupported segment or switch to the correct message type")
505 }
506 Self::InvalidElementCount { .. } => {
507 Some("Adjust the segment element count to the allowed min/max range")
508 }
509 Self::InvalidComponentCount { .. } => {
510 Some("Fix composite element arity to match the expected component count")
511 }
512 Self::InvalidCodeValue { .. } => {
513 Some("Use a value from the referenced code list for this element")
514 }
515 Self::MissingSegment { .. } => {
516 Some("Insert the required segment at the expected position")
517 }
518 Self::QualifierMismatch { .. } => {
519 Some("Set the segment qualifier to the expected value")
520 }
521 Self::ConditionalRequirementNotMet { .. } => {
522 Some("When the condition is met, include the conditionally required element")
523 }
524 Self::SegmentTooLong { limit, .. } => {
525 let _ = limit; Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
527 }
528 Self::InvalidEventSequence { .. } => {
529 Some("Emit StartSegment before Element, and Element before ComponentElement")
530 }
531 Self::InvalidElementPosition => Some(
532 "Set element position to a value >= 1; positions are one-based (1 = first element slot)",
533 ),
534 Self::IncompatibleReleaseScopes { .. } => Some(
535 "Only compose ProfileRulePack values that share the same release scope, or where at most one has a release scope set",
536 ),
537 Self::InvalidFieldValue { .. } => Some(
538 "Correct the field value to match the expected format or range for this element",
539 ),
540 Self::UnexpectedDataToken { .. } => Some(
541 "A data element appeared before any segment tag; check for partial writes or encoding corruption",
542 ),
543 Self::FunctionalGroupNotSupported { .. } => Some(
544 "Strip UNG/UNE segments before calling validate_envelope, or process the interchange as raw segments",
545 ),
546 Self::ValidationErrors { .. }
547 | Self::MessageCountMismatch { .. }
548 | Self::SegmentCountMismatch { .. }
549 | Self::UnexpectedMessageType { .. }
550 | Self::InterchangeTooLarge { .. }
551 | Self::InvalidUtf8
552 | Self::Io(_) => None,
553 }
554 }
555}
556
557#[cfg(feature = "diagnostics")]
558#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
559impl miette::Diagnostic for EdifactError {
560 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
561 Some(Box::new(self.stable_code()))
562 }
563
564 fn severity(&self) -> Option<miette::Severity> {
565 match self {
566 Self::InvalidCodeValue { .. }
567 | Self::InvalidComponentCount { .. }
568 | Self::QualifierMismatch { .. } => Some(miette::Severity::Warning),
569 _ => Some(miette::Severity::Error),
570 }
571 }
572
573 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
574 match self {
575 Self::InvalidUna => Some(Box::new(
577 "UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
578 )),
579 Self::InvalidUtf8 => Some(Box::new(
580 "Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
581 )),
582 Self::UnexpectedEof { offset } => Some(Box::new(format!(
584 "Check that all segments are terminated with the segment terminator (usually '). \
585 Reached end at offset {offset}",
586 ))),
587 Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
588 "The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
589 Check UNA configuration",
590 ))),
591 Self::InvalidText { offset } => Some(Box::new(format!(
592 "The byte sequence at offset {offset} contains invalid UTF-8. \
593 Ensure input is valid UTF-8",
594 ))),
595 Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
596 "Release character at offset {offset} is dangling. \
597 Ensure '?' is followed by an escaped byte",
598 ))),
599 Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
600 "UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
601 Check the UNZ message count",
602 ))),
603 Self::SegmentCountMismatch {
604 expected,
605 actual,
606 message_ref,
607 } => Some(Box::new(format!(
608 "UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
609 Check the UNT segment count",
610 ))),
611 Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
612 "Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
613 ))),
614 Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
615 "Segment {tag} requires element at index {element_index}",
616 ))),
617 Self::MissingRequiredComponent {
618 tag,
619 element_index,
620 component_index,
621 } => Some(Box::new(format!(
622 "Segment {tag} element {element_index} requires component at index {component_index}",
623 ))),
624 Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
625 Self::InvalidSegmentForMessage {
626 tag, message_type, ..
627 } => Some(Box::new(format!(
628 "Segment {tag} should not appear in a {message_type} message. \
629 Check the directory definition",
630 ))),
631 Self::InvalidElementCount {
632 tag,
633 min,
634 max,
635 actual,
636 ..
637 } => Some(Box::new(format!(
638 "Segment {tag} should have between {min} and {max} elements, but has {actual}. \
639 Check segment structure",
640 ))),
641 Self::InvalidComponentCount {
642 tag,
643 element_index,
644 expected,
645 actual,
646 ..
647 } => Some(Box::new(format!(
648 "In segment {tag}, element {element_index} should have {expected} components \
649 but has {actual}. Check element structure",
650 ))),
651 Self::InvalidCodeValue {
652 tag,
653 element_index,
654 value,
655 code_list,
656 ..
657 } => Some(Box::new(format!(
658 "Value '{value}' in segment {tag} element {element_index} is not in the \
659 {code_list} code list. Check the directory for valid codes",
660 ))),
661 Self::MissingSegment {
662 tag,
663 expected_position,
664 } => Some(Box::new(format!(
665 "Segment {tag} is required at position {expected_position} but is missing. \
666 Add this segment to the message",
667 ))),
668 Self::QualifierMismatch {
669 tag,
670 actual,
671 expected,
672 ..
673 } => Some(Box::new(format!(
674 "Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
675 Check the segment's first component",
676 ))),
677 Self::ConditionalRequirementNotMet {
678 tag,
679 element_index,
680 condition,
681 ..
682 } => Some(Box::new(format!(
683 "In segment {tag}, element {element_index} is conditionally required when: \
684 {condition}. Check if the condition is met",
685 ))),
686 Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
687 "Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
688 Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
689 or verify the input for a missing segment terminator",
690 ))),
691 Self::UnexpectedMessageType { message_type } => Some(Box::new(format!(
692 "No handler was registered for message type '{message_type}'. \
693 Register a handler with MessageDispatch::on(\"{message_type}\", ...)",
694 ))),
695 Self::InterchangeTooLarge { count } => Some(Box::new(format!(
696 "Interchange contains {count} items which exceeds the u32::MAX limit. \
697 This is an extremely unusual input; verify the message is not corrupted.",
698 ))),
699 Self::InvalidEventSequence { message } => Some(Box::new(format!(
700 "Event sequence violation: {message}. \
701 Check that StartSegment is emitted before Element, and Element before ComponentElement.",
702 ))),
703 Self::InvalidElementPosition => Some(Box::new(
704 "Element positions must be >= 1 (one-based). \
705 Ensure no OwnedElementRef is constructed with position == 0",
706 )),
707 Self::IncompatibleReleaseScopes { current, incoming } => Some(Box::new(format!(
708 "Release scope {current:?} and {incoming:?} are incompatible. \
709 Only compose ProfileRulePack values that share the same release scope, \
710 or where at most one carries a release scope",
711 ))),
712 Self::InvalidFieldValue {
713 tag,
714 element_index,
715 value,
716 } => Some(Box::new(format!(
717 "Segment {tag} element {element_index} has invalid value '{value}'. \
718 Check the expected format or range for this field",
719 ))),
720 Self::UnexpectedDataToken { offset } => Some(Box::new(format!(
721 "Data element at offset {offset} appeared before any segment tag. \
722 Check for partial writes or encoding corruption",
723 ))),
724 Self::FunctionalGroupNotSupported { offset } => Some(Box::new(format!(
725 "Functional group segment (UNG/UNE) found at offset {offset}. \
726 Strip UNG/UNE wrappers before calling validate_envelope",
727 ))),
728 Self::ValidationErrors { error_count, .. } => Some(Box::new(format!(
729 "Validation found {error_count} error(s). Inspect the ValidationReport for details",
730 ))),
731 }
732 }
733}
734
735pub use crate::report::ValidationReport;
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742
743 #[test]
744 fn recovery_hint_exists_for_common_malformed_cases() {
745 let err = EdifactError::InvalidReleaseSequence { offset: 10 };
746 assert!(err.recovery_hint().is_some());
747
748 let err = EdifactError::InvalidCodeValue {
749 tag: "BGM".to_owned(),
750 element_index: 0,
751 value: "X".to_owned(),
752 code_list: "1001".to_owned(),
753 offset: 0,
754 suggestion: None,
755 };
756 assert!(err.recovery_hint().is_some());
757 }
758}