Skip to main content

ccsds_ndm/kvn/
parser.rs

1// SPDX-FileCopyrightText: 2025 Jochim Maene <jochim.maene+github@gmail.com>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! Winnow-based parser combinators for CCSDS KVN format.
6//!
7//! This module provides reusable building blocks for parsing KVN (Key-Value Notation)
8//! files. KVN is a line-oriented format where each line is either:
9//! - A key-value pair: `KEY = value` or `KEY = value [unit]`
10//! - A comment: `COMMENT text`
11//! - A block delimiter: `META_START`, `META_STOP`, `DATA_START`, etc.
12//! - A raw data line (space-separated values)
13//! - An empty line
14//!
15//! # Architecture
16//!
17//! The parsing is split into two layers:
18//! 1. **Line-level**: Parse individual KVN lines into structured tokens
19//! 2. **Message-level**: Compose line parsers to build complete message structures
20
21use crate::common::{AdmHeader, OdmHeader, OpmCovarianceMatrix, SpacecraftParameters, StateVector};
22use crate::error::{
23    CcsdsNdmError, EnumParseError, FormatError, InternalParserError, ValidationError,
24};
25use crate::traits::{CcsdsNullable, FromKvnFloat, FromKvnValue};
26use crate::types::{UserDefined, UserDefinedParameter, *};
27use fast_float;
28use std::str::FromStr;
29use winnow::ascii::{line_ending, space0, till_line_ending};
30use winnow::combinator::{alt, delimited, opt, peek, preceded, repeat, terminated};
31use winnow::error::{
32    AddContext, ErrMode, FromExternalError, ParserError, StrContext, StrContextValue,
33};
34use winnow::prelude::*;
35use winnow::stream::Offset;
36use winnow::token::{one_of, take_till, take_while};
37
38/// A result type for winnow parsers using the library's internal lightweight error type.
39pub type KvnResult<O, E = InternalParserError> = Result<O, ErrMode<E>>;
40
41//----------------------------------------------------------------------
42// Low-level fast parsers
43//----------------------------------------------------------------------
44
45/// Parses a float directly from the input.
46pub fn parse_f64_winnow(input: &mut &str) -> KvnResult<f64> {
47    let s = take_while::<_, _, ()>(1.., ('0'..='9', '.', '-', '+', 'e', 'E'))
48        .parse_next(input)
49        .map_err(|_| cut_err(input, "Invalid float"))?;
50    fast_float::parse(s).map_err(|_| cut_err(input, "Invalid float"))
51}
52
53/// Parses up to the next space or line ending, skipping leading whitespace.
54pub fn till_space<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
55    preceded(ws, take_till(1.., (' ', '\t', '\r', '\n'))).parse_next(input)
56}
57
58/// Parses up to the next space or line ending, or end of input, skipping leading whitespace.
59pub fn till_space_or_eol<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
60    preceded(ws, take_till(1.., (' ', '\t', '\r', '\n'))).parse_next(input)
61}
62
63/// Parses a float and its optional unit from a KVN line, allowing empty input.
64/// Returns Ok((None, u)) if no float found (but valid line end or unit-only).
65pub fn kv_float_unit_opt<'a>(input: &mut &'a str) -> KvnResult<(Option<f64>, Option<&'a str>)> {
66    ws.parse_next(input)?;
67
68    // Try to parse a float (peek first to ensure we don't consume partial match tokens if not float?)
69    // parse_f64_winnow works by take_while. If it takes nothing, it fails.
70    if peek(parse_f64_winnow).parse_next(input).is_ok() {
71        let f = parse_f64_winnow.parse_next(input)?;
72        let u = kv_unit.parse_next(input)?;
73        opt_line_ending.parse_next(input)?;
74        Ok((Some(f), u))
75    } else {
76        // No float. Could be just unit, or empty.
77        let u = kv_unit.parse_next(input)?;
78
79        // After optional unit, we MUST be at end of line/comment or whitespace.
80        // If there's still non-whitespace content, it's invalid (garbage).
81        let remainder = till_line_ending.parse_next(input)?;
82        if !remainder.is_null() {
83            return Err(cut_err(input, "Invalid float value"));
84        }
85
86        opt_line_ending.parse_next(input)?;
87        Ok((None, u))
88    }
89}
90
91//----------------------------------------------------------------------
92// Error Handling
93//----------------------------------------------------------------------
94
95/// Converts a winnow error to our library's error type.
96pub fn to_ccsds_error(
97    input: &str,
98    err: winnow::error::ParseError<&str, InternalParserError>,
99) -> CcsdsNdmError {
100    let offset = err.offset();
101    let inner = err.into_inner();
102
103    let base_err = match *inner.kind {
104        crate::error::ParserErrorKind::Validation(e) => CcsdsNdmError::Validation(Box::new(e)),
105        crate::error::ParserErrorKind::Epoch(e) => CcsdsNdmError::Epoch(e),
106        crate::error::ParserErrorKind::Enum(e) => {
107            CcsdsNdmError::Format(Box::new(FormatError::Enum(e)))
108        }
109        crate::error::ParserErrorKind::ParseInt(e) => {
110            CcsdsNdmError::Format(Box::new(FormatError::ParseInt(e)))
111        }
112        crate::error::ParserErrorKind::ParseFloat(e) => {
113            CcsdsNdmError::Format(Box::new(FormatError::ParseFloat(e)))
114        }
115        crate::error::ParserErrorKind::MissingRequiredField { block, field } => {
116            return CcsdsNdmError::Validation(Box::new(ValidationError::MissingRequiredField {
117                block: std::borrow::Cow::Borrowed(block),
118                field: std::borrow::Cow::Borrowed(field),
119                line: None,
120            }))
121            .with_location(input, offset);
122        }
123        _ => {
124            let message = inner.message;
125
126            let raw = crate::error::RawParsePosition {
127                offset,
128                message,
129                contexts: inner.contexts,
130            };
131            CcsdsNdmError::Format(Box::new(FormatError::Kvn(Box::new(
132                raw.into_parse_error(input),
133            ))))
134        }
135    };
136
137    base_err.with_location(input, offset)
138}
139
140/// Creates a winnow ErrMode::Cut with a static context label.
141pub fn cut_err(input: &mut &str, label: &'static str) -> ErrMode<InternalParserError> {
142    ErrMode::Cut(InternalParserError::from_input(input).add_context(
143        input,
144        &input.checkpoint(),
145        StrContext::Label(label),
146    ))
147}
148
149/// Creates a winnow ErrMode::Cut for a missing required field.
150pub fn missing_field_err(
151    _input: &mut &str,
152    block: &'static str,
153    field: &'static str,
154) -> ErrMode<InternalParserError> {
155    ErrMode::Cut(InternalParserError {
156        message: std::borrow::Cow::Borrowed(""),
157        contexts: crate::error::ContextStack::new(),
158        kind: Box::new(crate::error::ParserErrorKind::MissingRequiredField { block, field }),
159    })
160}
161
162/// Extracts a required field from an optional parser result.
163pub fn require_field<T>(
164    input: &mut &str,
165    block: &'static str,
166    field: &'static str,
167    value: Option<T>,
168) -> KvnResult<T> {
169    value.ok_or_else(|| missing_field_err(input, block, field))
170}
171
172//----------------------------------------------------------------------
173// Low-level Token Parsers
174//----------------------------------------------------------------------
175
176/// Parses optional whitespace (spaces and tabs only, not newlines).
177pub fn ws<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
178    space0.parse_next(input)
179}
180
181/// Parses a KVN keyword (uppercase letters, digits, underscores).
182/// Keywords must start with a letter.
183pub fn keyword<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
184    (
185        one_of('A'..='Z'),
186        take_while(0.., ('A'..='Z', '0'..='9', '_')),
187    )
188        .take()
189        .parse_next(input)
190}
191
192/// Parses the `= ` separator in a key-value pair.
193pub fn kv_sep(input: &mut &str) -> KvnResult<()> {
194    (ws, '=', ws).void().parse_next(input)
195}
196
197/// Parses an optional unit in brackets: `[unit]`
198/// If a `[` is encountered, a matching `]` is strictly enforced.
199pub fn kv_unit<'a>(input: &mut &'a str) -> KvnResult<Option<&'a str>> {
200    ws.parse_next(input)?;
201    if input.starts_with('[') {
202        let u = delimited('[', take_till(0.., |c: char| c == ']'), ']')
203            .context(StrContext::Label("unit in brackets"))
204            .parse_next(input)?;
205        Ok(Some(u))
206    } else {
207        Ok(None)
208    }
209}
210
211/// Parses the value part of a key-value pair.
212/// Handles values with or without units.
213pub fn kvn_value<'a>(input: &mut &'a str) -> KvnResult<(&'a str, Option<&'a str>)> {
214    let val = take_till(0.., |c: char| c == '[' || c == '\r' || c == '\n')
215        .map(|s: &str| s.trim())
216        .parse_next(input)?;
217
218    if val.is_empty() {
219        // If it starts with '[', it could be a unit OR the value itself could start with '['
220        // (like MAN_UNITS = [n/a, ...])
221        // In KVN, units are typically at the end.
222        // Let's take everything till the end of the line.
223        let rest = till_line_ending.parse_next(input)?;
224        let trimmed = rest.trim();
225        Ok((trimmed, None))
226    } else {
227        let unit = kv_unit.parse_next(input)?;
228        Ok((val, unit))
229    }
230}
231
232//----------------------------------------------------------------------
233// Line-level Parsers
234//----------------------------------------------------------------------
235
236/// A parsed KVN line.
237#[derive(Debug, Clone, PartialEq)]
238pub enum KvnToken<'a> {
239    /// A key-value pair with optional unit.
240    KeyValue {
241        key: &'a str,
242        value: &'a str,
243        unit: Option<&'a str>,
244    },
245    /// A comment line.
246    Comment(&'a str),
247    /// A block start marker (e.g., "META" from "META_START").
248    BlockStart(&'a str),
249    /// A block end marker (e.g., "META" from "META_STOP").
250    BlockEnd(&'a str),
251    /// A raw data line (space-separated values).
252    Raw(&'a str),
253    /// An empty line.
254    Empty,
255}
256
257/// Parses a COMMENT line.
258pub fn comment_line<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
259    preceded((ws, "COMMENT", space0), till_line_ending).parse_next(input)
260}
261
262/// Parses a key-value pair line.
263pub fn key_value_line<'a>(input: &mut &'a str) -> KvnResult<(&'a str, &'a str, Option<&'a str>)> {
264    (preceded(ws, keyword), kv_sep, kvn_value)
265        .map(|(key, _, (value, unit))| (key, value, unit))
266        .parse_next(input)
267}
268
269/// Parses a block start marker (e.g., META_START).
270pub fn block_start<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
271    let content = preceded(ws, till_line_ending).parse_next(input)?;
272    let content = content.trim();
273
274    if let Some(prefix) = content.strip_suffix("_START") {
275        if !prefix.contains(char::is_whitespace) {
276            return Ok(prefix);
277        }
278    }
279    Err(ErrMode::Backtrack(InternalParserError::from_input(input)))
280}
281
282/// Parses a block end marker (e.g., META_STOP or COVARIANCE_END).
283pub fn block_end<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
284    let content = preceded(ws, till_line_ending).parse_next(input)?;
285    let content = content.trim();
286
287    if let Some(prefix) = content.strip_suffix("_STOP") {
288        if !prefix.contains(char::is_whitespace) {
289            return Ok(prefix);
290        }
291    } else if let Some(prefix) = content.strip_suffix("_END") {
292        if !prefix.contains(char::is_whitespace) {
293            return Ok(prefix);
294        }
295    }
296    Err(ErrMode::Backtrack(InternalParserError::from_input(input)))
297}
298
299/// Parses an empty line.
300pub fn empty_line(input: &mut &str) -> KvnResult<()> {
301    (
302        ws,
303        peek(alt((line_ending.void(), winnow::combinator::eof.void()))),
304    )
305        .void()
306        .parse_next(input)
307}
308
309/// Parses a raw data line (no equals sign, not a keyword).
310pub fn raw_line<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
311    let content = preceded(ws, till_line_ending).parse_next(input)?;
312    let trimmed = content.trim();
313
314    // Raw lines should not be empty, comments, or contain '='
315    if trimmed.is_empty()
316        || trimmed.starts_with("COMMENT")
317        || trimmed.contains('=')
318        || trimmed.ends_with("_START")
319        || trimmed.ends_with("_STOP")
320        || trimmed.ends_with("_END")
321    {
322        return Err(ErrMode::Backtrack(InternalParserError::from_input(input)));
323    }
324
325    Ok(trimmed)
326}
327
328/// Parses any KVN line into a token.
329pub fn kvn_token<'a>(input: &mut &'a str) -> KvnResult<KvnToken<'a>> {
330    // Skip leading whitespace on the line
331    ws.parse_next(input)?;
332
333    alt((
334        empty_line.map(|_| KvnToken::Empty),
335        comment_line.map(KvnToken::Comment),
336        block_start.map(KvnToken::BlockStart),
337        block_end.map(KvnToken::BlockEnd),
338        key_value_line.map(|(k, v, u)| KvnToken::KeyValue {
339            key: k,
340            value: v,
341            unit: u,
342        }),
343        raw_line.map(KvnToken::Raw),
344    ))
345    .parse_next(input)
346}
347
348/// Parses "KEY =" and returns the key.
349pub fn key_token<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
350    terminated(preceded(ws, keyword), kv_sep).parse_next(input)
351}
352
353/// Parses the rest of a KVN line (value and optional unit).
354pub fn kv_rest<'a>(input: &mut &'a str) -> KvnResult<(&'a str, Option<&'a str>)> {
355    terminated(kvn_value, opt_line_ending).parse_next(input)
356}
357
358/// Fast float parser for KVN values.
359pub fn kv_float(input: &mut &str) -> KvnResult<f64> {
360    let checkpoint = input.checkpoint();
361    terminated(
362        (
363            parse_f64_winnow.context(StrContext::Label("float")),
364            kv_unit,
365        )
366            .map(|(f, _)| f),
367        opt_line_ending,
368    )
369    .parse_next(input)
370    .map_err(|e| {
371        if e.is_backtrack() {
372            let mut err = InternalParserError::from_input(input);
373            err.message = std::borrow::Cow::Borrowed("Invalid float");
374            ErrMode::Cut(err.add_context(input, &checkpoint, StrContext::Label("Invalid float")))
375        } else {
376            e
377        }
378    })
379}
380
381/// Fast i32 parser for KVN values.
382pub fn kv_i32(input: &mut &str) -> KvnResult<i32> {
383    let checkpoint = input.checkpoint();
384    terminated(
385        (
386            take_while(1.., ('0'..='9', '-', '+'))
387                .map(|s: &str| s.parse::<i32>())
388                .verify(|res| res.is_ok())
389                .map(|res| res.unwrap()),
390            kv_unit,
391        )
392            .map(|(i, _)| i),
393        opt_line_ending,
394    )
395    .parse_next(input)
396    .map_err(|e| {
397        if e.is_backtrack() {
398            let mut err = InternalParserError::from_input(input);
399            err.message = std::borrow::Cow::Borrowed("Invalid integer");
400            ErrMode::Cut(err.add_context(input, &checkpoint, StrContext::Label("Invalid integer")))
401        } else {
402            e
403        }
404    })
405}
406
407/// Fast u32 parser for KVN values.
408pub fn kv_u32(input: &mut &str) -> KvnResult<u32> {
409    let checkpoint = input.checkpoint();
410    terminated(
411        (
412            take_while(1.., '0'..='9')
413                .map(|s: &str| s.parse::<u32>())
414                .verify(|res| res.is_ok())
415                .map(|res| res.unwrap()),
416            kv_unit,
417        )
418            .map(|(u, _)| u),
419        opt_line_ending,
420    )
421    .parse_next(input)
422    .map_err(|e| {
423        if e.is_backtrack() {
424            let mut err = InternalParserError::from_input(input);
425            err.message = std::borrow::Cow::Borrowed("Invalid unsigned integer");
426            ErrMode::Cut(err.add_context(
427                input,
428                &checkpoint,
429                StrContext::Label("Invalid unsigned integer"),
430            ))
431        } else {
432            e
433        }
434    })
435}
436
437/// Parses an optional u32 value from a KVN line.
438pub fn kv_u32_opt(input: &mut &str) -> KvnResult<Option<u32>> {
439    let checkpoint = input.checkpoint();
440    ws.parse_next(input)?;
441
442    // Check if line contains only whitespace/unit or is empty
443    let remainder = peek(till_line_ending).parse_next(input)?;
444    if remainder.is_null() || remainder.trim().starts_with('[') {
445        let _ = kv_unit.parse_next(input)?;
446        opt_line_ending.parse_next(input)?;
447        return Ok(None);
448    }
449
450    terminated(
451        (
452            take_while(1.., '0'..='9')
453                .map(|s: &str| s.parse::<u32>())
454                .verify(|res| res.is_ok())
455                .map(|res| res.ok()),
456            kv_unit,
457        )
458            .map(|(u, _)| u),
459        opt_line_ending,
460    )
461    .parse_next(input)
462    .map_err(|e| {
463        if e.is_backtrack() {
464            let mut err = InternalParserError::from_input(input);
465            err.message = std::borrow::Cow::Borrowed("Invalid unsigned integer");
466            ErrMode::Cut(err.add_context(
467                input,
468                &checkpoint,
469                StrContext::Label("Invalid unsigned integer"),
470            ))
471        } else {
472            e
473        }
474    })
475}
476
477/// Fast u64 parser for KVN values.
478pub fn kv_u64(input: &mut &str) -> KvnResult<u64> {
479    let checkpoint = input.checkpoint();
480    terminated(
481        (
482            take_while(1.., '0'..='9')
483                .map(|s: &str| s.parse::<u64>())
484                .verify(|res| res.is_ok())
485                .map(|res| res.unwrap()),
486            kv_unit,
487        )
488            .map(|(u, _)| u),
489        opt_line_ending,
490    )
491    .parse_next(input)
492    .map_err(|e| {
493        if e.is_backtrack() {
494            let mut err = InternalParserError::from_input(input);
495            err.message = std::borrow::Cow::Borrowed("Invalid unsigned integer");
496            ErrMode::Cut(err.add_context(
497                input,
498                &checkpoint,
499                StrContext::Label("Invalid unsigned integer"),
500            ))
501        } else {
502            e
503        }
504    })
505}
506
507/// Parses an optional u64 value from a KVN line.
508pub fn kv_u64_opt(input: &mut &str) -> KvnResult<Option<u64>> {
509    let checkpoint = input.checkpoint();
510    ws.parse_next(input)?;
511
512    // Check if line contains only whitespace/unit or is empty
513    let remainder = peek(till_line_ending).parse_next(input)?;
514    if remainder.is_null() || remainder.trim().starts_with('[') {
515        let _ = kv_unit.parse_next(input)?;
516        opt_line_ending.parse_next(input)?;
517        return Ok(None);
518    }
519
520    terminated(
521        (
522            take_while(1.., '0'..='9')
523                .map(|s: &str| s.parse::<u64>())
524                .verify(|res| res.is_ok())
525                .map(|res| res.ok()),
526            kv_unit,
527        )
528            .map(|(u, _)| u),
529        opt_line_ending,
530    )
531    .parse_next(input)
532    .map_err(|e| {
533        if e.is_backtrack() {
534            let mut err = InternalParserError::from_input(input);
535            err.message = std::borrow::Cow::Borrowed("Invalid unsigned integer");
536            ErrMode::Cut(err.add_context(
537                input,
538                &checkpoint,
539                StrContext::Label("Invalid unsigned integer"),
540            ))
541        } else {
542            e
543        }
544    })
545}
546
547/// Skips whitespace and empty lines.
548pub fn skip_empty_lines(input: &mut &str) -> KvnResult<()> {
549    repeat(0.., (space0, line_ending))
550        .map(|_: ()| ()) // Corrected: map to () instead of consuming the tuple
551        .parse_next(input)
552}
553
554/// Parses an optional line ending, consuming any trailing horizontal whitespace.
555pub fn opt_line_ending(input: &mut &str) -> KvnResult<()> {
556    (space0, opt(line_ending)).void().parse_next(input)
557}
558
559//----------------------------------------------------------------------
560// Direct Value Parsers
561//----------------------------------------------------------------------
562
563/// Parses a string value from a KVN line.
564pub fn kv_string(input: &mut &str) -> KvnResult<String> {
565    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
566    Ok(v.trim().to_string())
567}
568
569/// Parses an optional string value from a KVN line.
570/// Returns None if empty.
571pub fn kv_string_opt(input: &mut &str) -> KvnResult<Option<String>> {
572    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
573    let trimmed = v.trim();
574    if trimmed.is_null() {
575        Ok(None)
576    } else {
577        Ok(Some(trimmed.to_string()))
578    }
579}
580
581/// Parses an Epoch value from a KVN line.
582pub fn kv_epoch(input: &mut &str) -> KvnResult<Epoch> {
583    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
584    Epoch::from_str(v.trim())
585        .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
586}
587
588/// Parses an optional Epoch value from a KVN line.
589pub fn kv_epoch_opt(input: &mut &str) -> KvnResult<Option<Epoch>> {
590    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
591    let trimmed = v.trim();
592    if trimmed.is_null() {
593        Ok(None)
594    } else {
595        Epoch::from_str(trimmed)
596            .map(Some)
597            .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
598    }
599}
600
601/// Parses an Epoch value as a single token (until next space).
602pub fn kv_epoch_token(input: &mut &str) -> KvnResult<Epoch> {
603    let v = till_space.parse_next(input)?;
604    Epoch::from_str(v.trim())
605        .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
606}
607
608/// Parses a boolean (YES/NO) from a KVN line.
609pub fn kv_yes_no(input: &mut &str) -> KvnResult<YesNo> {
610    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
611    YesNo::from_str(v.trim())
612        .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
613}
614
615/// Parses an optional boolean (YES/NO) from a KVN line.
616pub fn kv_yes_no_opt(input: &mut &str) -> KvnResult<Option<YesNo>> {
617    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
618    let trimmed = v.trim();
619    if trimmed.is_null() {
620        Ok(None)
621    } else {
622        YesNo::from_str(trimmed)
623            .map(Some)
624            .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
625    }
626}
627
628/// Parses any type that implements FromStr from a KVN line.
629pub fn kv_enum<T: FromStr>(input: &mut &str) -> KvnResult<T>
630where
631    EnumParseError: From<T::Err>,
632{
633    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
634    T::from_str(v.trim()).map_err(|e| {
635        ErrMode::Cut(InternalParserError::from_external_error(
636            input,
637            EnumParseError::from(e),
638        ))
639    })
640}
641
642/// Parses an optional type that implements FromStr from a KVN line.
643pub fn kv_enum_opt<T: FromStr>(input: &mut &str) -> KvnResult<Option<T>>
644where
645    EnumParseError: From<T::Err>,
646{
647    let v = terminated(till_line_ending, opt_line_ending).parse_next(input)?;
648    let trimmed = v.trim();
649    if trimmed.is_null() {
650        Ok(None)
651    } else {
652        T::from_str(trimmed).map(Some).map_err(|e| {
653            ErrMode::Cut(InternalParserError::from_external_error(
654                input,
655                EnumParseError::from(e),
656            ))
657        })
658    }
659}
660
661/// Parses a value from a KVN line using the `FromKvnValue` trait.
662pub fn kv_from_kvn_value<T: FromKvnValue>(input: &mut &str) -> KvnResult<T> {
663    let (v, _) = kv_rest.parse_next(input)?;
664    T::from_kvn_value(v)
665        .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
666}
667
668/// Parses any type that implements FromKvnFloat from a KVN line.
669pub fn kv_from_kvn<T: FromKvnFloat>(input: &mut &str) -> KvnResult<T> {
670    let (v, u) = kv_float_unit.parse_next(input)?;
671    T::from_kvn_float(v, u)
672        .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
673}
674
675/// Parses any optional type that implements FromKvnFloat from a KVN line.
676pub fn kv_from_kvn_opt<T: FromKvnFloat>(input: &mut &str) -> KvnResult<Option<T>> {
677    let (v, u) = kv_float_unit_opt.parse_next(input)?;
678    if let Some(val) = v {
679        T::from_kvn_float(val, u)
680            .map(Some)
681            .map_err(|e| ErrMode::Cut(InternalParserError::from_external_error(input, e)))
682    } else {
683        Ok(None)
684    }
685}
686
687/// Parses a float and its optional unit from a KVN line.
688pub fn kv_float_unit<'a>(input: &mut &'a str) -> KvnResult<(f64, Option<&'a str>)> {
689    terminated((parse_f64_winnow, kv_unit), opt_line_ending).parse_next(input)
690}
691
692//----------------------------------------------------------------------
693// Value Parsing Helpers
694//----------------------------------------------------------------------
695
696/// Parses an f64 value from a string slice.
697pub fn parse_f64(value: &str) -> crate::error::Result<f64> {
698    value.trim().parse::<f64>().map_err(CcsdsNdmError::from)
699}
700
701/// Parses an i32 value from a string slice.
702pub fn parse_i32(value: &str) -> crate::error::Result<i32> {
703    value.trim().parse::<i32>().map_err(CcsdsNdmError::from)
704}
705
706/// Parses a u32 value from a string slice.
707pub fn parse_u32(value: &str) -> crate::error::Result<u32> {
708    value.trim().parse::<u32>().map_err(CcsdsNdmError::from)
709}
710
711/// Parses a u64 value from a string slice.
712pub fn parse_u64(value: &str) -> crate::error::Result<u64> {
713    value.trim().parse::<u64>().map_err(CcsdsNdmError::from)
714}
715
716//----------------------------------------------------------------------
717// High-level Parsing Traits
718//----------------------------------------------------------------------
719
720/// Trait for types that can be parsed from KVN using winnow.
721///
722/// This is the primary trait for message-level parsing. Each message type
723/// implements this trait to define how it parses from KVN.
724pub trait ParseKvn: Sized {
725    /// Parse the type from a KVN input stream.
726    fn parse_kvn(input: &mut &str) -> KvnResult<Self>;
727
728    /// Convenience method to parse from a string.
729    fn from_kvn_str(s: &str) -> crate::error::Result<Self> {
730        kvn_entry(Self::parse_kvn)
731            .parse(s)
732            .map_err(|e| to_ccsds_error(s, e))
733    }
734}
735
736//----------------------------------------------------------------------
737// Combinator Helpers
738//----------------------------------------------------------------------
739
740/// Parses a specific key-value pair by key name and applies a value parser.
741/// Returns the parsed value and optional unit.
742pub fn expect_kv<'a, T, P>(
743    expected_key: &'static str,
744    mut val_parser: P,
745) -> impl FnMut(&mut &'a str) -> KvnResult<(T, Option<&'a str>)>
746where
747    P: winnow::Parser<&'a str, T, ErrMode<InternalParserError>>,
748{
749    move |input: &mut &'a str| {
750        (
751            ws,
752            keyword.context(StrContext::Label("KVN keyword")),
753            kv_sep,
754        )
755            .verify(|(_, key, _)| *key == expected_key)
756            .context(StrContext::Expected(StrContextValue::Description(
757                expected_key,
758            )))
759            .parse_next(input)?;
760
761        let val = val_parser.parse_next(input)?;
762        let unit = kv_unit.parse_next(input)?;
763        opt_line_ending.parse_next(input)?;
764
765        Ok((val, unit))
766    }
767}
768
769/// Parses a specific key-value pair by key name.
770/// Returns the value and optional unit.
771pub fn expect_key<'a>(
772    expected_key: &'static str,
773) -> impl FnMut(&mut &'a str) -> KvnResult<(&'a str, Option<&'a str>)> {
774    expect_kv(expected_key, kvn_value_only)
775}
776
777fn kvn_value_only<'a>(input: &mut &'a str) -> KvnResult<&'a str> {
778    take_till(0.., |c: char| c == '[' || c == '\r' || c == '\n')
779        .map(|s: &str| s.trim())
780        .parse_next(input)
781}
782
783/// Parses a key-value pair where the key matches a predicate.
784/// Returns (key, value, unit).
785pub fn key_matching<'a, F>(
786    predicate: F,
787) -> impl FnMut(&mut &'a str) -> KvnResult<(&'a str, &'a str, Option<&'a str>)>
788where
789    F: Fn(&str) -> bool + Copy,
790{
791    move |input: &mut &'a str| {
792        ws.parse_next(input)?;
793        let key = keyword.parse_next(input)?;
794        if !predicate(key) {
795            return Err(ErrMode::Backtrack(InternalParserError::from_input(input)));
796        }
797        kv_sep.parse_next(input)?;
798        let (value, unit) = kvn_value.parse_next(input)?;
799        opt_line_ending.parse_next(input)?;
800        Ok((key, value, unit))
801    }
802}
803
804/// Skips comment lines and collects them into a Vec.
805pub fn collect_comments(input: &mut &str) -> KvnResult<Vec<String>> {
806    // Fast path: if no COMMENT or newline, return empty Vec immediately
807    let checkpoint = input.checkpoint();
808    let _ = ws.parse_next(input)?;
809    if input.is_empty()
810        || (!input.starts_with("COMMENT") && !input.starts_with('\r') && !input.starts_with('\n'))
811    {
812        input.reset(&checkpoint);
813        return Ok(Vec::new());
814    }
815    input.reset(&checkpoint);
816
817    repeat(
818        0..,
819        alt((
820            // Corrected: removed unnecessary parentheses around alt
821            preceded(ws, comment_line).map(|s| Some(s.trim().to_string())),
822            (ws, line_ending).map(|_| None),
823        )),
824    )
825    .fold(Vec::new, |mut acc: Vec<String>, item| {
826        if let Some(s) = item {
827            acc.push(s);
828        }
829        acc
830    })
831    .parse_next(input)
832}
833
834/// Skips empty lines and comments, discarding them.
835pub fn skip_empty_and_comments(input: &mut &str) -> KvnResult<()> {
836    loop {
837        let checkpoint = input.checkpoint();
838        if alt(((ws, comment_line).void(), (ws, line_ending).void()))
839            .parse_next(input)
840            .is_err()
841        {
842            input.reset(&checkpoint);
843            break;
844        }
845    }
846    let _ = ws.parse_next(input);
847    Ok(())
848}
849
850/// Entry point for message parsers that handles leading whitespace.
851pub fn kvn_entry<'a, O, P>(mut parser: P) -> impl FnMut(&mut &'a str) -> KvnResult<O>
852where
853    P: Parser<&'a str, O, ErrMode<InternalParserError>>,
854{
855    move |input: &mut &'a str| {
856        skip_empty_and_comments.parse_next(input)?;
857        let res = parser.parse_next(input)?;
858        let _ = skip_empty_and_comments.parse_next(input);
859        Ok(res)
860    }
861}
862
863/// Macro for declarative KVN block parsing.
864///
865/// This macro generates a loop that matches keys using `dispatch!`,
866/// handles comment collection, and provides helpful context on errors.
867#[macro_export]
868macro_rules! parse_block {
869    // Flexible variant that supports both simple assignment and action blocks, with or without error label
870    ($input:ident, $comments:expr, {
871        $($($key:literal)|+ => $target:ident : $parser:expr $(=> $action:block)? ),* $(,)?
872    }, $break_condition:expr $(, $error_label:expr)?)
873     => {
874        loop {
875            let checkpoint = $input.checkpoint();
876            let loop_comments = collect_comments.parse_next($input)?;
877
878            if ($break_condition)(&mut *$input) {
879                $comments.extend(loop_comments);
880                break;
881            }
882
883            let key = match key_token.parse_next($input) {
884                Ok(k) => k,
885                Err(_) => {
886                    $input.reset(&checkpoint);
887                    break;
888                }
889            };
890
891            match key {
892                $( // Corrected: removed unnecessary parentheses around match arm
893                    $($key)|+ => {
894                        let val = $parser.parse_next($input)?;
895                        parse_block!(@action $comments, loop_comments, val, $target $(, $action)?);
896                    }
897                )*
898                _ => {
899                    $input.reset(&checkpoint);
900                    $( // Corrected: removed unnecessary parentheses around match arm
901                        return Err(winnow::error::ErrMode::Cut(InternalParserError::from_input($input).add_context(
902                            $input,
903                            &$input.checkpoint(),
904                            winnow::error::StrContext::Label($error_label),
905                        )));
906                    )?
907                    #[allow(unreachable_code)]
908                    {
909                        break;
910                    }
911                }
912            }
913        }
914    };
915
916    // Internal helper for action handling
917    (@action $comments:expr, $loop_comments:ident, $val:ident, $target:ident) => {
918        $comments.extend($loop_comments);
919        $target = Some($val);
920    };
921    (@action $comments:expr, $loop_comments:ident, $val:ident, $binding:ident, $action:block) => {
922        $comments.extend($loop_comments);
923        let $binding = $val;
924        $action
925    };
926}
927
928/// Checks if we're at a specific block start without full string scan.
929pub fn at_block_start(tag: &str, input: &str) -> bool {
930    let s = input.trim_start_matches([' ', '\t']);
931    if let Some(rest) = s.strip_prefix(tag) {
932        if let Some(suffix) = rest.strip_prefix("_START") {
933            return suffix.starts_with('\r') || suffix.starts_with('\n') || suffix.is_empty();
934        }
935    }
936    false
937}
938
939/// Checks if we're at a specific block end without full string scan.
940pub fn at_block_end(tag: &str, input: &str) -> bool {
941    let s = input.trim_start_matches([' ', '\t']);
942    if let Some(rest) = s.strip_prefix(tag) {
943        if let Some(suffix) = rest
944            .strip_prefix("_STOP")
945            .or_else(|| rest.strip_prefix("_END"))
946        {
947            return suffix.starts_with('\r') || suffix.starts_with('\n') || suffix.is_empty();
948        }
949    }
950    false
951}
952
953/// Expects a specific block start and consumes it.
954pub fn expect_block_start<'a>(
955    expected_tag: &'static str,
956) -> impl FnMut(&mut &'a str) -> KvnResult<()> {
957    move |input: &mut &'a str| {
958        (ws, block_start, opt_line_ending)
959            .verify(|(_, tag, _)| *tag == expected_tag)
960            .void()
961            .context(StrContext::Label("Block start"))
962            .context(StrContext::Expected(StrContextValue::Description(
963                expected_tag,
964            )))
965            .parse_next(input)
966    }
967}
968
969/// Expects a specific block end and consumes it.
970pub fn expect_block_end<'a>(
971    expected_tag: &'static str,
972) -> impl FnMut(&mut &'a str) -> KvnResult<()> {
973    move |input: &mut &'a str| {
974        (ws, block_end, opt_line_ending)
975            .verify(|(_, tag, _)| *tag == expected_tag)
976            .void()
977            .context(StrContext::Label("Block end"))
978            .context(StrContext::Expected(StrContextValue::Description(
979                expected_tag,
980            )))
981            .parse_next(input)
982    }
983}
984
985/// Parses the ODM header section.
986pub fn odm_header(input: &mut &str) -> KvnResult<OdmHeader> {
987    let mut comment = Vec::new();
988    let mut classification = None;
989    let mut creation_date = None;
990    let mut originator = None;
991    let mut message_id = None;
992
993    loop {
994        let checkpoint = input.checkpoint();
995        comment.extend(collect_comments.parse_next(input)?);
996
997        let key = match preceded(ws, keyword).parse_next(input) {
998            Ok(k) => k,
999            Err(_) => {
1000                input.reset(&checkpoint);
1001                break;
1002            }
1003        };
1004
1005        // Stop if we encounter a metadata key
1006        if key == "OBJECT_NAME" || key == "META_START" {
1007            input.reset(&checkpoint);
1008            break;
1009        }
1010
1011        kv_sep.parse_next(input)?;
1012        match key {
1013            "CLASSIFICATION" => {
1014                classification = Some(kv_string.parse_next(input)?);
1015            }
1016            "CREATION_DATE" => {
1017                creation_date = Some(kv_epoch.parse_next(input)?);
1018            }
1019            "ORIGINATOR" => {
1020                originator = Some(kv_string.parse_next(input)?);
1021            }
1022            "MESSAGE_ID" => {
1023                message_id = Some(kv_string.parse_next(input)?);
1024            }
1025            _ => {
1026                input.reset(&checkpoint);
1027                break;
1028            }
1029        }
1030
1031        if input.offset_from(&checkpoint) == 0 {
1032            break;
1033        }
1034    }
1035
1036    Ok(OdmHeader {
1037        comment,
1038        classification,
1039        creation_date: creation_date.ok_or_else(|| cut_err(input, "Expected CREATION_DATE"))?,
1040        originator: originator.ok_or_else(|| cut_err(input, "Expected ORIGINATOR"))?,
1041        message_id,
1042    })
1043}
1044
1045/// Parses the ADM header section.
1046pub fn adm_header(input: &mut &str) -> KvnResult<AdmHeader> {
1047    let mut comment = Vec::new();
1048    let mut classification = None;
1049    let mut creation_date = None;
1050    let mut originator = None;
1051    let mut message_id = None;
1052
1053    loop {
1054        let checkpoint = input.checkpoint();
1055        comment.extend(collect_comments.parse_next(input)?);
1056
1057        let key = match preceded(ws, keyword).parse_next(input) {
1058            Ok(k) => k,
1059            Err(_) => {
1060                input.reset(&checkpoint);
1061                break;
1062            }
1063        };
1064
1065        // Stop if we encounter a metadata key
1066        // AEM/APM metadata usually starts with OBJECT_NAME or META_START
1067        if key == "OBJECT_NAME" || key == "META_START" {
1068            input.reset(&checkpoint);
1069            break;
1070        }
1071
1072        kv_sep.parse_next(input)?;
1073        match key {
1074            "CLASSIFICATION" => {
1075                classification = Some(kv_string.parse_next(input)?);
1076            }
1077            "CREATION_DATE" => {
1078                creation_date = Some(kv_epoch.parse_next(input)?);
1079            }
1080            "ORIGINATOR" => {
1081                originator = Some(kv_string.parse_next(input)?);
1082            }
1083            "MESSAGE_ID" => {
1084                message_id = Some(kv_string.parse_next(input)?);
1085            }
1086            _ => {
1087                input.reset(&checkpoint);
1088                break;
1089            }
1090        }
1091
1092        if input.offset_from(&checkpoint) == 0 {
1093            break;
1094        }
1095    }
1096
1097    Ok(AdmHeader {
1098        comment,
1099        classification,
1100        creation_date: creation_date.ok_or_else(|| cut_err(input, "Expected CREATION_DATE"))?,
1101        originator: originator.ok_or_else(|| cut_err(input, "Expected ORIGINATOR"))?,
1102        message_id,
1103    })
1104}
1105
1106//----------------------------------------------------------------------
1107// Common Parsers
1108//----------------------------------------------------------------------
1109
1110/// Parses the state vector section.
1111pub fn state_vector(input: &mut &str) -> KvnResult<(Vec<String>, StateVector)> {
1112    let mut comment = Vec::new();
1113    let mut epoch = None;
1114    let mut x = None;
1115    let mut y = None;
1116    let mut z = None;
1117    let mut x_dot = None;
1118    let mut y_dot = None;
1119    let mut z_dot = None;
1120
1121    parse_block!(input, comment, {
1122        "EPOCH" => epoch: kv_epoch,
1123        "X" => x: kv_from_kvn,
1124        "Y" => y: kv_from_kvn,
1125        "Z" => z: kv_from_kvn,
1126        "X_DOT" => x_dot: kv_from_kvn,
1127        "Y_DOT" => y_dot: kv_from_kvn,
1128        "Z_DOT" => z_dot: kv_from_kvn,
1129    }, |_| false);
1130
1131    let sv = StateVector {
1132        comment: Vec::new(), // comments are returned separately for proper placement
1133        epoch: epoch.ok_or_else(|| missing_field_err(input, "State Vector", "EPOCH"))?,
1134        x: x.ok_or_else(|| missing_field_err(input, "State Vector", "X"))?,
1135        y: y.ok_or_else(|| missing_field_err(input, "State Vector", "Y"))?,
1136        z: z.ok_or_else(|| missing_field_err(input, "State Vector", "Z"))?,
1137        x_dot: x_dot.ok_or_else(|| missing_field_err(input, "State Vector", "X_DOT"))?,
1138        y_dot: y_dot.ok_or_else(|| missing_field_err(input, "State Vector", "Y_DOT"))?,
1139        z_dot: z_dot.ok_or_else(|| missing_field_err(input, "State Vector", "Z_DOT"))?,
1140    };
1141
1142    Ok((comment, sv))
1143}
1144
1145/// Parses the optional covariance matrix section.
1146pub fn covariance_matrix(input: &mut &str) -> KvnResult<Option<OpmCovarianceMatrix>> {
1147    let mut comment = Vec::new();
1148    let mut cov_ref_frame = None;
1149    let mut cx_x = None;
1150    let mut cy_x = None;
1151    let mut cy_y = None;
1152    let mut cz_x = None;
1153    let mut cz_y = None;
1154    let mut cz_z = None;
1155    let mut cx_dot_x = None;
1156    let mut cx_dot_y = None;
1157    let mut cx_dot_z = None;
1158    let mut cx_dot_x_dot = None;
1159    let mut cy_dot_x = None;
1160    let mut cy_dot_y = None;
1161    let mut cy_dot_z = None;
1162    let mut cy_dot_x_dot = None;
1163    let mut cy_dot_y_dot = None;
1164    let mut cz_dot_x = None;
1165    let mut cz_dot_y = None;
1166    let mut cz_dot_z = None;
1167    let mut cz_dot_x_dot = None;
1168    let mut cz_dot_y_dot = None;
1169    let mut cz_dot_z_dot = None;
1170
1171    parse_block!(input, comment, {
1172        "COV_REF_FRAME" => cov_ref_frame: kv_string,
1173        "CX_X" => cx_x: kv_from_kvn,
1174        "CY_X" => cy_x: kv_from_kvn,
1175        "CY_Y" => cy_y: kv_from_kvn,
1176        "CZ_X" => cz_x: kv_from_kvn,
1177        "CZ_Y" => cz_y: kv_from_kvn,
1178        "CZ_Z" => cz_z: kv_from_kvn,
1179        "CX_DOT_X" => cx_dot_x: kv_from_kvn,
1180        "CX_DOT_Y" => cx_dot_y: kv_from_kvn,
1181        "CX_DOT_Z" => cx_dot_z: kv_from_kvn,
1182        "CX_DOT_X_DOT" => cx_dot_x_dot: kv_from_kvn,
1183        "CY_DOT_X" => cy_dot_x: kv_from_kvn,
1184        "CY_DOT_Y" => cy_dot_y: kv_from_kvn,
1185        "CY_DOT_Z" => cy_dot_z: kv_from_kvn,
1186        "CY_DOT_X_DOT" => cy_dot_x_dot: kv_from_kvn,
1187        "CY_DOT_Y_DOT" => cy_dot_y_dot: kv_from_kvn,
1188        "CZ_DOT_X" => cz_dot_x: kv_from_kvn,
1189        "CZ_DOT_Y" => cz_dot_y: kv_from_kvn,
1190        "CZ_DOT_Z" => cz_dot_z: kv_from_kvn,
1191        "CZ_DOT_X_DOT" => cz_dot_x_dot: kv_from_kvn,
1192        "CZ_DOT_Y_DOT" => cz_dot_y_dot: kv_from_kvn,
1193        "CZ_DOT_Z_DOT" => cz_dot_z_dot: kv_from_kvn,
1194    }, |_| false);
1195
1196    // If we have covariance data, build the struct
1197    if cx_x.is_some() {
1198        Ok(Some(OpmCovarianceMatrix {
1199            comment,
1200            cov_ref_frame,
1201            cx_x: cx_x.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CX_X"))?,
1202            cy_x: cy_x.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_X"))?,
1203            cy_y: cy_y.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_Y"))?,
1204            cz_x: cz_x.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_X"))?,
1205            cz_y: cz_y.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_Y"))?,
1206            cz_z: cz_z.ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_Z"))?,
1207            cx_dot_x: cx_dot_x
1208                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CX_DOT_X"))?,
1209            cx_dot_y: cx_dot_y
1210                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CX_DOT_Y"))?,
1211            cx_dot_z: cx_dot_z
1212                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CX_DOT_Z"))?,
1213            cx_dot_x_dot: cx_dot_x_dot
1214                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CX_DOT_X_DOT"))?,
1215            cy_dot_x: cy_dot_x
1216                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_DOT_X"))?,
1217            cy_dot_y: cy_dot_y
1218                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_DOT_Y"))?,
1219            cy_dot_z: cy_dot_z
1220                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_DOT_Z"))?,
1221            cy_dot_x_dot: cy_dot_x_dot
1222                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_DOT_X_DOT"))?,
1223            cy_dot_y_dot: cy_dot_y_dot
1224                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CY_DOT_Y_DOT"))?,
1225            cz_dot_x: cz_dot_x
1226                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_X"))?,
1227            cz_dot_y: cz_dot_y
1228                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_Y"))?,
1229            cz_dot_z: cz_dot_z
1230                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_Z"))?,
1231            cz_dot_x_dot: cz_dot_x_dot
1232                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_X_DOT"))?,
1233            cz_dot_y_dot: cz_dot_y_dot
1234                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_Y_DOT"))?,
1235            cz_dot_z_dot: cz_dot_z_dot
1236                .ok_or_else(|| missing_field_err(input, "Covariance Matrix", "CZ_DOT_Z_DOT"))?,
1237        }))
1238    } else {
1239        Ok(None)
1240    }
1241}
1242
1243/// Parses the optional spacecraft parameters section.
1244pub fn spacecraft_parameters(input: &mut &str) -> KvnResult<Option<SpacecraftParameters>> {
1245    let mut comment = Vec::new();
1246    let mut mass = None;
1247    let mut solar_rad_area = None;
1248    let mut solar_rad_coeff = None;
1249    let mut drag_area = None;
1250    let mut drag_coeff = None;
1251
1252    parse_block!(input, comment, {
1253        "MASS" => mass: kv_from_kvn,
1254        "SOLAR_RAD_AREA" => solar_rad_area: kv_from_kvn,
1255        "SOLAR_RAD_COEFF" => solar_rad_coeff: kv_from_kvn,
1256        "DRAG_AREA" => drag_area: kv_from_kvn,
1257        "DRAG_COEFF" => drag_coeff: kv_from_kvn,
1258    }, |_| false);
1259
1260    // If we have any spacecraft data, build the struct
1261    if mass.is_some() || solar_rad_area.is_some() || drag_area.is_some() {
1262        Ok(Some(SpacecraftParameters {
1263            comment,
1264            mass,
1265            solar_rad_area,
1266            solar_rad_coeff,
1267            drag_area,
1268            drag_coeff,
1269        }))
1270    } else {
1271        Ok(None)
1272    }
1273}
1274
1275/// Parses user-defined parameters.
1276pub fn user_defined_parameters(input: &mut &str) -> KvnResult<Option<UserDefined>> {
1277    let mut comment = Vec::new();
1278    let mut params = Vec::new();
1279
1280    loop {
1281        let checkpoint = input.checkpoint();
1282        let comments = collect_comments.parse_next(input)?;
1283
1284        let key = match key_token.parse_next(input) {
1285            Ok(k) => k,
1286            Err(_) => {
1287                input.reset(&checkpoint);
1288                break;
1289            }
1290        };
1291
1292        if key.starts_with("USER_DEFINED_") {
1293            comment.extend(comments);
1294            let val = kv_string.parse_next(input)?;
1295            params.push(UserDefinedParameter {
1296                parameter: key.strip_prefix("USER_DEFINED_").unwrap().to_string(),
1297                value: val,
1298            });
1299        } else {
1300            // Backtrack and end user defined section
1301            input.reset(&checkpoint);
1302            break;
1303        }
1304
1305        if input.offset_from(&checkpoint) == 0 {
1306            break;
1307        }
1308    }
1309
1310    if params.is_empty() {
1311        Ok(None)
1312    } else {
1313        Ok(Some(UserDefined {
1314            comment,
1315            user_defined: params,
1316        }))
1317    }
1318}
1319
1320//----------------------------------------------------------------------
1321// Tests
1322//----------------------------------------------------------------------
1323
1324#[cfg(test)]
1325mod tests {
1326    use super::*;
1327
1328    #[test]
1329    fn test_keyword() {
1330        let mut input = "OBJECT_NAME";
1331        assert_eq!(keyword.parse_next(&mut input).unwrap(), "OBJECT_NAME");
1332
1333        let mut input = "CCSDS_OPM_VERS";
1334        assert_eq!(keyword.parse_next(&mut input).unwrap(), "CCSDS_OPM_VERS");
1335
1336        let mut input = "X_DOT";
1337        assert_eq!(keyword.parse_next(&mut input).unwrap(), "X_DOT");
1338    }
1339
1340    #[test]
1341    fn test_kvn_value_without_unit() {
1342        let mut input = "SATELLITE-1\n";
1343        let (value, unit) = kvn_value.parse_next(&mut input).unwrap();
1344        assert_eq!(value, "SATELLITE-1");
1345        assert_eq!(unit, None);
1346    }
1347
1348    #[test]
1349    fn test_kvn_value_with_unit() {
1350        let mut input = "6503.514 [km]\n";
1351        let (value, unit) = kvn_value.parse_next(&mut input).unwrap();
1352        assert_eq!(value, "6503.514");
1353        assert_eq!(unit, Some("km"));
1354    }
1355
1356    #[test]
1357    fn test_key_value_line() {
1358        let mut input = "OBJECT_NAME = SATELLITE-1\n";
1359        let (key, value, unit) = key_value_line.parse_next(&mut input).unwrap();
1360        assert_eq!(key, "OBJECT_NAME");
1361        assert_eq!(value, "SATELLITE-1");
1362        assert_eq!(unit, None);
1363
1364        let mut input = "X = 6503.514 [km]\n";
1365        let (key, value, unit) = key_value_line.parse_next(&mut input).unwrap();
1366        assert_eq!(key, "X");
1367        assert_eq!(value, "6503.514");
1368        assert_eq!(unit, Some("km"));
1369    }
1370
1371    #[test]
1372    fn test_comment_line() {
1373        let mut input = "COMMENT This is a comment\n";
1374        let content = comment_line.parse_next(&mut input).unwrap();
1375        assert_eq!(content.trim(), "This is a comment");
1376
1377        let mut input = "COMMENT\n";
1378        let content = comment_line.parse_next(&mut input).unwrap();
1379        assert_eq!(content.trim(), "");
1380    }
1381
1382    #[test]
1383    fn test_block_start() {
1384        let mut input = "META_START\n";
1385        let tag = block_start.parse_next(&mut input).unwrap();
1386        assert_eq!(tag, "META");
1387
1388        let mut input = "COVARIANCE_START\n";
1389        let tag = block_start.parse_next(&mut input).unwrap();
1390        assert_eq!(tag, "COVARIANCE");
1391    }
1392
1393    #[test]
1394    fn test_block_end() {
1395        let mut input = "META_STOP\n";
1396        let tag = block_end.parse_next(&mut input).unwrap();
1397        assert_eq!(tag, "META");
1398
1399        let mut input = "COVARIANCE_END\n";
1400        let tag = block_end.parse_next(&mut input).unwrap();
1401        assert_eq!(tag, "COVARIANCE");
1402    }
1403
1404    #[test]
1405    fn test_expect_key() {
1406        let mut input = "OBJECT_NAME = SAT-1\n";
1407        let (value, unit) = expect_key("OBJECT_NAME").parse_next(&mut input).unwrap();
1408        assert_eq!(value, "SAT-1");
1409        assert_eq!(unit, None);
1410    }
1411
1412    #[test]
1413    fn test_collect_comments() {
1414        let mut input = "COMMENT Line 1\nCOMMENT Line 2\nOBJECT_NAME = SAT\n";
1415        let comments = collect_comments.parse_next(&mut input).unwrap();
1416        assert_eq!(comments, vec!["Line 1", "Line 2"]);
1417    }
1418
1419    #[test]
1420    fn test_raw_line() {
1421        let mut input = "2023-01-01T00:00:00 1000 2000 3000 1.0 2.0 3.0\n";
1422        let content = raw_line.parse_next(&mut input).unwrap();
1423        assert_eq!(content, "2023-01-01T00:00:00 1000 2000 3000 1.0 2.0 3.0");
1424    }
1425
1426    #[test]
1427    fn test_malformed_key_value() {
1428        // Missing equals
1429        let mut input = "KEY value\n";
1430        assert!(key_value_line.parse_next(&mut input).is_err());
1431
1432        // Missing value (valid in some contexts? No, value parser expects something)
1433        // Actually, empty values might be allowed if line ends?
1434        // kvn_value parses `take_till(...)`
1435        let mut input_empty = "KEY =\n";
1436        let (_, val, _) = key_value_line.parse_next(&mut input_empty).unwrap();
1437        assert_eq!(val, ""); // It allows empty values currently
1438    }
1439
1440    #[test]
1441    fn test_whitespace_variations() {
1442        let mut input = "  KEY  =  value  [unit]  \n";
1443        let (key, val, unit) = key_value_line.parse_next(&mut input).unwrap();
1444        assert_eq!(key, "KEY");
1445        assert_eq!(val, "value");
1446        assert_eq!(unit, Some("unit"));
1447    }
1448
1449    #[test]
1450    fn test_broken_unit() {
1451        let mut input = "KEY = value [unit\n"; // Missing closing bracket
1452        assert!(key_value_line.parse_next(&mut input).is_err());
1453    }
1454
1455    #[test]
1456    fn test_require_field_success() {
1457        let mut input = "";
1458        let value = require_field(&mut input, "TestBlock", "TEST_FIELD", Some(42_u32)).unwrap();
1459        assert_eq!(value, 42);
1460    }
1461
1462    #[test]
1463    fn test_require_field_missing() {
1464        let mut input = "";
1465        let err = require_field::<u32>(&mut input, "TestBlock", "TEST_FIELD", None).unwrap_err();
1466        match err {
1467            ErrMode::Cut(inner) => match *inner.kind {
1468                crate::error::ParserErrorKind::MissingRequiredField { block, field } => {
1469                    assert_eq!(block, "TestBlock");
1470                    assert_eq!(field, "TEST_FIELD");
1471                }
1472                _ => panic!("Expected MissingRequiredField"),
1473            },
1474            _ => panic!("Expected cut error"),
1475        }
1476    }
1477}