Skip to main content

edi_energy/
parse.rs

1/// Entry points for parsing EDIFACT/EDI@Energy messages.
2///
3/// # Quick start
4///
5/// ```no_run
6/// use edi_energy::{Platform, EdiEnergyMessage};
7///
8/// let input = std::fs::read("message.edi").unwrap();
9/// let msg = Platform::with_all_profiles().parse(&input).unwrap();
10/// if let Some(mt) = msg.try_message_type() { println!("type: {}", mt.as_str()); }
11/// let report = msg.validate().unwrap();
12/// println!("valid: {}", report.is_valid());
13/// ```
14use std::io::{BufReader, Read};
15
16use edifact_rs::{MessageWindowsIter, OwnedSegment, ReaderConfig, from_bufread_stream_with_config};
17
18use crate::{AnyMessage, Error, MessageType};
19
20// ── Security helpers ─────────────────────────────────────────────────────────
21
22/// Sanitize an untrusted release code before including it in any log output.
23///
24/// Valid BDEW release codes are ≤ 16 ASCII alphanumeric characters plus `.`.
25/// Anything outside that set could contain log-injection sequences, ANSI escape
26/// codes, or GDPR-sensitive data that must not appear in operator logs.
27fn sanitize_release_code(s: &str) -> std::borrow::Cow<'_, str> {
28    const MAX_LEN: usize = 16;
29    if s.len() <= MAX_LEN && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '.') {
30        std::borrow::Cow::Borrowed(s)
31    } else {
32        std::borrow::Cow::Owned(format!("<invalid-code:{} bytes>", s.len()))
33    }
34}
35
36// ── PID source lookup ─────────────────────────────────────────────────────────
37
38/// Determine where the Prüfidentifikator lives for the given message type and
39/// release by consulting the given profile registry.
40///
41/// Falls back to [`crate::registry::PidSource::BgmDe1004`] when no profile is
42/// registered (feature disabled, unknown release, etc.).  This is safe because
43/// the `dispatch_message` path for unknown types ultimately produces
44/// [`AnyMessage::Unknown`] regardless.
45fn resolve_pid_source(
46    msg_type_code: &str,
47    assoc_code: &str,
48    registry: &crate::registry::ReleaseRegistry,
49) -> crate::registry::PidSource {
50    MessageType::from_unh_code(msg_type_code)
51        .and_then(|mt| {
52            let rel = crate::release::Release::new(assoc_code);
53            registry.profile(mt, &rel).ok()
54        })
55        .map(super::registry::Profile::pid_source)
56        .unwrap_or_default()
57}
58
59/// Same as `resolve_pid_source` but accessible from `light_message.rs`.
60pub(crate) fn resolve_pid_source_pub(
61    msg_type_code: &str,
62    assoc_code: &str,
63    registry: &crate::registry::ReleaseRegistry,
64) -> crate::registry::PidSource {
65    resolve_pid_source(msg_type_code, assoc_code, registry)
66}
67
68// ── Public API ────────────────────────────────────────────────────────────────
69
70/// Default per-segment byte limit for [`ParseConfig`].
71///
72/// 64 KiB matches the built-in limit of `edifact_rs::from_bytes` and guards
73/// against maliciously crafted oversized segments.
74pub const DEFAULT_MAX_SEGMENT_BYTES: usize = 64 * 1024;
75
76/// Configuration for the EDIFACT byte-slice parser.
77///
78/// Use [`ParseConfig::default()`] for standard EDI@Energy messages.  The
79/// default applies a 64 KiB per-segment limit to protect against `DoS` attacks.
80/// To disable the limit entirely (trusted, size-bounded sources only), set
81/// `max_segment_bytes` to [`usize::MAX`].
82#[derive(Debug, Clone, Copy)]
83pub struct ParseConfig {
84    /// Maximum number of bytes allowed per EDIFACT segment.
85    ///
86    /// Defaults to [`DEFAULT_MAX_SEGMENT_BYTES`] (64 KiB).  Set to
87    /// [`usize::MAX`] to disable the limit for trusted, bounded inputs.
88    pub max_segment_bytes: usize,
89    /// Maximum number of segments to parse before returning an error.
90    ///
91    /// `None` means no limit (default).
92    pub max_segments: Option<usize>,
93    /// Maximum total input bytes to consume before returning an error.
94    ///
95    /// `None` means no limit (default).
96    pub max_input_bytes: Option<usize>,
97    /// Maximum number of messages (UNH…UNT pairs) allowed per interchange.
98    ///
99    /// Defaults to `Some(1000)`.  Set to `None` to disable the limit for
100    /// trusted, size-bounded sources only.  A crafted interchange with many
101    /// lightweight messages inside the per-message limits can otherwise consume
102    /// unbounded memory.
103    pub max_messages_per_interchange: Option<usize>,
104    /// Stop parsing after this many UNH/UNT message pairs have been read.
105    ///
106    /// Maps directly to [`edifact_rs::ReaderConfig::max_messages`] (0.11.0+).
107    /// Useful for sampling large interchanges — e.g., `max_messages: Some(1)`
108    /// extracts only the first message without reading the rest of the file.
109    ///
110    /// `None` (the default) means no limit.
111    pub max_messages: Option<usize>,
112    /// Maximum number of segments allowed within a single EDIFACT message (UNH…UNT pair).
113    ///
114    /// Defaults to `Some(500)`.  Set to `None` to disable the limit for
115    /// trusted, size-bounded sources only.
116    ///
117    /// This limit acts as the primary per-message `DoS` defence: an adversary
118    /// who constructs a valid interchange with one very deep message can
119    /// otherwise drive segment allocations up to the interchange-wide
120    /// `max_segments` ceiling (default 10,000) within a single UNH/UNT pair.
121    /// At 500 segments/message × 1,000 messages the interchange limit still
122    /// binds first; this field provides defence-in-depth for the per-message
123    /// allocation cost.
124    pub max_segments_per_message: Option<usize>,
125    /// Reference date used for profile validity lookups during `validate()`.
126    ///
127    /// When `None` (the default), `time::OffsetDateTime::now_utc().date()` is
128    /// used at validation time.  Set this to a fixed date in tests so that
129    /// profile resolution is deterministic regardless of when the test runs.
130    ///
131    /// # Example
132    /// ```rust
133    /// use edi_energy::ParseConfig;
134    /// let cfg = ParseConfig::default()
135    ///     .with_reference_date(
136    ///         time::Date::from_calendar_date(2026, time::Month::January, 1).unwrap()
137    ///     );
138    /// ```
139    pub reference_date: Option<time::Date>,
140}
141
142impl Default for ParseConfig {
143    fn default() -> Self {
144        Self {
145            max_segment_bytes: DEFAULT_MAX_SEGMENT_BYTES,
146            max_segments: Some(10_000),
147            max_input_bytes: Some(10 * 1024 * 1024),
148            max_messages_per_interchange: Some(1_000),
149            max_segments_per_message: Some(500),
150            max_messages: None,
151            reference_date: None,
152        }
153    }
154}
155
156impl ParseConfig {
157    /// Set the reference date used for profile validity lookups during `validate()`.
158    ///
159    /// See [`ParseConfig::reference_date`] for details.
160    #[must_use]
161    pub fn with_reference_date(mut self, date: time::Date) -> Self {
162        self.reference_date = Some(date);
163        self
164    }
165
166    pub(crate) fn to_reader_config(self) -> ReaderConfig {
167        let mut cfg = ReaderConfig::default().max_segment_bytes(self.max_segment_bytes);
168        if let Some(n) = self.max_segments {
169            cfg = cfg.max_segments(n);
170        }
171        if let Some(n) = self.max_input_bytes {
172            cfg = cfg.max_input_bytes(n as u64);
173        }
174        if let Some(n) = self.max_messages {
175            cfg = cfg.max_messages(n);
176        }
177        cfg
178    }
179}
180
181/// Parse only the UNH/BGM envelope fields from a byte slice, **without**
182/// constructing typed message structs.
183///
184/// Returns a `LightMessage` that exposes message type, release, message
185/// reference, and Prüfidentifikator at minimal cost.  Typed field extraction
186/// (the `Vec<Dtm>`, `Vec<Nad>`, etc. on concrete message structs) is deferred
187/// to `LightMessage::into_message`.
188///
189/// Use this for routing/forwarding paths that must inspect envelope fields
190/// before deciding whether to run full validation or typed access.
191///
192/// The default [`ParseConfig`] is applied.  For custom limits use
193/// [`Parser::parse_envelope_only`].
194///
195/// # Errors
196///
197/// Returns `Err` on EDIFACT syntax errors or a missing UNH segment.
198pub fn parse_envelope_only(input: &[u8]) -> Result<crate::light_message::LightMessage, Error> {
199    let cfg = ParseConfig::default().to_reader_config();
200    let segments: Vec<OwnedSegment> = edifact_rs::from_bytes_owned_with_config(input, cfg)
201        .collect::<Result<_, _>>()
202        .map_err(Error::Parse)?;
203    crate::light_message::LightMessage::from_segments(
204        segments,
205        crate::registry::ReleaseRegistry::global(),
206    )
207}
208
209/// Parse a single EDIFACT message from a byte slice using the global registry.
210///
211/// Applies the default [`ParseConfig`] limits.  For custom limits or a custom
212/// registry, use [`Parser`] directly.
213///
214/// # Errors
215///
216/// Returns `Err` on EDIFACT syntax errors, unknown message type, or profile
217/// lookup failure.
218pub fn parse(input: &[u8]) -> Result<AnyMessage, Error> {
219    parse_with_registry(
220        input,
221        ParseConfig::default(),
222        crate::registry::ReleaseRegistry::global(),
223    )
224}
225
226/// Parse a single message using an explicit registry.
227///
228/// Used by [`Platform::parse`] to avoid the global-registry singleton.
229pub(crate) fn parse_with_registry(
230    input: &[u8],
231    config: ParseConfig,
232    registry: &crate::registry::ReleaseRegistry,
233) -> Result<AnyMessage, Error> {
234    let per_msg_limit = config.max_segments_per_message;
235    let cfg = config.to_reader_config();
236    let segments: Vec<OwnedSegment> = edifact_rs::from_bytes_owned_with_config(input, cfg)
237        .collect::<Result<_, _>>()
238        .map_err(Error::Parse)?;
239    if let Some(lim) = per_msg_limit {
240        let actual = segments.len();
241        if actual > lim {
242            return Err(Error::TooManySegmentsInMessage { limit: lim, actual });
243        }
244    }
245    dispatch_message(segments, registry)
246}
247
248/// Parse all messages from an interchange using the global registry.
249///
250/// Parse all EDIFACT messages from an interchange reader using the global registry.
251///
252/// Applies the default [`ParseConfig`] limits.  For custom limits, use
253/// [`Parser::parse_interchange`].
254///
255/// # Errors
256///
257/// Each item in the iterator is a `Result`; parse errors or unknown message
258/// types are surfaced per-message rather than aborting the whole interchange.
259pub fn parse_interchange(reader: impl Read) -> impl Iterator<Item = Result<AnyMessage, Error>> {
260    parse_interchange_with_registry(reader, ParseConfig::default())
261}
262
263/// Parse all messages from an interchange using the global registry.
264///
265/// Used by the free-function API with the global singleton.  Delegates to
266/// `parse_interchange_impl` with the global `Arc`.
267pub(crate) fn parse_interchange_with_registry(
268    reader: impl Read,
269    config: ParseConfig,
270) -> impl Iterator<Item = Result<AnyMessage, Error>> {
271    parse_interchange_impl(
272        reader,
273        config,
274        std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
275    )
276}
277
278/// Shared implementation for both the `&'static` and `Arc`-owned registry paths.
279///
280/// Eliminates the duplicate `max_messages_per_interchange` / `map` logic that
281/// previously appeared in both `parse_interchange_with_registry` and
282/// `parse_interchange_with_arc_registry`.
283pub(crate) fn parse_interchange_impl(
284    reader: impl Read,
285    config: ParseConfig,
286    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
287) -> impl Iterator<Item = Result<AnyMessage, Error>> {
288    let limit = config.max_messages_per_interchange;
289    let per_msg_limit = config.max_segments_per_message;
290    let cfg = config.to_reader_config();
291    MessageWindowsIter::new(from_bufread_stream_with_config(BufReader::new(reader), cfg))
292        .enumerate()
293        .map(move |(index, window)| {
294            if let Some(lim) = limit {
295                if index >= lim {
296                    return Err(Error::TooManyMessages { limit: lim });
297                }
298            }
299            let window = window.map_err(Error::Parse)?;
300            if let Some(lim) = per_msg_limit {
301                let actual = window.segments.len();
302                if actual > lim {
303                    return Err(Error::TooManySegmentsInMessage { limit: lim, actual });
304                }
305            }
306            dispatch_message(window.segments, &registry)
307        })
308}
309
310/// Parse all messages from an interchange using an `Arc`-owned registry.
311///
312/// Used by [`Platform::parse_interchange`] to avoid the `'static` bound on
313/// the free-function variant.  Delegates to `parse_interchange_impl`.
314pub(crate) fn parse_interchange_with_arc_registry(
315    reader: impl Read,
316    config: ParseConfig,
317    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
318) -> impl Iterator<Item = Result<AnyMessage, Error>> {
319    parse_interchange_impl(reader, config, registry)
320}
321
322/// Shared implementation for the buffered (header-first, lazy-messages) path.
323///
324/// Used by [`Parser::parse_interchange_buffered`] with the global Arc and by
325/// [`Platform`](crate::Platform) with its own isolated registry.  Holds the full
326/// segment Vec in memory for the lifetime of the returned [`InterchangeIter`].
327#[cfg_attr(
328    feature = "tracing",
329    tracing::instrument(skip(reader, config, registry))
330)]
331pub(crate) fn parse_interchange_buffered_impl(
332    reader: impl Read,
333    config: ParseConfig,
334    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
335) -> Result<(crate::interchange::InterchangeHeader, InterchangeIter), Error> {
336    let cfg = config.to_reader_config();
337    let segments: Vec<OwnedSegment> = from_bufread_stream_with_config(BufReader::new(reader), cfg)
338        .collect::<Result<_, _>>()
339        .map_err(Error::Parse)?;
340
341    // Parse UNB header eagerly so callers can route before deserialising messages.
342    let header = parse_interchange_header_from_segments(&segments)?;
343
344    let unz_ref = segments
345        .iter()
346        .rfind(|s| s.tag == "UNZ")
347        .and_then(|unz| unz.element_str(1))
348        .map(str::to_owned);
349    let declared_count = segments
350        .iter()
351        .rfind(|s| s.tag == "UNZ")
352        .and_then(|unz| unz.element_str(0))
353        .and_then(|s| s.parse::<usize>().ok())
354        .unwrap_or(0);
355
356    let msg_iter = MessageWindowsIter::new(segments.into_iter().map(
357        Ok::<_, edifact_rs::EdifactError>
358            as fn(OwnedSegment) -> Result<OwnedSegment, edifact_rs::EdifactError>,
359    ));
360
361    let iter = InterchangeIter {
362        inner: msg_iter,
363        header: header.clone(),
364        registry,
365        limit: config.max_messages_per_interchange,
366        index: 0,
367        actual_count: 0,
368        declared_count,
369        unz_ref,
370        unz_checked: false,
371        done: false,
372    };
373
374    Ok((header, iter))
375}
376
377// ── Parser struct ────────────────────────────────────────────────────────────
378
379/// A configured parser for EDI@Energy messages and interchanges.
380///
381/// `Parser` is the primary API for parsing with custom [`ParseConfig`].
382/// Construct with [`Parser::new`] (default config) or [`Parser::with_config`].
383///
384/// The [`Parser`] API is the primary API for parsing with custom [`ParseConfig`].
385/// Construct with [`Parser::new`] (default config) or [`Parser::with_config`].
386/// Use `Parser` directly whenever you need custom segment limits, a reference date, or the
387/// more advanced interchange paths.
388///
389/// # Example
390///
391/// ```no_run
392/// use edi_energy::{Parser, ParseConfig};
393///
394/// let config = ParseConfig { max_segments: Some(5_000), ..ParseConfig::default() };
395/// let parser = Parser::with_config(config);
396///
397/// // Single message from bytes
398/// let msg = parser.parse(b"UNH+1+UTILMD:D:11A:UN:S2.1'...")?;
399///
400/// // Single message from a reader
401/// let reader = std::fs::File::open("message.edi")?;
402/// let msg = parser.parse_reader(reader)?;
403///
404/// // Interchange — lazy iterator
405/// let reader = std::fs::File::open("interchange.edi")?;
406/// for result in parser.parse_interchange(reader) {
407///     let _msg: edi_energy::AnyMessage = result?;
408/// }
409///
410/// // Interchange — envelope first, messages lazily
411/// let reader = std::fs::File::open("interchange.edi")?;
412/// let (header, iter) = parser.parse_interchange_buffered(reader)?;
413/// println!("sender: {}", header.sender_id);
414/// for result in iter { let env = result?; }
415///
416/// // Interchange — fully materialise into ParsedInterchange
417/// let reader = std::fs::File::open("interchange.edi")?;
418/// let ic = parser.parse_interchange_full(reader)?;
419/// assert!(ic.is_structurally_valid());
420/// # Ok::<(), edi_energy::Error>(())
421/// ```
422#[derive(Debug, Clone)]
423pub struct Parser {
424    config: ParseConfig,
425}
426
427impl Default for Parser {
428    fn default() -> Self {
429        Self::new()
430    }
431}
432
433impl Parser {
434    /// Create a `Parser` with the default [`ParseConfig`].
435    #[must_use]
436    pub fn new() -> Self {
437        Self {
438            config: ParseConfig::default(),
439        }
440    }
441
442    /// Create a `Parser` with the given [`ParseConfig`].
443    #[must_use]
444    pub fn with_config(config: ParseConfig) -> Self {
445        Self { config }
446    }
447
448    /// Parse a single EDI@Energy message from an in-memory byte slice.
449    ///
450    /// # Errors
451    ///
452    /// Returns `Err` on EDIFACT syntax errors or unknown message type.
453    #[cfg_attr(
454        feature = "tracing",
455        tracing::instrument(skip(self, input), fields(bytes = input.len()))
456    )]
457    pub fn parse(&self, input: &[u8]) -> Result<AnyMessage, Error> {
458        parse_with_registry(
459            input,
460            self.config,
461            crate::registry::ReleaseRegistry::global(),
462        )
463    }
464
465    /// Parse a single EDI@Energy message from a [`Read`] source.
466    ///
467    /// Reads the entire source into a segment list, then dispatches to the
468    /// appropriate typed message variant.  For `&[u8]` inputs, prefer
469    /// [`Parser::parse`] to avoid the buffered read.
470    ///
471    /// # Errors
472    ///
473    /// Returns `Err` on I/O errors, EDIFACT syntax errors, or unknown message type.
474    pub fn parse_reader(&self, reader: impl Read) -> Result<AnyMessage, Error> {
475        let cfg = self.config.to_reader_config();
476        let segments: Vec<OwnedSegment> =
477            from_bufread_stream_with_config(BufReader::new(reader), cfg)
478                .collect::<Result<_, _>>()
479                .map_err(Error::Parse)?;
480        dispatch_message(segments, crate::registry::ReleaseRegistry::global())
481    }
482
483    /// Parse only the UNH/BGM envelope fields from a byte slice, without
484    /// constructing typed message structs.
485    ///
486    /// Returns a `LightMessage` that exposes message type, release, message
487    /// reference, and Prüfidentifikator at minimal cost (~zero allocation beyond
488    /// the raw segment buffer).  Useful for routing and forwarding paths that
489    /// must inspect envelope fields before deciding whether to run full validation.
490    ///
491    /// Call `LightMessage::into_message` when full typed access is needed.
492    ///
493    /// # Errors
494    ///
495    /// Returns `Err` on EDIFACT syntax errors or a missing UNH segment.
496    pub fn parse_envelope_only(
497        &self,
498        input: &[u8],
499    ) -> Result<crate::light_message::LightMessage, Error> {
500        let cfg = self.config.to_reader_config();
501        let segments: Vec<OwnedSegment> = edifact_rs::from_bytes_owned_with_config(input, cfg)
502            .collect::<Result<_, _>>()
503            .map_err(Error::Parse)?;
504        crate::light_message::LightMessage::from_segments(
505            segments,
506            crate::registry::ReleaseRegistry::global(),
507        )
508    }
509
510    /// Parse all messages from an EDIFACT interchange (lazy iterator).
511    ///
512    /// Returns a lazy iterator yielding one `Result<AnyMessage, Error>` per
513    /// UNH…UNT message window.  The UNB/UNZ envelope is consumed but not
514    /// preserved; use [`Parser::parse_interchange_buffered`] or
515    /// [`Parser::parse_interchange_full`] when the envelope is needed.
516    ///
517    /// The `max_messages_per_interchange` from the parser's [`ParseConfig`]
518    /// (default: 1 000) is enforced.  Override via [`Parser::with_config`].
519    ///
520    /// # Errors
521    ///
522    /// Each iterator item is `Result<AnyMessage, Error>`.
523    /// - [`Error::Parse`] — I/O or EDIFACT syntax error.
524    /// - [`Error::TooManyMessages`] — interchange exceeds the configured
525    ///   message limit (`ParseConfig::max_messages_per_interchange`).
526    pub fn parse_interchange(
527        &self,
528        reader: impl Read,
529    ) -> impl Iterator<Item = Result<AnyMessage, Error>> {
530        parse_interchange_with_registry(reader, self.config)
531    }
532
533    /// Parse an EDIFACT interchange, returning the `InterchangeHeader` eagerly
534    /// and messages lazily via [`InterchangeIter`].
535    ///
536    /// **Segment tokenization is eager** — the entire input is tokenized into a
537    /// `Vec<OwnedSegment>` before this method returns.  **Message deserialization
538    /// is lazy** — typed struct construction is deferred to each `next()` call.
539    ///
540    /// This is the recommended path for AS4 adapters that must inspect the
541    /// UNB sender/receiver GLN and decide whether to process a message before
542    /// paying the deserialization cost.
543    ///
544    /// The `max_messages_per_interchange` from the parser's [`ParseConfig`]
545    /// (default: 1 000) is enforced during iteration.
546    ///
547    /// # Errors
548    ///
549    /// Returns `Err` eagerly on I/O errors, syntax errors, or a missing UNB.
550    /// Per-message errors and [`Error::TooManyMessages`] are returned as
551    /// `Err` iterator items from the returned [`InterchangeIter`].
552    pub fn parse_interchange_buffered(
553        &self,
554        reader: impl Read,
555    ) -> Result<(crate::interchange::InterchangeHeader, InterchangeIter), Error> {
556        parse_interchange_buffered_impl(
557            reader,
558            self.config,
559            std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
560        )
561    }
562
563    /// Fully parse an EDIFACT interchange into a `ParsedInterchange`, materialising
564    /// all messages eagerly.
565    ///
566    /// Use this when you need all messages and the UNB/UNZ envelope together.  For
567    /// large interchanges prefer [`Parser::parse_interchange_buffered`] to keep memory
568    /// usage proportional to the number of messages you actually need.
569    ///
570    /// Validates the UNZ control reference and message count before returning.
571    ///
572    /// # Errors
573    ///
574    /// Returns `Err` on I/O errors, syntax errors, envelope structural errors
575    /// (missing UNB/UNZ, mismatched control reference or count), or individual
576    /// message parse errors.
577    pub fn parse_interchange_full(
578        &self,
579        reader: impl Read,
580    ) -> Result<crate::interchange::ParsedInterchange, Error> {
581        let reader_cfg = self.config.to_reader_config();
582        let segments: Vec<OwnedSegment> =
583            from_bufread_stream_with_config(BufReader::new(reader), reader_cfg)
584                .collect::<Result<_, _>>()
585                .map_err(Error::Parse)?;
586        parse_interchange_full_from_segments(segments, &self.config)
587    }
588}
589
590/// Lazy iterator over [`MessageEnvelope`][crate::interchange::MessageEnvelope]s
591/// from a parsed EDIFACT interchange.
592///
593/// Returned by [`Parser::parse_interchange_buffered`].
594///
595/// After all messages are yielded, the iterator emits one final `Err` item if
596/// the UNZ control reference or message count is mismatched; after that it
597/// returns `None` permanently.
598pub struct InterchangeIter {
599    #[expect(clippy::type_complexity)]
600    inner: MessageWindowsIter<
601        std::iter::Map<
602            std::vec::IntoIter<OwnedSegment>,
603            fn(OwnedSegment) -> Result<OwnedSegment, edifact_rs::EdifactError>,
604        >,
605    >,
606    header: crate::interchange::InterchangeHeader,
607    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
608    limit: Option<usize>,
609    index: usize,
610    actual_count: usize,
611    declared_count: usize,
612    unz_ref: Option<String>,
613    unz_checked: bool,
614    done: bool,
615}
616
617impl Iterator for InterchangeIter {
618    type Item = Result<crate::interchange::MessageEnvelope, Error>;
619
620    fn next(&mut self) -> Option<Self::Item> {
621        if self.done {
622            return None;
623        }
624
625        // Try to advance the message window iterator.
626        if let Some(window_result) = self.inner.next() {
627            let index = self.index;
628            self.index += 1;
629            self.actual_count += 1;
630
631            // Check per-interchange message limit.
632            if let Some(lim) = self.limit {
633                if index >= lim {
634                    self.done = true;
635                    return Some(Err(Error::TooManyMessages { limit: lim }));
636                }
637            }
638
639            let result = (|| {
640                let window = window_result.map_err(Error::Parse)?;
641                let message = dispatch_message(window.segments, &self.registry)?;
642                Ok(crate::interchange::MessageEnvelope {
643                    message,
644                    header: self.header.clone(),
645                    message_index: index,
646                })
647            })();
648            Some(result)
649        } else {
650            // All message windows exhausted — check UNZ.
651            if self.unz_checked {
652                self.done = true;
653                return None;
654            }
655            self.unz_checked = true;
656            // Validate UNZ control reference.
657            if let Some(ref uref) = self.unz_ref {
658                if !uref.is_empty() && uref.as_str() != self.header.control_ref.as_ref() {
659                    self.done = true;
660                    return Some(Err(Error::InterchangeRefMismatch {
661                        unb_ref: self.header.control_ref.to_string(),
662                        unz_ref: uref.clone(),
663                    }));
664                }
665            }
666            // Validate UNZ message count.
667            if self.declared_count != 0 && self.declared_count != self.actual_count {
668                self.done = true;
669                return Some(Err(Error::InterchangeCountMismatch {
670                    declared: self.declared_count,
671                    actual: self.actual_count,
672                }));
673            }
674            self.done = true;
675            None
676        }
677    }
678}
679
680/// Extract the [`InterchangeHeader`][crate::interchange::InterchangeHeader] from
681/// the UNB segment in a segment list.
682fn parse_interchange_header_from_segments(
683    segments: &[OwnedSegment],
684) -> Result<crate::interchange::InterchangeHeader, Error> {
685    use crate::interchange::InterchangeHeader;
686    let unb = segments
687        .iter()
688        .find(|s| s.tag == "UNB")
689        .ok_or(Error::MissingSegment("UNB"))?;
690
691    let syntax_id = unb.component_str(0, 0).unwrap_or("UNOC").to_owned();
692    let syntax_version: u8 = unb
693        .component_str(0, 1)
694        .and_then(|s| s.parse().ok())
695        .unwrap_or(3);
696    let sender_id = unb.component_str(1, 0).unwrap_or("").to_owned();
697    let sender_qualifier = unb.component_str(1, 2).unwrap_or("").to_owned();
698    let receiver_id = unb.component_str(2, 0).unwrap_or("").to_owned();
699    let receiver_qualifier = unb.component_str(2, 2).unwrap_or("").to_owned();
700    let transmission_datetime = parse_unb_datetime(
701        unb.component_str(3, 0).unwrap_or(""),
702        unb.component_str(3, 1).unwrap_or(""),
703    );
704    let control_ref = unb.element_str(4).unwrap_or("").to_owned();
705    let test_indicator = unb.element_str(10).is_some_and(|v| v.trim() == "1");
706
707    Ok(InterchangeHeader {
708        sender_id: sender_id.into_boxed_str(),
709        sender_qualifier: sender_qualifier.into_boxed_str(),
710        receiver_id: receiver_id.into_boxed_str(),
711        receiver_qualifier: receiver_qualifier.into_boxed_str(),
712        transmission_datetime,
713        control_ref: control_ref.into_boxed_str(),
714        syntax_id: syntax_id.into_boxed_str(),
715        syntax_version,
716        test_indicator,
717    })
718}
719
720/// Core implementation: parse UNB+UNZ envelope and all messages from a flat segment list.
721fn parse_interchange_full_from_segments(
722    segments: Vec<OwnedSegment>,
723    config: &ParseConfig,
724) -> Result<crate::interchange::ParsedInterchange, Error> {
725    parse_interchange_full_from_segments_with_registry(
726        segments,
727        config,
728        std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
729    )
730}
731
732/// Arc-registry variant used by [`Platform::parse_interchange_full`].
733pub(crate) fn parse_interchange_full_with_arc_registry(
734    data: &[u8],
735    config: ParseConfig,
736    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
737) -> Result<crate::interchange::ParsedInterchange, Error> {
738    let cfg = config.to_reader_config();
739    let segments: Vec<OwnedSegment> = edifact_rs::from_bytes_owned_with_config(data, cfg)
740        .collect::<Result<_, _>>()
741        .map_err(Error::Parse)?;
742    parse_interchange_full_from_segments_with_registry(segments, &config, registry)
743}
744
745#[expect(clippy::needless_pass_by_value)]
746fn parse_interchange_full_from_segments_with_registry(
747    segments: Vec<OwnedSegment>,
748    config: &ParseConfig,
749    registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
750) -> Result<crate::interchange::ParsedInterchange, Error> {
751    use crate::interchange::{InterchangeHeader, MessageEnvelope, ParsedInterchange};
752
753    // ── Parse UNB ──────────────────────────────────────────────────────────────
754    let unb = segments
755        .iter()
756        .find(|s| s.tag == "UNB")
757        .ok_or(Error::MissingSegment("UNB"))?;
758
759    // S001: syntax identifier composite — components: [syntax_id, syntax_version]
760    let syntax_id = unb.component_str(0, 0).unwrap_or("UNOC").to_owned();
761    let syntax_version: u8 = unb
762        .component_str(0, 1)
763        .and_then(|s| s.parse().ok())
764        .unwrap_or(3);
765
766    // S002: sender — components: [id, sub_id, id_qualifier, routing]
767    let sender_id = unb.component_str(1, 0).unwrap_or("").to_owned();
768    let sender_qualifier = unb.component_str(1, 2).unwrap_or("").to_owned();
769
770    // S003: receiver — components: [id, sub_id, id_qualifier, routing]
771    let receiver_id = unb.component_str(2, 0).unwrap_or("").to_owned();
772    let receiver_qualifier = unb.component_str(2, 2).unwrap_or("").to_owned();
773
774    // S004: date+time of preparation — components: [date (YYMMDD), time (HHMM)]
775    let transmission_datetime = parse_unb_datetime(
776        unb.component_str(3, 0).unwrap_or(""),
777        unb.component_str(3, 1).unwrap_or(""),
778    );
779
780    // DE 0020: interchange control reference
781    let control_ref = unb.element_str(4).unwrap_or("").to_owned();
782
783    #[cfg(feature = "tracing")]
784    let _span = tracing::debug_span!(
785        "parse_interchange",
786        sender = %sender_id,
787        receiver = %receiver_id,
788        control_ref = %control_ref,
789        segment_count = segments.len(),
790    )
791    .entered();
792
793    // DE 0035: test indicator.  "1" = test message; absent or any other value = production.
794    // Per Allgemeine Festlegungen V6.1d §3: test messages must not be processed as production.
795    let test_indicator = unb.element_str(10).is_some_and(|v| v.trim() == "1");
796
797    let header = InterchangeHeader {
798        sender_id: sender_id.into_boxed_str(),
799        sender_qualifier: sender_qualifier.into_boxed_str(),
800        receiver_id: receiver_id.into_boxed_str(),
801        receiver_qualifier: receiver_qualifier.into_boxed_str(),
802        transmission_datetime,
803        control_ref: control_ref.into_boxed_str(),
804        syntax_id: syntax_id.into_boxed_str(),
805        syntax_version,
806        test_indicator,
807    };
808
809    // ── Parse UNZ ─────────────────────────────────────────────────────────────
810    let unz_seg = segments.iter().rfind(|s| s.tag == "UNZ");
811    let (trailer_ref, declared_message_count) = match unz_seg {
812        Some(unz_seg) => {
813            let count: usize = unz_seg
814                .element_str(0)
815                .and_then(|s| s.parse().ok())
816                .unwrap_or(0);
817            let tref = unz_seg.element_str(1).unwrap_or("").to_owned();
818            (tref.into_boxed_str(), count)
819        }
820        None => ("".into(), 0),
821    };
822
823    // ── Dispatch all messages using MessageWindowsIter ────────────────────────
824    let msg_iter = edifact_rs::MessageWindowsIter::new(
825        segments.into_iter().map(Ok::<_, edifact_rs::EdifactError>),
826    );
827
828    let mut messages: Vec<MessageEnvelope> = Vec::new();
829    for (index, window_result) in msg_iter.enumerate() {
830        // enforce max_messages_per_interchange before parsing the next message.
831        if let Some(limit) = config.max_messages_per_interchange {
832            if index >= limit {
833                return Err(Error::TooManyMessages { limit });
834            }
835        }
836        let window = window_result.map_err(Error::Parse)?;
837        let message = dispatch_message(window.segments, &registry)?;
838        messages.push(MessageEnvelope {
839            message,
840            header: header.clone(),
841            message_index: index,
842        });
843    }
844
845    // validate UNZ control reference matches UNB control reference.
846    if !trailer_ref.is_empty() && trailer_ref.as_ref() != header.control_ref.as_ref() {
847        return Err(Error::InterchangeRefMismatch {
848            unb_ref: header.control_ref.to_string(),
849            unz_ref: trailer_ref.to_string(),
850        });
851    }
852
853    // validate UNZ message count matches actual message count.
854    if declared_message_count != 0 && declared_message_count != messages.len() {
855        return Err(Error::InterchangeCountMismatch {
856            declared: declared_message_count,
857            actual: messages.len(),
858        });
859    }
860
861    Ok(ParsedInterchange {
862        header,
863        messages,
864        trailer_ref,
865        declared_message_count,
866    })
867}
868
869/// Parse a UNB S004 date+time into an `OffsetDateTime`.
870///
871/// EDIFACT date format: `YYMMDD` or `YYYYMMDD`; time format: `HHMM` or `HHMMSS`.
872fn parse_unb_datetime(date: &str, time: &str) -> Option<time::OffsetDateTime> {
873    use time::{Date, Month, OffsetDateTime, Time, UtcOffset};
874
875    let (year, month_n, day) = match date.len() {
876        6 => {
877            // YYMMDD — interpret YY as 20YY (valid for 2000–2099)
878            let yy: i32 = date[0..2].parse().ok()?;
879            let mm: u8 = date[2..4].parse().ok()?;
880            let dd: u8 = date[4..6].parse().ok()?;
881            (2000 + yy, mm, dd)
882        }
883        8 => {
884            let yyyy: i32 = date[0..4].parse().ok()?;
885            let mm: u8 = date[4..6].parse().ok()?;
886            let dd: u8 = date[6..8].parse().ok()?;
887            (yyyy, mm, dd)
888        }
889        _ => return None,
890    };
891
892    let month = Month::try_from(month_n).ok()?;
893    let d = Date::from_calendar_date(year, month, day).ok()?;
894
895    let (hh, mi, ss) = match time.len() {
896        4 => {
897            let hh: u8 = time[0..2].parse().ok()?;
898            let mi: u8 = time[2..4].parse().ok()?;
899            (hh, mi, 0u8)
900        }
901        6 => {
902            let hh: u8 = time[0..2].parse().ok()?;
903            let mi: u8 = time[2..4].parse().ok()?;
904            let ss: u8 = time[4..6].parse().ok()?;
905            (hh, mi, ss)
906        }
907        _ => return None,
908    };
909
910    let t = Time::from_hms(hh, mi, ss).ok()?;
911    Some(OffsetDateTime::new_utc(d, t).replace_offset(UtcOffset::UTC))
912}
913
914// ── Dispatch ─────────────────────────────────────────────────────────────────
915
916/// Inspect the UNH segment and dispatch to the correct [`AnyMessage`] variant,
917/// using the provided registry to look up PID source strategies.
918pub(crate) fn dispatch_message(
919    segments: Vec<OwnedSegment>,
920    registry: &crate::registry::ReleaseRegistry,
921) -> Result<AnyMessage, Error> {
922    // Locate the UNH segment (always the second segment after UNB).
923    // Extract all needed strings before releasing the borrow so `segments` can
924    // be moved into the concrete message constructor below.
925    let (message_ref, msg_type_code, assoc_code) = {
926        let unh = segments
927            .iter()
928            .find(|s| s.tag == "UNH")
929            .ok_or(Error::MissingSegment("UNH"))?;
930
931        let message_ref = unh.element_str(0).unwrap_or_default().to_owned();
932        // S009 composite — element 1:
933        //   component 0: DE 0065 — message type (e.g. "UTILMD")
934        //   component 4: DE 0057 — association assigned code (e.g. "5.5.3a")
935        let msg_type_code = unh
936            .component_str(1, 0)
937            .ok_or(Error::MalformedSegment("UNH"))?
938            .to_owned();
939        let assoc_code = unh.component_str(1, 4).unwrap_or_default().to_owned();
940        (message_ref, msg_type_code, assoc_code)
941    };
942
943    // Prüfidentifikator extraction: look up the profile to determine whether
944    // this message type stores its PID in BGM element 1 (DE 1004) or in a
945    // top-level RFF+Z13 segment.  The strategy is driven by the profile
946    // registry so that no message-type list needs to be maintained here.
947    let pruefidentifikator: Option<u32> =
948        match resolve_pid_source(&msg_type_code, &assoc_code, registry) {
949            crate::registry::PidSource::RffZ13 => segments
950                .iter()
951                .find(|s| s.tag == "RFF" && (s.element_str(0) == Some("Z13")))
952                .and_then(|rff| rff.component_str(0, 1))
953                .and_then(|s| s.parse().ok()),
954            crate::registry::PidSource::BgmDe1004 => segments
955                .iter()
956                .find(|s| s.tag == "BGM")
957                .and_then(|bgm| bgm.element_str(1))
958                .and_then(|s| s.parse().ok()),
959        };
960
961    // Warn when the association code is not one of the recognised EDI@Energy release
962    // patterns.  An `Opaque` release will cause `validate()` to return
963    // `ProfileNotFound`; surfacing the warning here makes it easier to diagnose.
964    if matches!(
965        crate::release::Release::new(&assoc_code).kind(),
966        crate::release::ReleaseKind::Opaque(_)
967    ) {
968        // Sanitize the release code before including it in any log output.
969        // Valid BDEW codes are ≤ 16 ASCII alphanumeric chars plus '.'; anything
970        // else may contain log-injection sequences or GDPR-sensitive data.
971        let safe_code = sanitize_release_code(&assoc_code);
972        #[cfg(feature = "tracing")]
973        tracing::warn!(
974            release = %safe_code,
975            "unrecognised EDI@Energy release code — validate() will return ProfileNotFound"
976        );
977        // Always surface the warning even when the `tracing` feature is disabled,
978        // so misconfigured senders are never silently accepted.
979        #[cfg(not(feature = "tracing"))]
980        eprintln!(
981            "edi-energy: warning: unrecognised release code `{safe_code}` — \
982             validate() will return ProfileNotFound"
983        );
984    } else {
985        #[cfg(feature = "tracing")]
986        tracing::debug!(
987            message_type = %msg_type_code,
988            release = %assoc_code,
989            segment_count = segments.len(),
990            "parsed EDIFACT message"
991        );
992    }
993
994    dispatch_by_type(
995        &msg_type_code,
996        segments,
997        message_ref,
998        assoc_code,
999        pruefidentifikator,
1000    )
1001}
1002
1003/// Dispatch to a concrete `AnyMessage` variant based on the decoded type code.
1004#[allow(unused_variables)] // params unused when all features are disabled
1005#[allow(clippy::too_many_lines)]
1006fn dispatch_by_type(
1007    msg_type_code: &str,
1008    segments: Vec<OwnedSegment>,
1009    message_ref: String,
1010    assoc_code: String,
1011    pruefidentifikator: Option<u32>,
1012) -> Result<AnyMessage, Error> {
1013    match msg_type_code {
1014        #[cfg(feature = "utilmd")]
1015        "UTILMD" => Ok(AnyMessage::Utilmd(
1016            crate::messages::utilmd::UtilmdMessage::from_parts(
1017                segments,
1018                message_ref,
1019                assoc_code,
1020                pruefidentifikator,
1021            ),
1022        )),
1023        #[cfg(feature = "mscons")]
1024        "MSCONS" => Ok(AnyMessage::Mscons(
1025            crate::messages::mscons::MsconsMessage::from_parts(
1026                segments,
1027                message_ref,
1028                assoc_code,
1029                pruefidentifikator,
1030            ),
1031        )),
1032        #[cfg(feature = "aperak")]
1033        "APERAK" => Ok(AnyMessage::Aperak(
1034            crate::messages::aperak::AperakMessage::from_parts(
1035                segments,
1036                message_ref,
1037                assoc_code,
1038                pruefidentifikator,
1039            ),
1040        )),
1041        #[cfg(feature = "contrl")]
1042        "CONTRL" => Ok(AnyMessage::Contrl(
1043            crate::messages::contrl::ContrlMessage::from_parts(
1044                segments,
1045                message_ref,
1046                assoc_code,
1047                pruefidentifikator,
1048            ),
1049        )),
1050        #[cfg(feature = "invoic")]
1051        "INVOIC" => Ok(AnyMessage::Invoic(
1052            crate::messages::invoic::InvoicMessage::from_parts(
1053                segments,
1054                message_ref,
1055                assoc_code,
1056                pruefidentifikator,
1057            ),
1058        )),
1059        #[cfg(feature = "remadv")]
1060        "REMADV" => Ok(AnyMessage::Remadv(
1061            crate::messages::remadv::RemadvMessage::from_parts(
1062                segments,
1063                message_ref,
1064                assoc_code,
1065                pruefidentifikator,
1066            ),
1067        )),
1068        #[cfg(feature = "orders")]
1069        "ORDERS" => Ok(AnyMessage::Orders(
1070            crate::messages::orders::OrdersMessage::from_parts(
1071                segments,
1072                message_ref,
1073                assoc_code,
1074                pruefidentifikator,
1075            ),
1076        )),
1077        #[cfg(feature = "iftsta")]
1078        "IFTSTA" => Ok(AnyMessage::Iftsta(
1079            crate::messages::iftsta::IftstaMessage::from_parts(
1080                segments,
1081                message_ref,
1082                assoc_code,
1083                pruefidentifikator,
1084            ),
1085        )),
1086        #[cfg(feature = "insrpt")]
1087        "INSRPT" => Ok(AnyMessage::Insrpt(
1088            crate::messages::insrpt::InsrptMessage::from_parts(
1089                segments,
1090                message_ref,
1091                assoc_code,
1092                pruefidentifikator,
1093            ),
1094        )),
1095        #[cfg(feature = "reqote")]
1096        "REQOTE" => Ok(AnyMessage::Reqote(
1097            crate::messages::reqote::ReqoteMessage::from_parts(
1098                segments,
1099                message_ref,
1100                assoc_code,
1101                pruefidentifikator,
1102            ),
1103        )),
1104        #[cfg(feature = "partin")]
1105        "PARTIN" => Ok(AnyMessage::Partin(
1106            crate::messages::partin::PartinMessage::from_parts(
1107                segments,
1108                message_ref,
1109                assoc_code,
1110                pruefidentifikator,
1111            ),
1112        )),
1113        #[cfg(feature = "ordchg")]
1114        "ORDCHG" => Ok(AnyMessage::Ordchg(
1115            crate::messages::ordchg::OrdchgMessage::from_parts(
1116                segments,
1117                message_ref,
1118                assoc_code,
1119                pruefidentifikator,
1120            ),
1121        )),
1122        #[cfg(feature = "ordrsp")]
1123        "ORDRSP" => Ok(AnyMessage::Ordrsp(
1124            crate::messages::ordrsp::OrdrespMessage::from_parts(
1125                segments,
1126                message_ref,
1127                assoc_code,
1128                pruefidentifikator,
1129            ),
1130        )),
1131        #[cfg(feature = "quotes")]
1132        "QUOTES" => Ok(AnyMessage::Quotes(
1133            crate::messages::quotes::QuotesMessage::from_parts(
1134                segments,
1135                message_ref,
1136                assoc_code,
1137                pruefidentifikator,
1138            ),
1139        )),
1140        #[cfg(feature = "comdis")]
1141        "COMDIS" => Ok(AnyMessage::Comdis(
1142            crate::messages::comdis::ComdisMessage::from_parts(
1143                segments,
1144                message_ref,
1145                assoc_code,
1146                pruefidentifikator,
1147            ),
1148        )),
1149        #[cfg(feature = "pricat")]
1150        "PRICAT" => Ok(AnyMessage::Pricat(
1151            crate::messages::pricat::PricatMessage::from_parts(
1152                segments,
1153                message_ref,
1154                assoc_code,
1155                pruefidentifikator,
1156            ),
1157        )),
1158        #[cfg(feature = "utilts")]
1159        "UTILTS" => Ok(AnyMessage::Utilts(
1160            crate::messages::utilts::UtiltsMessage::from_parts(
1161                segments,
1162                message_ref,
1163                assoc_code,
1164                pruefidentifikator,
1165            ),
1166        )),
1167        other => {
1168            // Check if this is a known EDI@Energy message type whose Cargo feature
1169            // is not compiled in.  Return FeatureNotEnabled to give the caller
1170            // actionable guidance instead of silently producing Unknown.
1171            if let Some(mt) = MessageType::from_unh_code(other) {
1172                if !mt.is_feature_enabled() {
1173                    return Err(Error::FeatureNotEnabled {
1174                        message_type: other.to_owned(),
1175                        feature: mt.as_str().to_lowercase(),
1176                    });
1177                }
1178            }
1179            // Truly unknown type (not in EDI@Energy): return Unknown for pass-through.
1180            Ok(AnyMessage::Unknown {
1181                message_type_code: other.into(),
1182                release: crate::Release::new(&assoc_code),
1183                message_ref: message_ref.into(),
1184                segments,
1185            })
1186        }
1187    }
1188}
1189
1190// ── MessageType helper ────────────────────────────────────────────────────────
1191
1192impl MessageType {
1193    /// Returns `true` when the Cargo feature for this message type is compiled in.
1194    #[must_use]
1195    pub fn is_feature_enabled(self) -> bool {
1196        match self {
1197            MessageType::Utilmd => cfg!(feature = "utilmd"),
1198            MessageType::Mscons => cfg!(feature = "mscons"),
1199            MessageType::Aperak => cfg!(feature = "aperak"),
1200            MessageType::Contrl => cfg!(feature = "contrl"),
1201            MessageType::Invoic => cfg!(feature = "invoic"),
1202            MessageType::Remadv => cfg!(feature = "remadv"),
1203            MessageType::Orders => cfg!(feature = "orders"),
1204            MessageType::Iftsta => cfg!(feature = "iftsta"),
1205            MessageType::Insrpt => cfg!(feature = "insrpt"),
1206            MessageType::Reqote => cfg!(feature = "reqote"),
1207            MessageType::Partin => cfg!(feature = "partin"),
1208            MessageType::Ordchg => cfg!(feature = "ordchg"),
1209            MessageType::Ordrsp => cfg!(feature = "ordrsp"),
1210            MessageType::Quotes => cfg!(feature = "quotes"),
1211            MessageType::Comdis => cfg!(feature = "comdis"),
1212            MessageType::Pricat => cfg!(feature = "pricat"),
1213            MessageType::Utilts => cfg!(feature = "utilts"),
1214        }
1215    }
1216}