1use core::fmt;
9
10use crate::astro::time::civil::j2000_seconds;
11use crate::data::{AnalysisCenter, DataCatalogError, ProductDate, ProductIdentity, ProductType};
12use crate::tolerances::WHOLE_SECOND_EPS_S;
13
14use super::{Sp3, Sp3DataType, Sp3Version};
15
16const SP3_MAX_EPOCH_INTERVAL_S: f64 = 100_000.0;
19const J2000_MJD_DAY: i64 = 51_544;
21const GPS_ZERO_MJD_DAY: i64 = 44_244;
23const SECONDS_PER_DAY_I64: i64 = 86_400;
24const SECONDS_PER_WEEK_I64: i64 = 604_800;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ExactSp3Coverage {
29 HalfOpen,
32 Inclusive,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ExactSp3Request {
45 date: ProductDate,
46 issue: Option<String>,
47 span: String,
48 sample: String,
49 format_version: Option<String>,
50 expected_agency: Option<String>,
51}
52
53impl ExactSp3Request {
54 pub fn new(
56 date: ProductDate,
57 issue: Option<&str>,
58 span: &str,
59 sample: &str,
60 ) -> Result<Self, ExactSp3ValidationError> {
61 ProductDate::new(date.year, date.month, date.day)
63 .map_err(ExactSp3ValidationError::Catalog)?;
64 parse_issue(issue)?;
65 parse_duration_token(span, DurationField::Span)?;
66 let cadence_s = parse_duration_token(sample, DurationField::Sample)?;
67 if cadence_s as f64 >= SP3_MAX_EPOCH_INTERVAL_S {
68 return Err(ExactSp3ValidationError::UnsupportedSampleToken {
69 token: sample.to_owned(),
70 });
71 }
72
73 Ok(Self {
74 date,
75 issue: issue.map(str::to_owned),
76 span: span.to_owned(),
77 sample: sample.to_owned(),
78 format_version: None,
79 expected_agency: None,
80 })
81 }
82
83 pub fn from_identity(identity: &ProductIdentity) -> Result<Self, ExactSp3ValidationError> {
85 if identity.family != ProductType::Sp3 {
86 return Err(ExactSp3ValidationError::WrongProductFamily {
87 actual: identity.family,
88 });
89 }
90 identity
91 .validate()
92 .map_err(ExactSp3ValidationError::Catalog)?;
93 let mut request = Self::new(
94 identity.date,
95 identity.issue.as_deref(),
96 &identity.span,
97 &identity.sample,
98 )?;
99 request.format_version = identity.format_version.clone();
100 request.expected_agency = Some(
101 match identity.analysis_center {
102 AnalysisCenter::Igs => "IGS",
103 AnalysisCenter::Esa | AnalysisCenter::EsaUlt => "ESOC",
104 AnalysisCenter::Gfz | AnalysisCenter::GfzUlt => "GFZ",
105 AnalysisCenter::Cod
106 | AnalysisCenter::CodRap
107 | AnalysisCenter::CodPrd1
108 | AnalysisCenter::CodPrd2
109 | AnalysisCenter::CodUlt => "AIUB",
110 AnalysisCenter::IgsUlt => "IGS",
111 }
112 .to_owned(),
113 );
114 Ok(request)
115 }
116
117 pub fn with_expected_agency(mut self, agency: &str) -> Result<Self, ExactSp3ValidationError> {
122 if agency.is_empty()
123 || agency.len() > 4
124 || !agency
125 .bytes()
126 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
127 {
128 return Err(ExactSp3ValidationError::InvalidExpectedAgency {
129 agency: agency.to_owned(),
130 });
131 }
132 self.expected_agency = Some(agency.to_owned());
133 Ok(self)
134 }
135
136 pub fn date(&self) -> ProductDate {
138 self.date
139 }
140
141 pub fn issue(&self) -> Option<&str> {
143 self.issue.as_deref()
144 }
145
146 pub fn span(&self) -> &str {
148 &self.span
149 }
150
151 pub fn sample(&self) -> &str {
153 &self.sample
154 }
155
156 pub fn format_version(&self) -> Option<&str> {
158 self.format_version.as_deref()
159 }
160
161 pub fn expected_agency(&self) -> Option<&str> {
163 self.expected_agency.as_deref()
164 }
165}
166
167#[derive(Debug, Clone, PartialEq)]
169#[non_exhaustive]
170pub enum ExactSp3ValidationError {
171 Parse(crate::Error),
173 Catalog(DataCatalogError),
175 WrongProductFamily {
177 actual: ProductType,
179 },
180 InvalidIssue {
182 issue: String,
185 },
186 UnsupportedSpanToken {
188 token: String,
190 },
191 UnsupportedSampleToken {
193 token: String,
195 },
196 NonCanonicalSpanToken {
198 token: String,
200 canonical: String,
202 },
203 NonCanonicalSampleToken {
205 token: String,
207 canonical: String,
209 },
210 InvalidExpectedAgency {
212 agency: String,
214 },
215 AgencyMismatch {
217 expected: String,
219 actual: String,
221 },
222 MissingEof,
224 TrailingContentAfterEof,
226 MandatoryHeaderRecordCount {
228 record: &'static str,
230 expected: usize,
232 actual: usize,
234 },
235 MissingDeclaredSatelliteCount,
237 DeclaredSatelliteCountMismatch {
240 declared: usize,
242 tokens: usize,
244 },
245 DuplicateDeclaredSatellite {
247 token: String,
249 first_index: usize,
251 duplicate_index: usize,
253 },
254 NoDeclaredSatellites,
256 SatelliteRecordSequenceMismatch {
259 record: &'static str,
261 epoch_index: usize,
263 expected: Vec<String>,
265 actual: Vec<String>,
267 },
268 BodyRecordInterleavingMismatch {
271 epoch_index: usize,
273 expected: Vec<String>,
275 actual: Vec<String>,
277 },
278 NonFiniteHeaderCadence,
280 NonPositiveHeaderCadence {
282 actual_s: f64,
284 },
285 UnsupportedHeaderCadence {
287 actual_s: f64,
289 },
290 CadenceMismatch {
292 requested_s: f64,
294 header_s: f64,
296 },
297 DeclaredEpochCountMismatch {
299 declared: u64,
301 parsed: usize,
303 },
304 MissingDeclaredStart,
306 DeclaredStartMismatch {
308 requested_j2000_s: f64,
310 declared_j2000_s: f64,
312 },
313 RequestBeforeGpsEpoch,
316 NonFiniteHeaderStartMetadata {
318 field: &'static str,
320 },
321 InvalidHeaderStartMetadata {
323 field: &'static str,
325 actual: f64,
327 },
328 HeaderStartMetadataMismatch {
331 field: &'static str,
333 requested: f64,
335 actual: f64,
337 },
338 EmptyEpochGrid,
340 FirstEpochMismatch {
342 requested_j2000_s: f64,
344 actual_j2000_s: f64,
346 },
347 IrregularEpochGrid {
349 epoch_index: usize,
351 requested_s: f64,
353 actual_s: f64,
355 },
356 SpanNotMultipleOfCadence {
358 span_s: u64,
360 cadence_s: u64,
362 },
363 SpanMismatch {
366 parsed: usize,
368 half_open: usize,
370 inclusive: usize,
372 },
373 FormatVersionMismatch {
376 requested: String,
378 actual: String,
380 },
381}
382
383impl fmt::Display for ExactSp3ValidationError {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 match self {
386 Self::Parse(error) => write!(f, "exact SP3 parse failed: {error}"),
387 Self::Catalog(error) => write!(f, "invalid exact SP3 identity: {error}"),
388 Self::WrongProductFamily { actual } => {
389 write!(f, "exact SP3 validation cannot validate {actual}")
390 }
391 Self::InvalidIssue { issue } => {
392 write!(f, "invalid exact SP3 issue time {issue:?}; expected HHMM")
393 }
394 Self::UnsupportedSpanToken { token } => {
395 write!(f, "unsupported exact SP3 span token {token:?}")
396 }
397 Self::UnsupportedSampleToken { token } => {
398 write!(f, "unsupported exact SP3 sample token {token:?}")
399 }
400 Self::NonCanonicalSpanToken { token, canonical } => write!(
401 f,
402 "noncanonical exact SP3 span token {token:?}; use {canonical:?}"
403 ),
404 Self::NonCanonicalSampleToken { token, canonical } => write!(
405 f,
406 "noncanonical exact SP3 sample token {token:?}; use {canonical:?}"
407 ),
408 Self::InvalidExpectedAgency { agency } => {
409 write!(f, "invalid exact SP3 expected agency {agency:?}")
410 }
411 Self::AgencyMismatch { expected, actual } => write!(
412 f,
413 "SP3 agency mismatch: requested {expected:?}, header declares {actual:?}"
414 ),
415 Self::MissingEof => write!(f, "SP3 product is missing its EOF record"),
416 Self::TrailingContentAfterEof => {
417 write!(f, "SP3 product contains nonblank records after EOF")
418 }
419 Self::MandatoryHeaderRecordCount {
420 record,
421 expected,
422 actual,
423 } => write!(
424 f,
425 "SP3 header record {record} count is {actual}, expected {expected}"
426 ),
427 Self::MissingDeclaredSatelliteCount => {
428 write!(f, "SP3 line 3 has no valid declared satellite count")
429 }
430 Self::DeclaredSatelliteCountMismatch { declared, tokens } => write!(
431 f,
432 "SP3 declared satellite-count mismatch: line 3 declares {declared}, header contains {tokens} tokens"
433 ),
434 Self::DuplicateDeclaredSatellite {
435 token,
436 first_index,
437 duplicate_index,
438 } => write!(
439 f,
440 "SP3 header satellite {token:?} is duplicated at indices {first_index} and {duplicate_index}"
441 ),
442 Self::NoDeclaredSatellites => {
443 write!(f, "SP3 product declares no satellites")
444 }
445 Self::SatelliteRecordSequenceMismatch {
446 record,
447 epoch_index,
448 expected,
449 actual,
450 } => write!(
451 f,
452 "SP3 epoch {epoch_index} {record}-record sequence mismatch: expected {expected:?}, got {actual:?}"
453 ),
454 Self::BodyRecordInterleavingMismatch {
455 epoch_index,
456 expected,
457 actual,
458 } => write!(
459 f,
460 "SP3 epoch {epoch_index} body record ordering mismatch: expected {expected:?}, got {actual:?}"
461 ),
462 Self::NonFiniteHeaderCadence => {
463 write!(f, "SP3 header cadence is not finite")
464 }
465 Self::NonPositiveHeaderCadence { actual_s } => {
466 write!(f, "SP3 header cadence must be positive, got {actual_s}")
467 }
468 Self::UnsupportedHeaderCadence { actual_s } => write!(
469 f,
470 "SP3 header cadence {actual_s} s is outside the supported format range"
471 ),
472 Self::CadenceMismatch {
473 requested_s,
474 header_s,
475 } => write!(
476 f,
477 "SP3 cadence mismatch: requested {requested_s} s, header declares {header_s} s"
478 ),
479 Self::DeclaredEpochCountMismatch { declared, parsed } => write!(
480 f,
481 "SP3 epoch-count mismatch: header declares {declared}, parsed {parsed}"
482 ),
483 Self::MissingDeclaredStart => {
484 write!(f, "SP3 header line 1 has no valid declared start epoch")
485 }
486 Self::DeclaredStartMismatch {
487 requested_j2000_s,
488 declared_j2000_s,
489 } => write!(
490 f,
491 "SP3 declared start mismatch: requested {requested_j2000_s} J2000 s, header declares {declared_j2000_s} J2000 s"
492 ),
493 Self::RequestBeforeGpsEpoch => {
494 write!(f, "exact SP3 request starts before the GPS week epoch")
495 }
496 Self::NonFiniteHeaderStartMetadata { field } => {
497 write!(f, "SP3 header start field {field} is not finite")
498 }
499 Self::InvalidHeaderStartMetadata { field, actual } => {
500 write!(f, "SP3 header start field {field} is out of range: {actual}")
501 }
502 Self::HeaderStartMetadataMismatch {
503 field,
504 requested,
505 actual,
506 } => write!(
507 f,
508 "SP3 header start mismatch in {field}: requested {requested}, header has {actual}"
509 ),
510 Self::EmptyEpochGrid => write!(f, "SP3 product has no epoch records"),
511 Self::FirstEpochMismatch {
512 requested_j2000_s,
513 actual_j2000_s,
514 } => write!(
515 f,
516 "SP3 first epoch mismatch: requested {requested_j2000_s} J2000 s, parsed {actual_j2000_s} J2000 s"
517 ),
518 Self::IrregularEpochGrid {
519 epoch_index,
520 requested_s,
521 actual_s,
522 } => write!(
523 f,
524 "SP3 epoch grid is irregular at index {epoch_index}: requested step {requested_s} s, got {actual_s} s"
525 ),
526 Self::SpanNotMultipleOfCadence {
527 span_s,
528 cadence_s,
529 } => write!(
530 f,
531 "exact SP3 span {span_s} s is not a multiple of cadence {cadence_s} s"
532 ),
533 Self::SpanMismatch {
534 parsed,
535 half_open,
536 inclusive,
537 } => write!(
538 f,
539 "SP3 span mismatch: parsed {parsed} epochs, expected {half_open} half-open or {inclusive} inclusive"
540 ),
541 Self::FormatVersionMismatch { requested, actual } => write!(
542 f,
543 "SP3 format-version mismatch: requested {requested:?}, parsed {actual:?}"
544 ),
545 }
546 }
547}
548
549impl std::error::Error for ExactSp3ValidationError {
550 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
551 match self {
552 Self::Parse(error) => Some(error),
553 Self::Catalog(error) => Some(error),
554 _ => None,
555 }
556 }
557}
558
559pub fn parse_exact_sp3(
564 bytes: &[u8],
565 request: &ExactSp3Request,
566) -> Result<(Sp3, ExactSp3Coverage), ExactSp3ValidationError> {
567 let product = Sp3::parse(bytes).map_err(ExactSp3ValidationError::Parse)?;
568 let coverage = validate_exact_sp3(&product, request)?;
569 Ok((product, coverage))
570}
571
572pub fn validate_exact_sp3(
578 product: &Sp3,
579 request: &ExactSp3Request,
580) -> Result<ExactSp3Coverage, ExactSp3ValidationError> {
581 let cadence_s = parse_duration_token(&request.sample, DurationField::Sample)?;
582 let span_s = parse_duration_token(&request.span, DurationField::Span)?;
583
584 validate_mandatory_structure(product)?;
585 if let Some(expected) = request.expected_agency.as_deref() {
586 let actual = product.header.agency.trim();
587 if actual != expected {
588 return Err(ExactSp3ValidationError::AgencyMismatch {
589 expected: expected.to_owned(),
590 actual: actual.to_owned(),
591 });
592 }
593 }
594
595 let header_cadence_s = product.header.epoch_interval_s;
596 if !header_cadence_s.is_finite() {
597 return Err(ExactSp3ValidationError::NonFiniteHeaderCadence);
598 }
599 if header_cadence_s <= 0.0 {
600 return Err(ExactSp3ValidationError::NonPositiveHeaderCadence {
601 actual_s: header_cadence_s,
602 });
603 }
604 if header_cadence_s >= SP3_MAX_EPOCH_INTERVAL_S {
605 return Err(ExactSp3ValidationError::UnsupportedHeaderCadence {
606 actual_s: header_cadence_s,
607 });
608 }
609 if !seconds_match(header_cadence_s, cadence_s as f64) {
610 return Err(ExactSp3ValidationError::CadenceMismatch {
611 requested_s: cadence_s as f64,
612 header_s: header_cadence_s,
613 });
614 }
615
616 if product.declared_num_epochs != product.epoch_j2000_s.len() as u64 {
617 return Err(ExactSp3ValidationError::DeclaredEpochCountMismatch {
618 declared: product.declared_num_epochs,
619 parsed: product.epoch_j2000_s.len(),
620 });
621 }
622
623 let requested_start_j2000_s = requested_start_j2000_s(request);
624 let declared_start_j2000_s = product
625 .declared_start_j2000_s
626 .ok_or(ExactSp3ValidationError::MissingDeclaredStart)?;
627 if !seconds_match(declared_start_j2000_s, requested_start_j2000_s) {
628 return Err(ExactSp3ValidationError::DeclaredStartMismatch {
629 requested_j2000_s: requested_start_j2000_s,
630 declared_j2000_s: declared_start_j2000_s,
631 });
632 }
633 validate_line2_start_metadata(product, requested_start_j2000_s)?;
634
635 let first_j2000_s = product
636 .epoch_j2000_s
637 .first()
638 .copied()
639 .ok_or(ExactSp3ValidationError::EmptyEpochGrid)?;
640 if !seconds_match(first_j2000_s, requested_start_j2000_s) {
641 return Err(ExactSp3ValidationError::FirstEpochMismatch {
642 requested_j2000_s: requested_start_j2000_s,
643 actual_j2000_s: first_j2000_s,
644 });
645 }
646
647 for (index, pair) in product.epoch_j2000_s.windows(2).enumerate() {
648 let actual_s = pair[1] - pair[0];
649 if !actual_s.is_finite() || !seconds_match(actual_s, cadence_s as f64) {
650 return Err(ExactSp3ValidationError::IrregularEpochGrid {
651 epoch_index: index + 1,
652 requested_s: cadence_s as f64,
653 actual_s,
654 });
655 }
656 }
657
658 if span_s % cadence_s != 0 {
659 return Err(ExactSp3ValidationError::SpanNotMultipleOfCadence { span_s, cadence_s });
660 }
661 let half_open = usize::try_from(span_s / cadence_s).unwrap_or(usize::MAX);
662 let inclusive = half_open.saturating_add(1);
663 match product.epoch_j2000_s.len() {
664 count if count == half_open => Ok(ExactSp3Coverage::HalfOpen),
665 count if count == inclusive => Ok(ExactSp3Coverage::Inclusive),
666 parsed => Err(ExactSp3ValidationError::SpanMismatch {
667 parsed,
668 half_open,
669 inclusive,
670 }),
671 }
672 .and_then(|coverage| {
673 validate_format_version(product.header.version, request.format_version.as_deref())?;
674 Ok(coverage)
675 })
676}
677
678fn validate_mandatory_structure(product: &Sp3) -> Result<(), ExactSp3ValidationError> {
679 if !product.had_eof {
680 return Err(ExactSp3ValidationError::MissingEof);
681 }
682 if product.trailing_content_after_eof {
683 return Err(ExactSp3ValidationError::TrailingContentAfterEof);
684 }
685 if product.satellite_header_lines < 5 {
686 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
687 record: "+",
688 expected: 5,
689 actual: product.satellite_header_lines,
690 });
691 }
692 if product.accuracy_header_lines != product.satellite_header_lines {
693 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
694 record: "++",
695 expected: product.satellite_header_lines,
696 actual: product.accuracy_header_lines,
697 });
698 }
699 for (record, actual) in [
700 ("%c", product.time_system_header_lines),
701 ("%f", product.float_header_lines),
702 ("%i", product.integer_header_lines),
703 ] {
704 if actual != 2 {
705 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
706 record,
707 expected: 2,
708 actual,
709 });
710 }
711 }
712 if product.header_comment_lines < 4 {
713 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
714 record: "/*",
715 expected: 4,
716 actual: product.header_comment_lines,
717 });
718 }
719 let declared_count = product
720 .declared_satellite_count
721 .ok_or(ExactSp3ValidationError::MissingDeclaredSatelliteCount)?;
722 if declared_count != product.declared_satellite_tokens.len() {
723 return Err(ExactSp3ValidationError::DeclaredSatelliteCountMismatch {
724 declared: declared_count,
725 tokens: product.declared_satellite_tokens.len(),
726 });
727 }
728 for duplicate_index in 0..product.declared_satellite_tokens.len() {
729 if let Some(first_index) = product.declared_satellite_tokens[..duplicate_index]
730 .iter()
731 .position(|token| token == &product.declared_satellite_tokens[duplicate_index])
732 {
733 return Err(ExactSp3ValidationError::DuplicateDeclaredSatellite {
734 token: product.declared_satellite_tokens[duplicate_index].clone(),
735 first_index,
736 duplicate_index,
737 });
738 }
739 }
740 if product.header.satellites.is_empty() {
741 return Err(ExactSp3ValidationError::NoDeclaredSatellites);
742 }
743 for epoch_index in 0..product.epochs.len() {
744 let positions = product
745 .epoch_position_tokens
746 .get(epoch_index)
747 .cloned()
748 .unwrap_or_default();
749 if positions != product.declared_satellite_tokens {
750 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
751 record: "P",
752 epoch_index,
753 expected: product.declared_satellite_tokens.clone(),
754 actual: positions,
755 });
756 }
757 let velocities = product
758 .epoch_velocity_tokens
759 .get(epoch_index)
760 .cloned()
761 .unwrap_or_default();
762 let expected_velocities = match product.header.data_type {
763 Sp3DataType::Position => Vec::new(),
764 Sp3DataType::Velocity => product.declared_satellite_tokens.clone(),
765 };
766 if velocities != expected_velocities {
767 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
768 record: "V",
769 epoch_index,
770 expected: expected_velocities,
771 actual: velocities,
772 });
773 }
774 let mut expected_body = Vec::with_capacity(match product.header.data_type {
775 Sp3DataType::Position => product.declared_satellite_tokens.len(),
776 Sp3DataType::Velocity => product.declared_satellite_tokens.len() * 2,
777 });
778 for token in &product.declared_satellite_tokens {
779 expected_body.push(format!("P{token}"));
780 if matches!(product.header.data_type, Sp3DataType::Velocity) {
781 expected_body.push(format!("V{token}"));
782 }
783 }
784 let actual_body = product
785 .epoch_state_record_sequence
786 .get(epoch_index)
787 .map(|records| {
788 records
789 .iter()
790 .map(|(record, token)| format!("{record}{token}"))
791 .collect::<Vec<_>>()
792 })
793 .unwrap_or_default();
794 if actual_body != expected_body {
795 return Err(ExactSp3ValidationError::BodyRecordInterleavingMismatch {
796 epoch_index,
797 expected: expected_body,
798 actual: actual_body,
799 });
800 }
801 }
802 Ok(())
803}
804
805#[derive(Debug, Clone, Copy)]
806enum DurationField {
807 Span,
808 Sample,
809}
810
811fn parse_duration_token(token: &str, field: DurationField) -> Result<u64, ExactSp3ValidationError> {
812 let bytes = token.as_bytes();
813 let invalid = || match field {
814 DurationField::Span => ExactSp3ValidationError::UnsupportedSpanToken {
815 token: token.to_owned(),
816 },
817 DurationField::Sample => ExactSp3ValidationError::UnsupportedSampleToken {
818 token: token.to_owned(),
819 },
820 };
821 if bytes.len() != 3 || !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
822 return Err(invalid());
823 }
824 let amount = u64::from(bytes[0] - b'0') * 10 + u64::from(bytes[1] - b'0');
825 if amount == 0 {
826 return Err(invalid());
827 }
828 let unit_s = match bytes[2] {
829 b'S' => 1,
830 b'M' => 60,
831 b'H' => 3_600,
832 b'D' => 86_400,
833 b'W' if matches!(field, DurationField::Span) => 604_800,
834 _ => return Err(invalid()),
837 };
838 let canonical = match bytes[2] {
839 b'S' if amount % 60 == 0 => Some(format!("{:02}M", amount / 60)),
840 b'M' if amount % 60 == 0 => Some(format!("{:02}H", amount / 60)),
841 b'H' if amount % 24 == 0 => Some(format!("{:02}D", amount / 24)),
842 _ => None,
843 };
844 if let Some(canonical) = canonical {
845 return Err(match field {
846 DurationField::Span => ExactSp3ValidationError::NonCanonicalSpanToken {
847 token: token.to_owned(),
848 canonical,
849 },
850 DurationField::Sample => ExactSp3ValidationError::NonCanonicalSampleToken {
851 token: token.to_owned(),
852 canonical,
853 },
854 });
855 }
856 amount.checked_mul(unit_s).ok_or_else(invalid)
857}
858
859fn parse_issue(issue: Option<&str>) -> Result<(u8, u8), ExactSp3ValidationError> {
860 let Some(issue) = issue else {
861 return Ok((0, 0));
862 };
863 let bytes = issue.as_bytes();
864 if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
865 return Err(ExactSp3ValidationError::InvalidIssue {
866 issue: issue.to_owned(),
867 });
868 }
869 let hour = (bytes[0] - b'0') * 10 + (bytes[1] - b'0');
870 let minute = (bytes[2] - b'0') * 10 + (bytes[3] - b'0');
871 if hour > 23 || minute > 59 {
872 return Err(ExactSp3ValidationError::InvalidIssue {
873 issue: issue.to_owned(),
874 });
875 }
876 Ok((hour, minute))
877}
878
879fn requested_start_j2000_s(request: &ExactSp3Request) -> f64 {
880 let (hour, minute) = parse_issue(request.issue.as_deref())
881 .expect("ExactSp3Request construction validates its issue token");
882 j2000_seconds(
883 request.date.year,
884 i32::from(request.date.month),
885 i32::from(request.date.day),
886 i32::from(hour),
887 i32::from(minute),
888 0.0,
889 )
890}
891
892fn validate_line2_start_metadata(
899 product: &Sp3,
900 requested_start_j2000_s: f64,
901) -> Result<(), ExactSp3ValidationError> {
902 let requested_start_j2000_s = requested_start_j2000_s as i64;
905 let requested_mjd_total_s =
906 requested_start_j2000_s + J2000_MJD_DAY * SECONDS_PER_DAY_I64 + SECONDS_PER_DAY_I64 / 2;
907 let gps_zero_mjd_total_s = GPS_ZERO_MJD_DAY * SECONDS_PER_DAY_I64;
908 let since_gps_zero_s = requested_mjd_total_s - gps_zero_mjd_total_s;
909 if since_gps_zero_s < 0 {
910 return Err(ExactSp3ValidationError::RequestBeforeGpsEpoch);
911 }
912
913 let requested_week = since_gps_zero_s.div_euclid(SECONDS_PER_WEEK_I64);
914 let requested_sow_s = since_gps_zero_s.rem_euclid(SECONDS_PER_WEEK_I64) as f64;
915 if i64::from(product.header.gnss_week) != requested_week {
916 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
917 field: "gps_week",
918 requested: requested_week as f64,
919 actual: f64::from(product.header.gnss_week),
920 });
921 }
922
923 let header_sow_s = product.header.seconds_of_week;
924 if !header_sow_s.is_finite() {
925 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
926 field: "seconds_of_week",
927 });
928 }
929 if !(0.0..SECONDS_PER_WEEK_I64 as f64).contains(&header_sow_s) {
930 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
931 field: "seconds_of_week",
932 actual: header_sow_s,
933 });
934 }
935 if !seconds_match(header_sow_s, requested_sow_s) {
936 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
937 field: "seconds_of_week",
938 requested: requested_sow_s,
939 actual: header_sow_s,
940 });
941 }
942
943 let header_mjd_fraction = product.header.mjd_fraction;
944 if !header_mjd_fraction.is_finite() {
945 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
946 field: "mjd_fraction",
947 });
948 }
949 if !(0.0..1.0).contains(&header_mjd_fraction) {
950 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
951 field: "mjd_fraction",
952 actual: header_mjd_fraction,
953 });
954 }
955 let header_mjd_total_s =
956 i64::from(product.header.mjd) as f64 * 86_400.0 + header_mjd_fraction * 86_400.0;
957 if !seconds_match(header_mjd_total_s, requested_mjd_total_s as f64) {
958 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
959 field: "mjd",
960 requested: requested_mjd_total_s as f64 / 86_400.0,
961 actual: header_mjd_total_s / 86_400.0,
962 });
963 }
964 Ok(())
965}
966
967fn seconds_match(left: f64, right: f64) -> bool {
968 (left - right).abs() <= WHOLE_SECOND_EPS_S
969}
970
971fn validate_format_version(
972 actual: Sp3Version,
973 requested: Option<&str>,
974) -> Result<(), ExactSp3ValidationError> {
975 let Some(requested) = requested else {
976 return Ok(());
977 };
978 let actual = match actual {
979 Sp3Version::A => "SP3-a",
980 Sp3Version::B => "SP3-b",
981 Sp3Version::C => "SP3-c",
982 Sp3Version::D => "SP3-d",
983 };
984 if requested.eq_ignore_ascii_case(actual) {
985 Ok(())
986 } else {
987 Err(ExactSp3ValidationError::FormatVersionMismatch {
988 requested: requested.to_owned(),
989 actual: actual.to_owned(),
990 })
991 }
992}