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 content_start_offset_s: i64,
54}
55
56impl ExactSp3Request {
57 pub fn new(
59 date: ProductDate,
60 issue: Option<&str>,
61 span: &str,
62 sample: &str,
63 ) -> Result<Self, ExactSp3ValidationError> {
64 ProductDate::new(date.year, date.month, date.day)
66 .map_err(ExactSp3ValidationError::Catalog)?;
67 parse_issue(issue)?;
68 parse_duration_token(span, DurationField::Span)?;
69 let cadence_s = parse_duration_token(sample, DurationField::Sample)?;
70 if cadence_s as f64 >= SP3_MAX_EPOCH_INTERVAL_S {
71 return Err(ExactSp3ValidationError::UnsupportedSampleToken {
72 token: sample.to_owned(),
73 });
74 }
75
76 Ok(Self {
77 date,
78 issue: issue.map(str::to_owned),
79 span: span.to_owned(),
80 sample: sample.to_owned(),
81 format_version: None,
82 expected_agency: None,
83 content_start_offset_s: 0,
84 })
85 }
86
87 pub fn from_identity(identity: &ProductIdentity) -> Result<Self, ExactSp3ValidationError> {
89 if identity.family != ProductType::Sp3 {
90 return Err(ExactSp3ValidationError::WrongProductFamily {
91 actual: identity.family,
92 });
93 }
94 identity
95 .validate()
96 .map_err(ExactSp3ValidationError::Catalog)?;
97 let mut request = Self::new(
98 identity.date,
99 identity.issue.as_deref(),
100 &identity.span,
101 &identity.sample,
102 )?;
103 request.format_version = identity.format_version.clone();
104 request.content_start_offset_s = crate::data::exact_sp3_content_start_offset_s(identity)
105 .map_err(ExactSp3ValidationError::Catalog)?;
106 request.expected_agency = Some(
107 match identity.analysis_center {
108 AnalysisCenter::Igs => "IGS",
109 AnalysisCenter::Esa | AnalysisCenter::EsaUlt => "ESOC",
110 AnalysisCenter::Gfz | AnalysisCenter::GfzUlt => "GFZ",
111 AnalysisCenter::Cod
112 | AnalysisCenter::CodRap
113 | AnalysisCenter::CodPrd1
114 | AnalysisCenter::CodPrd2
115 | AnalysisCenter::CodUlt => "AIUB",
116 AnalysisCenter::IgsUlt => "IGS",
117 AnalysisCenter::WumNrt => "WHU",
118 }
119 .to_owned(),
120 );
121 Ok(request)
122 }
123
124 pub fn with_expected_agency(mut self, agency: &str) -> Result<Self, ExactSp3ValidationError> {
129 if agency.is_empty()
130 || agency.len() > 4
131 || !agency
132 .bytes()
133 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
134 {
135 return Err(ExactSp3ValidationError::InvalidExpectedAgency {
136 agency: agency.to_owned(),
137 });
138 }
139 self.expected_agency = Some(agency.to_owned());
140 Ok(self)
141 }
142
143 pub fn date(&self) -> ProductDate {
149 self.date
150 }
151
152 pub fn issue(&self) -> Option<&str> {
157 self.issue.as_deref()
158 }
159
160 pub fn span(&self) -> &str {
162 &self.span
163 }
164
165 pub fn sample(&self) -> &str {
167 &self.sample
168 }
169
170 pub fn format_version(&self) -> Option<&str> {
172 self.format_version.as_deref()
173 }
174
175 pub fn expected_agency(&self) -> Option<&str> {
177 self.expected_agency.as_deref()
178 }
179}
180
181#[derive(Debug, Clone, PartialEq)]
183#[non_exhaustive]
184pub enum ExactSp3ValidationError {
185 Parse(crate::Error),
187 Catalog(DataCatalogError),
189 WrongProductFamily {
191 actual: ProductType,
193 },
194 InvalidIssue {
196 issue: String,
199 },
200 UnsupportedSpanToken {
202 token: String,
204 },
205 UnsupportedSampleToken {
207 token: String,
209 },
210 NonCanonicalSpanToken {
212 token: String,
214 canonical: String,
216 },
217 NonCanonicalSampleToken {
219 token: String,
221 canonical: String,
223 },
224 InvalidExpectedAgency {
226 agency: String,
228 },
229 AgencyMismatch {
231 expected: String,
233 actual: String,
235 },
236 MissingEof,
238 MalformedEofRecord {
241 line_number: usize,
243 record_length: usize,
245 },
246 TrailingContentAfterEof,
248 MandatoryHeaderRecordCount {
250 record: &'static str,
252 expected: usize,
254 actual: usize,
256 },
257 MissingDeclaredSatelliteCount,
259 DeclaredSatelliteCountMismatch {
262 declared: usize,
264 tokens: usize,
266 },
267 DuplicateDeclaredSatellite {
269 token: String,
271 first_index: usize,
273 duplicate_index: usize,
275 },
276 NoDeclaredSatellites,
278 SatelliteRecordSequenceMismatch {
281 record: &'static str,
283 epoch_index: usize,
285 expected: Vec<String>,
287 actual: Vec<String>,
289 },
290 BodyRecordInterleavingMismatch {
293 epoch_index: usize,
295 expected: Vec<String>,
297 actual: Vec<String>,
299 },
300 NonFiniteHeaderCadence,
302 NonPositiveHeaderCadence {
304 actual_s: f64,
306 },
307 UnsupportedHeaderCadence {
309 actual_s: f64,
311 },
312 CadenceMismatch {
314 requested_s: f64,
316 header_s: f64,
318 },
319 DeclaredEpochCountMismatch {
321 declared: u64,
323 parsed: usize,
325 },
326 MissingDeclaredStart,
328 DeclaredStartMismatch {
330 requested_j2000_s: f64,
332 declared_j2000_s: f64,
334 },
335 RequestBeforeGpsEpoch,
338 NonFiniteHeaderStartMetadata {
340 field: &'static str,
342 },
343 InvalidHeaderStartMetadata {
345 field: &'static str,
347 actual: f64,
349 },
350 HeaderStartMetadataMismatch {
353 field: &'static str,
355 requested: f64,
357 actual: f64,
359 },
360 EmptyEpochGrid,
362 FirstEpochMismatch {
364 requested_j2000_s: f64,
366 actual_j2000_s: f64,
368 },
369 IrregularEpochGrid {
371 epoch_index: usize,
373 requested_s: f64,
375 actual_s: f64,
377 },
378 SpanNotMultipleOfCadence {
380 span_s: u64,
382 cadence_s: u64,
384 },
385 SpanMismatch {
388 parsed: usize,
390 half_open: usize,
392 inclusive: usize,
394 },
395 FormatVersionMismatch {
398 requested: String,
400 actual: String,
402 },
403}
404
405impl fmt::Display for ExactSp3ValidationError {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 match self {
408 Self::Parse(error) => write!(f, "exact SP3 parse failed: {error}"),
409 Self::Catalog(error) => write!(f, "invalid exact SP3 identity: {error}"),
410 Self::WrongProductFamily { actual } => {
411 write!(f, "exact SP3 validation cannot validate {actual}")
412 }
413 Self::InvalidIssue { issue } => {
414 write!(f, "invalid exact SP3 issue time {issue:?}; expected HHMM")
415 }
416 Self::UnsupportedSpanToken { token } => {
417 write!(f, "unsupported exact SP3 span token {token:?}")
418 }
419 Self::UnsupportedSampleToken { token } => {
420 write!(f, "unsupported exact SP3 sample token {token:?}")
421 }
422 Self::NonCanonicalSpanToken { token, canonical } => write!(
423 f,
424 "noncanonical exact SP3 span token {token:?}; use {canonical:?}"
425 ),
426 Self::NonCanonicalSampleToken { token, canonical } => write!(
427 f,
428 "noncanonical exact SP3 sample token {token:?}; use {canonical:?}"
429 ),
430 Self::InvalidExpectedAgency { agency } => {
431 write!(f, "invalid exact SP3 expected agency {agency:?}")
432 }
433 Self::AgencyMismatch { expected, actual } => write!(
434 f,
435 "SP3 agency mismatch: requested {expected:?}, header declares {actual:?}"
436 ),
437 Self::MissingEof => write!(f, "SP3 product is missing its EOF record"),
438 Self::MalformedEofRecord {
439 line_number,
440 record_length,
441 } => write!(
442 f,
443 "SP3 product contains a malformed EOF record at line {line_number} ({record_length} bytes)"
444 ),
445 Self::TrailingContentAfterEof => {
446 write!(f, "SP3 product contains nonblank records after EOF")
447 }
448 Self::MandatoryHeaderRecordCount {
449 record,
450 expected,
451 actual,
452 } => write!(
453 f,
454 "SP3 header record {record} count is {actual}, expected {expected}"
455 ),
456 Self::MissingDeclaredSatelliteCount => {
457 write!(f, "SP3 line 3 has no valid declared satellite count")
458 }
459 Self::DeclaredSatelliteCountMismatch { declared, tokens } => write!(
460 f,
461 "SP3 declared satellite-count mismatch: line 3 declares {declared}, header contains {tokens} tokens"
462 ),
463 Self::DuplicateDeclaredSatellite {
464 token,
465 first_index,
466 duplicate_index,
467 } => write!(
468 f,
469 "SP3 header satellite {token:?} is duplicated at indices {first_index} and {duplicate_index}"
470 ),
471 Self::NoDeclaredSatellites => {
472 write!(f, "SP3 product declares no satellites")
473 }
474 Self::SatelliteRecordSequenceMismatch {
475 record,
476 epoch_index,
477 expected,
478 actual,
479 } => write!(
480 f,
481 "SP3 epoch {epoch_index} {record}-record sequence mismatch: expected {expected:?}, got {actual:?}"
482 ),
483 Self::BodyRecordInterleavingMismatch {
484 epoch_index,
485 expected,
486 actual,
487 } => write!(
488 f,
489 "SP3 epoch {epoch_index} body record ordering mismatch: expected {expected:?}, got {actual:?}"
490 ),
491 Self::NonFiniteHeaderCadence => {
492 write!(f, "SP3 header cadence is not finite")
493 }
494 Self::NonPositiveHeaderCadence { actual_s } => {
495 write!(f, "SP3 header cadence must be positive, got {actual_s}")
496 }
497 Self::UnsupportedHeaderCadence { actual_s } => write!(
498 f,
499 "SP3 header cadence {actual_s} s is outside the supported format range"
500 ),
501 Self::CadenceMismatch {
502 requested_s,
503 header_s,
504 } => write!(
505 f,
506 "SP3 cadence mismatch: requested {requested_s} s, header declares {header_s} s"
507 ),
508 Self::DeclaredEpochCountMismatch { declared, parsed } => write!(
509 f,
510 "SP3 epoch-count mismatch: header declares {declared}, parsed {parsed}"
511 ),
512 Self::MissingDeclaredStart => {
513 write!(f, "SP3 header line 1 has no valid declared start epoch")
514 }
515 Self::DeclaredStartMismatch {
516 requested_j2000_s,
517 declared_j2000_s,
518 } => write!(
519 f,
520 "SP3 declared start mismatch: requested {requested_j2000_s} J2000 s, header declares {declared_j2000_s} J2000 s"
521 ),
522 Self::RequestBeforeGpsEpoch => {
523 write!(f, "exact SP3 request starts before the GPS week epoch")
524 }
525 Self::NonFiniteHeaderStartMetadata { field } => {
526 write!(f, "SP3 header start field {field} is not finite")
527 }
528 Self::InvalidHeaderStartMetadata { field, actual } => {
529 write!(f, "SP3 header start field {field} is out of range: {actual}")
530 }
531 Self::HeaderStartMetadataMismatch {
532 field,
533 requested,
534 actual,
535 } => write!(
536 f,
537 "SP3 header start mismatch in {field}: requested {requested}, header has {actual}"
538 ),
539 Self::EmptyEpochGrid => write!(f, "SP3 product has no epoch records"),
540 Self::FirstEpochMismatch {
541 requested_j2000_s,
542 actual_j2000_s,
543 } => write!(
544 f,
545 "SP3 first epoch mismatch: requested {requested_j2000_s} J2000 s, parsed {actual_j2000_s} J2000 s"
546 ),
547 Self::IrregularEpochGrid {
548 epoch_index,
549 requested_s,
550 actual_s,
551 } => write!(
552 f,
553 "SP3 epoch grid is irregular at index {epoch_index}: requested step {requested_s} s, got {actual_s} s"
554 ),
555 Self::SpanNotMultipleOfCadence {
556 span_s,
557 cadence_s,
558 } => write!(
559 f,
560 "exact SP3 span {span_s} s is not a multiple of cadence {cadence_s} s"
561 ),
562 Self::SpanMismatch {
563 parsed,
564 half_open,
565 inclusive,
566 } => write!(
567 f,
568 "SP3 span mismatch: parsed {parsed} epochs, expected {half_open} half-open or {inclusive} inclusive"
569 ),
570 Self::FormatVersionMismatch { requested, actual } => write!(
571 f,
572 "SP3 format-version mismatch: requested {requested:?}, parsed {actual:?}"
573 ),
574 }
575 }
576}
577
578impl std::error::Error for ExactSp3ValidationError {
579 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
580 match self {
581 Self::Parse(error) => Some(error),
582 Self::Catalog(error) => Some(error),
583 _ => None,
584 }
585 }
586}
587
588pub fn parse_exact_sp3(
593 bytes: &[u8],
594 request: &ExactSp3Request,
595) -> Result<(Sp3, ExactSp3Coverage), ExactSp3ValidationError> {
596 let product = Sp3::parse(bytes).map_err(ExactSp3ValidationError::Parse)?;
597 let coverage = validate_exact_sp3(&product, request)?;
598 Ok((product, coverage))
599}
600
601pub fn validate_exact_sp3(
607 product: &Sp3,
608 request: &ExactSp3Request,
609) -> Result<ExactSp3Coverage, ExactSp3ValidationError> {
610 let cadence_s = parse_duration_token(&request.sample, DurationField::Sample)?;
611 let span_s = parse_duration_token(&request.span, DurationField::Span)?;
612
613 validate_mandatory_structure(product)?;
614 if let Some(expected) = request.expected_agency.as_deref() {
615 let actual = product.header.agency.trim();
616 if actual != expected {
617 return Err(ExactSp3ValidationError::AgencyMismatch {
618 expected: expected.to_owned(),
619 actual: actual.to_owned(),
620 });
621 }
622 }
623
624 let header_cadence_s = product.header.epoch_interval_s;
625 if !header_cadence_s.is_finite() {
626 return Err(ExactSp3ValidationError::NonFiniteHeaderCadence);
627 }
628 if header_cadence_s <= 0.0 {
629 return Err(ExactSp3ValidationError::NonPositiveHeaderCadence {
630 actual_s: header_cadence_s,
631 });
632 }
633 if header_cadence_s >= SP3_MAX_EPOCH_INTERVAL_S {
634 return Err(ExactSp3ValidationError::UnsupportedHeaderCadence {
635 actual_s: header_cadence_s,
636 });
637 }
638 if !seconds_match(header_cadence_s, cadence_s as f64) {
639 return Err(ExactSp3ValidationError::CadenceMismatch {
640 requested_s: cadence_s as f64,
641 header_s: header_cadence_s,
642 });
643 }
644
645 if product.declared_num_epochs != product.epoch_j2000_s.len() as u64 {
646 return Err(ExactSp3ValidationError::DeclaredEpochCountMismatch {
647 declared: product.declared_num_epochs,
648 parsed: product.epoch_j2000_s.len(),
649 });
650 }
651
652 let requested_start_j2000_s = requested_start_j2000_s(request);
653 let declared_start_j2000_s = product
654 .declared_start_j2000_s
655 .ok_or(ExactSp3ValidationError::MissingDeclaredStart)?;
656 if !seconds_match(declared_start_j2000_s, requested_start_j2000_s) {
657 return Err(ExactSp3ValidationError::DeclaredStartMismatch {
658 requested_j2000_s: requested_start_j2000_s,
659 declared_j2000_s: declared_start_j2000_s,
660 });
661 }
662 validate_line2_start_metadata(product, requested_start_j2000_s)?;
663
664 let first_j2000_s = product
665 .epoch_j2000_s
666 .first()
667 .copied()
668 .ok_or(ExactSp3ValidationError::EmptyEpochGrid)?;
669 if !seconds_match(first_j2000_s, requested_start_j2000_s) {
670 return Err(ExactSp3ValidationError::FirstEpochMismatch {
671 requested_j2000_s: requested_start_j2000_s,
672 actual_j2000_s: first_j2000_s,
673 });
674 }
675
676 for (index, pair) in product.epoch_j2000_s.windows(2).enumerate() {
677 let actual_s = pair[1] - pair[0];
678 if !actual_s.is_finite() || !seconds_match(actual_s, cadence_s as f64) {
679 return Err(ExactSp3ValidationError::IrregularEpochGrid {
680 epoch_index: index + 1,
681 requested_s: cadence_s as f64,
682 actual_s,
683 });
684 }
685 }
686
687 if span_s % cadence_s != 0 {
688 return Err(ExactSp3ValidationError::SpanNotMultipleOfCadence { span_s, cadence_s });
689 }
690 let half_open = usize::try_from(span_s / cadence_s).unwrap_or(usize::MAX);
691 let inclusive = half_open.saturating_add(1);
692 match product.epoch_j2000_s.len() {
693 count if count == half_open => Ok(ExactSp3Coverage::HalfOpen),
694 count if count == inclusive => Ok(ExactSp3Coverage::Inclusive),
695 parsed => Err(ExactSp3ValidationError::SpanMismatch {
696 parsed,
697 half_open,
698 inclusive,
699 }),
700 }
701 .and_then(|coverage| {
702 validate_format_version(product.header.version, request.format_version.as_deref())?;
703 Ok(coverage)
704 })
705}
706
707fn validate_mandatory_structure(product: &Sp3) -> Result<(), ExactSp3ValidationError> {
708 if let Some(malformed) = product.terminal_record.first_malformed_record {
709 return Err(ExactSp3ValidationError::MalformedEofRecord {
710 line_number: malformed.line_number,
711 record_length: malformed.record_length,
712 });
713 }
714 if !product.terminal_record.had_valid_record {
715 return Err(ExactSp3ValidationError::MissingEof);
716 }
717 if product.terminal_record.had_trailing_content {
718 return Err(ExactSp3ValidationError::TrailingContentAfterEof);
719 }
720 if product.satellite_header_lines < 5 {
721 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
722 record: "+",
723 expected: 5,
724 actual: product.satellite_header_lines,
725 });
726 }
727 if product.accuracy_header_lines != product.satellite_header_lines {
728 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
729 record: "++",
730 expected: product.satellite_header_lines,
731 actual: product.accuracy_header_lines,
732 });
733 }
734 for (record, actual) in [
735 ("%c", product.time_system_header_lines),
736 ("%f", product.float_header_lines),
737 ("%i", product.integer_header_lines),
738 ] {
739 if actual != 2 {
740 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
741 record,
742 expected: 2,
743 actual,
744 });
745 }
746 }
747 if product.header_comment_lines < 4 {
748 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
749 record: "/*",
750 expected: 4,
751 actual: product.header_comment_lines,
752 });
753 }
754 let declared_count = product
755 .declared_satellite_count
756 .ok_or(ExactSp3ValidationError::MissingDeclaredSatelliteCount)?;
757 if declared_count != product.declared_satellite_tokens.len() {
758 return Err(ExactSp3ValidationError::DeclaredSatelliteCountMismatch {
759 declared: declared_count,
760 tokens: product.declared_satellite_tokens.len(),
761 });
762 }
763 for duplicate_index in 0..product.declared_satellite_tokens.len() {
764 if let Some(first_index) = product.declared_satellite_tokens[..duplicate_index]
765 .iter()
766 .position(|token| token == &product.declared_satellite_tokens[duplicate_index])
767 {
768 return Err(ExactSp3ValidationError::DuplicateDeclaredSatellite {
769 token: product.declared_satellite_tokens[duplicate_index].clone(),
770 first_index,
771 duplicate_index,
772 });
773 }
774 }
775 if product.header.satellites.is_empty() {
776 return Err(ExactSp3ValidationError::NoDeclaredSatellites);
777 }
778 for epoch_index in 0..product.epochs.len() {
779 let positions = product
780 .epoch_position_tokens
781 .get(epoch_index)
782 .cloned()
783 .unwrap_or_default();
784 if positions != product.declared_satellite_tokens {
785 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
786 record: "P",
787 epoch_index,
788 expected: product.declared_satellite_tokens.clone(),
789 actual: positions,
790 });
791 }
792 let velocities = product
793 .epoch_velocity_tokens
794 .get(epoch_index)
795 .cloned()
796 .unwrap_or_default();
797 let expected_velocities = match product.header.data_type {
798 Sp3DataType::Position => Vec::new(),
799 Sp3DataType::Velocity => product.declared_satellite_tokens.clone(),
800 };
801 if velocities != expected_velocities {
802 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
803 record: "V",
804 epoch_index,
805 expected: expected_velocities,
806 actual: velocities,
807 });
808 }
809 let mut expected_body = Vec::with_capacity(match product.header.data_type {
810 Sp3DataType::Position => product.declared_satellite_tokens.len(),
811 Sp3DataType::Velocity => product.declared_satellite_tokens.len() * 2,
812 });
813 for token in &product.declared_satellite_tokens {
814 expected_body.push(format!("P{token}"));
815 if matches!(product.header.data_type, Sp3DataType::Velocity) {
816 expected_body.push(format!("V{token}"));
817 }
818 }
819 let actual_body = product
820 .epoch_state_record_sequence
821 .get(epoch_index)
822 .map(|records| {
823 records
824 .iter()
825 .map(|(record, token)| format!("{record}{token}"))
826 .collect::<Vec<_>>()
827 })
828 .unwrap_or_default();
829 if actual_body != expected_body {
830 return Err(ExactSp3ValidationError::BodyRecordInterleavingMismatch {
831 epoch_index,
832 expected: expected_body,
833 actual: actual_body,
834 });
835 }
836 }
837 Ok(())
838}
839
840#[derive(Debug, Clone, Copy)]
841enum DurationField {
842 Span,
843 Sample,
844}
845
846fn parse_duration_token(token: &str, field: DurationField) -> Result<u64, ExactSp3ValidationError> {
847 let bytes = token.as_bytes();
848 let invalid = || match field {
849 DurationField::Span => ExactSp3ValidationError::UnsupportedSpanToken {
850 token: token.to_owned(),
851 },
852 DurationField::Sample => ExactSp3ValidationError::UnsupportedSampleToken {
853 token: token.to_owned(),
854 },
855 };
856 if bytes.len() != 3 || !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
857 return Err(invalid());
858 }
859 let amount = u64::from(bytes[0] - b'0') * 10 + u64::from(bytes[1] - b'0');
860 if amount == 0 {
861 return Err(invalid());
862 }
863 let unit_s = match bytes[2] {
864 b'S' => 1,
865 b'M' => 60,
866 b'H' => 3_600,
867 b'D' => 86_400,
868 b'W' if matches!(field, DurationField::Span) => 604_800,
869 _ => return Err(invalid()),
872 };
873 let canonical = match bytes[2] {
874 b'S' if amount % 60 == 0 => Some(format!("{:02}M", amount / 60)),
875 b'M' if amount % 60 == 0 => Some(format!("{:02}H", amount / 60)),
876 b'H' if amount % 24 == 0 => Some(format!("{:02}D", amount / 24)),
877 _ => None,
878 };
879 if let Some(canonical) = canonical {
880 return Err(match field {
881 DurationField::Span => ExactSp3ValidationError::NonCanonicalSpanToken {
882 token: token.to_owned(),
883 canonical,
884 },
885 DurationField::Sample => ExactSp3ValidationError::NonCanonicalSampleToken {
886 token: token.to_owned(),
887 canonical,
888 },
889 });
890 }
891 amount.checked_mul(unit_s).ok_or_else(invalid)
892}
893
894fn parse_issue(issue: Option<&str>) -> Result<(u8, u8), ExactSp3ValidationError> {
895 let Some(issue) = issue else {
896 return Ok((0, 0));
897 };
898 let bytes = issue.as_bytes();
899 if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
900 return Err(ExactSp3ValidationError::InvalidIssue {
901 issue: issue.to_owned(),
902 });
903 }
904 let hour = (bytes[0] - b'0') * 10 + (bytes[1] - b'0');
905 let minute = (bytes[2] - b'0') * 10 + (bytes[3] - b'0');
906 if hour > 23 || minute > 59 {
907 return Err(ExactSp3ValidationError::InvalidIssue {
908 issue: issue.to_owned(),
909 });
910 }
911 Ok((hour, minute))
912}
913
914fn requested_start_j2000_s(request: &ExactSp3Request) -> f64 {
915 let (hour, minute) = parse_issue(request.issue.as_deref())
916 .expect("ExactSp3Request construction validates its issue token");
917 let filename_epoch_j2000_s = j2000_seconds(
918 request.date.year,
919 i32::from(request.date.month),
920 i32::from(request.date.day),
921 i32::from(hour),
922 i32::from(minute),
923 0.0,
924 );
925 filename_epoch_j2000_s + request.content_start_offset_s as f64
929}
930
931fn validate_line2_start_metadata(
938 product: &Sp3,
939 requested_start_j2000_s: f64,
940) -> Result<(), ExactSp3ValidationError> {
941 let requested_start_j2000_s = requested_start_j2000_s as i64;
944 let requested_mjd_total_s =
945 requested_start_j2000_s + J2000_MJD_DAY * SECONDS_PER_DAY_I64 + SECONDS_PER_DAY_I64 / 2;
946 let gps_zero_mjd_total_s = GPS_ZERO_MJD_DAY * SECONDS_PER_DAY_I64;
947 let since_gps_zero_s = requested_mjd_total_s - gps_zero_mjd_total_s;
948 if since_gps_zero_s < 0 {
949 return Err(ExactSp3ValidationError::RequestBeforeGpsEpoch);
950 }
951
952 let requested_week = since_gps_zero_s.div_euclid(SECONDS_PER_WEEK_I64);
953 let requested_sow_s = since_gps_zero_s.rem_euclid(SECONDS_PER_WEEK_I64) as f64;
954 if i64::from(product.header.gnss_week) != requested_week {
955 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
956 field: "gps_week",
957 requested: requested_week as f64,
958 actual: f64::from(product.header.gnss_week),
959 });
960 }
961
962 let header_sow_s = product.header.seconds_of_week;
963 if !header_sow_s.is_finite() {
964 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
965 field: "seconds_of_week",
966 });
967 }
968 if !(0.0..SECONDS_PER_WEEK_I64 as f64).contains(&header_sow_s) {
969 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
970 field: "seconds_of_week",
971 actual: header_sow_s,
972 });
973 }
974 if !seconds_match(header_sow_s, requested_sow_s) {
975 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
976 field: "seconds_of_week",
977 requested: requested_sow_s,
978 actual: header_sow_s,
979 });
980 }
981
982 let header_mjd_fraction = product.header.mjd_fraction;
983 if !header_mjd_fraction.is_finite() {
984 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
985 field: "mjd_fraction",
986 });
987 }
988 if !(0.0..1.0).contains(&header_mjd_fraction) {
989 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
990 field: "mjd_fraction",
991 actual: header_mjd_fraction,
992 });
993 }
994 let header_mjd_total_s =
995 i64::from(product.header.mjd) as f64 * 86_400.0 + header_mjd_fraction * 86_400.0;
996 if !seconds_match(header_mjd_total_s, requested_mjd_total_s as f64) {
997 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
998 field: "mjd",
999 requested: requested_mjd_total_s as f64 / 86_400.0,
1000 actual: header_mjd_total_s / 86_400.0,
1001 });
1002 }
1003 Ok(())
1004}
1005
1006fn seconds_match(left: f64, right: f64) -> bool {
1007 (left - right).abs() <= WHOLE_SECOND_EPS_S
1008}
1009
1010fn validate_format_version(
1011 actual: Sp3Version,
1012 requested: Option<&str>,
1013) -> Result<(), ExactSp3ValidationError> {
1014 let Some(requested) = requested else {
1015 return Ok(());
1016 };
1017 let actual = match actual {
1018 Sp3Version::A => "SP3-a",
1019 Sp3Version::B => "SP3-b",
1020 Sp3Version::C => "SP3-c",
1021 Sp3Version::D => "SP3-d",
1022 };
1023 if requested.eq_ignore_ascii_case(actual) {
1024 Ok(())
1025 } else {
1026 Err(ExactSp3ValidationError::FormatVersionMismatch {
1027 requested: requested.to_owned(),
1028 actual: actual.to_owned(),
1029 })
1030 }
1031}