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 }
118 .to_owned(),
119 );
120 Ok(request)
121 }
122
123 pub fn with_expected_agency(mut self, agency: &str) -> Result<Self, ExactSp3ValidationError> {
128 if agency.is_empty()
129 || agency.len() > 4
130 || !agency
131 .bytes()
132 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
133 {
134 return Err(ExactSp3ValidationError::InvalidExpectedAgency {
135 agency: agency.to_owned(),
136 });
137 }
138 self.expected_agency = Some(agency.to_owned());
139 Ok(self)
140 }
141
142 pub fn date(&self) -> ProductDate {
148 self.date
149 }
150
151 pub fn issue(&self) -> Option<&str> {
156 self.issue.as_deref()
157 }
158
159 pub fn span(&self) -> &str {
161 &self.span
162 }
163
164 pub fn sample(&self) -> &str {
166 &self.sample
167 }
168
169 pub fn format_version(&self) -> Option<&str> {
171 self.format_version.as_deref()
172 }
173
174 pub fn expected_agency(&self) -> Option<&str> {
176 self.expected_agency.as_deref()
177 }
178}
179
180#[derive(Debug, Clone, PartialEq)]
182#[non_exhaustive]
183pub enum ExactSp3ValidationError {
184 Parse(crate::Error),
186 Catalog(DataCatalogError),
188 WrongProductFamily {
190 actual: ProductType,
192 },
193 InvalidIssue {
195 issue: String,
198 },
199 UnsupportedSpanToken {
201 token: String,
203 },
204 UnsupportedSampleToken {
206 token: String,
208 },
209 NonCanonicalSpanToken {
211 token: String,
213 canonical: String,
215 },
216 NonCanonicalSampleToken {
218 token: String,
220 canonical: String,
222 },
223 InvalidExpectedAgency {
225 agency: String,
227 },
228 AgencyMismatch {
230 expected: String,
232 actual: String,
234 },
235 MissingEof,
237 MalformedEofRecord {
240 line_number: usize,
242 record_length: usize,
244 },
245 TrailingContentAfterEof,
247 MandatoryHeaderRecordCount {
249 record: &'static str,
251 expected: usize,
253 actual: usize,
255 },
256 MissingDeclaredSatelliteCount,
258 DeclaredSatelliteCountMismatch {
261 declared: usize,
263 tokens: usize,
265 },
266 DuplicateDeclaredSatellite {
268 token: String,
270 first_index: usize,
272 duplicate_index: usize,
274 },
275 NoDeclaredSatellites,
277 SatelliteRecordSequenceMismatch {
280 record: &'static str,
282 epoch_index: usize,
284 expected: Vec<String>,
286 actual: Vec<String>,
288 },
289 BodyRecordInterleavingMismatch {
292 epoch_index: usize,
294 expected: Vec<String>,
296 actual: Vec<String>,
298 },
299 NonFiniteHeaderCadence,
301 NonPositiveHeaderCadence {
303 actual_s: f64,
305 },
306 UnsupportedHeaderCadence {
308 actual_s: f64,
310 },
311 CadenceMismatch {
313 requested_s: f64,
315 header_s: f64,
317 },
318 DeclaredEpochCountMismatch {
320 declared: u64,
322 parsed: usize,
324 },
325 MissingDeclaredStart,
327 DeclaredStartMismatch {
329 requested_j2000_s: f64,
331 declared_j2000_s: f64,
333 },
334 RequestBeforeGpsEpoch,
337 NonFiniteHeaderStartMetadata {
339 field: &'static str,
341 },
342 InvalidHeaderStartMetadata {
344 field: &'static str,
346 actual: f64,
348 },
349 HeaderStartMetadataMismatch {
352 field: &'static str,
354 requested: f64,
356 actual: f64,
358 },
359 EmptyEpochGrid,
361 FirstEpochMismatch {
363 requested_j2000_s: f64,
365 actual_j2000_s: f64,
367 },
368 IrregularEpochGrid {
370 epoch_index: usize,
372 requested_s: f64,
374 actual_s: f64,
376 },
377 SpanNotMultipleOfCadence {
379 span_s: u64,
381 cadence_s: u64,
383 },
384 SpanMismatch {
387 parsed: usize,
389 half_open: usize,
391 inclusive: usize,
393 },
394 FormatVersionMismatch {
397 requested: String,
399 actual: String,
401 },
402}
403
404impl fmt::Display for ExactSp3ValidationError {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 match self {
407 Self::Parse(error) => write!(f, "exact SP3 parse failed: {error}"),
408 Self::Catalog(error) => write!(f, "invalid exact SP3 identity: {error}"),
409 Self::WrongProductFamily { actual } => {
410 write!(f, "exact SP3 validation cannot validate {actual}")
411 }
412 Self::InvalidIssue { issue } => {
413 write!(f, "invalid exact SP3 issue time {issue:?}; expected HHMM")
414 }
415 Self::UnsupportedSpanToken { token } => {
416 write!(f, "unsupported exact SP3 span token {token:?}")
417 }
418 Self::UnsupportedSampleToken { token } => {
419 write!(f, "unsupported exact SP3 sample token {token:?}")
420 }
421 Self::NonCanonicalSpanToken { token, canonical } => write!(
422 f,
423 "noncanonical exact SP3 span token {token:?}; use {canonical:?}"
424 ),
425 Self::NonCanonicalSampleToken { token, canonical } => write!(
426 f,
427 "noncanonical exact SP3 sample token {token:?}; use {canonical:?}"
428 ),
429 Self::InvalidExpectedAgency { agency } => {
430 write!(f, "invalid exact SP3 expected agency {agency:?}")
431 }
432 Self::AgencyMismatch { expected, actual } => write!(
433 f,
434 "SP3 agency mismatch: requested {expected:?}, header declares {actual:?}"
435 ),
436 Self::MissingEof => write!(f, "SP3 product is missing its EOF record"),
437 Self::MalformedEofRecord {
438 line_number,
439 record_length,
440 } => write!(
441 f,
442 "SP3 product contains a malformed EOF record at line {line_number} ({record_length} bytes)"
443 ),
444 Self::TrailingContentAfterEof => {
445 write!(f, "SP3 product contains nonblank records after EOF")
446 }
447 Self::MandatoryHeaderRecordCount {
448 record,
449 expected,
450 actual,
451 } => write!(
452 f,
453 "SP3 header record {record} count is {actual}, expected {expected}"
454 ),
455 Self::MissingDeclaredSatelliteCount => {
456 write!(f, "SP3 line 3 has no valid declared satellite count")
457 }
458 Self::DeclaredSatelliteCountMismatch { declared, tokens } => write!(
459 f,
460 "SP3 declared satellite-count mismatch: line 3 declares {declared}, header contains {tokens} tokens"
461 ),
462 Self::DuplicateDeclaredSatellite {
463 token,
464 first_index,
465 duplicate_index,
466 } => write!(
467 f,
468 "SP3 header satellite {token:?} is duplicated at indices {first_index} and {duplicate_index}"
469 ),
470 Self::NoDeclaredSatellites => {
471 write!(f, "SP3 product declares no satellites")
472 }
473 Self::SatelliteRecordSequenceMismatch {
474 record,
475 epoch_index,
476 expected,
477 actual,
478 } => write!(
479 f,
480 "SP3 epoch {epoch_index} {record}-record sequence mismatch: expected {expected:?}, got {actual:?}"
481 ),
482 Self::BodyRecordInterleavingMismatch {
483 epoch_index,
484 expected,
485 actual,
486 } => write!(
487 f,
488 "SP3 epoch {epoch_index} body record ordering mismatch: expected {expected:?}, got {actual:?}"
489 ),
490 Self::NonFiniteHeaderCadence => {
491 write!(f, "SP3 header cadence is not finite")
492 }
493 Self::NonPositiveHeaderCadence { actual_s } => {
494 write!(f, "SP3 header cadence must be positive, got {actual_s}")
495 }
496 Self::UnsupportedHeaderCadence { actual_s } => write!(
497 f,
498 "SP3 header cadence {actual_s} s is outside the supported format range"
499 ),
500 Self::CadenceMismatch {
501 requested_s,
502 header_s,
503 } => write!(
504 f,
505 "SP3 cadence mismatch: requested {requested_s} s, header declares {header_s} s"
506 ),
507 Self::DeclaredEpochCountMismatch { declared, parsed } => write!(
508 f,
509 "SP3 epoch-count mismatch: header declares {declared}, parsed {parsed}"
510 ),
511 Self::MissingDeclaredStart => {
512 write!(f, "SP3 header line 1 has no valid declared start epoch")
513 }
514 Self::DeclaredStartMismatch {
515 requested_j2000_s,
516 declared_j2000_s,
517 } => write!(
518 f,
519 "SP3 declared start mismatch: requested {requested_j2000_s} J2000 s, header declares {declared_j2000_s} J2000 s"
520 ),
521 Self::RequestBeforeGpsEpoch => {
522 write!(f, "exact SP3 request starts before the GPS week epoch")
523 }
524 Self::NonFiniteHeaderStartMetadata { field } => {
525 write!(f, "SP3 header start field {field} is not finite")
526 }
527 Self::InvalidHeaderStartMetadata { field, actual } => {
528 write!(f, "SP3 header start field {field} is out of range: {actual}")
529 }
530 Self::HeaderStartMetadataMismatch {
531 field,
532 requested,
533 actual,
534 } => write!(
535 f,
536 "SP3 header start mismatch in {field}: requested {requested}, header has {actual}"
537 ),
538 Self::EmptyEpochGrid => write!(f, "SP3 product has no epoch records"),
539 Self::FirstEpochMismatch {
540 requested_j2000_s,
541 actual_j2000_s,
542 } => write!(
543 f,
544 "SP3 first epoch mismatch: requested {requested_j2000_s} J2000 s, parsed {actual_j2000_s} J2000 s"
545 ),
546 Self::IrregularEpochGrid {
547 epoch_index,
548 requested_s,
549 actual_s,
550 } => write!(
551 f,
552 "SP3 epoch grid is irregular at index {epoch_index}: requested step {requested_s} s, got {actual_s} s"
553 ),
554 Self::SpanNotMultipleOfCadence {
555 span_s,
556 cadence_s,
557 } => write!(
558 f,
559 "exact SP3 span {span_s} s is not a multiple of cadence {cadence_s} s"
560 ),
561 Self::SpanMismatch {
562 parsed,
563 half_open,
564 inclusive,
565 } => write!(
566 f,
567 "SP3 span mismatch: parsed {parsed} epochs, expected {half_open} half-open or {inclusive} inclusive"
568 ),
569 Self::FormatVersionMismatch { requested, actual } => write!(
570 f,
571 "SP3 format-version mismatch: requested {requested:?}, parsed {actual:?}"
572 ),
573 }
574 }
575}
576
577impl std::error::Error for ExactSp3ValidationError {
578 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
579 match self {
580 Self::Parse(error) => Some(error),
581 Self::Catalog(error) => Some(error),
582 _ => None,
583 }
584 }
585}
586
587pub fn parse_exact_sp3(
592 bytes: &[u8],
593 request: &ExactSp3Request,
594) -> Result<(Sp3, ExactSp3Coverage), ExactSp3ValidationError> {
595 let product = Sp3::parse(bytes).map_err(ExactSp3ValidationError::Parse)?;
596 let coverage = validate_exact_sp3(&product, request)?;
597 Ok((product, coverage))
598}
599
600pub fn validate_exact_sp3(
606 product: &Sp3,
607 request: &ExactSp3Request,
608) -> Result<ExactSp3Coverage, ExactSp3ValidationError> {
609 let cadence_s = parse_duration_token(&request.sample, DurationField::Sample)?;
610 let span_s = parse_duration_token(&request.span, DurationField::Span)?;
611
612 validate_mandatory_structure(product)?;
613 if let Some(expected) = request.expected_agency.as_deref() {
614 let actual = product.header.agency.trim();
615 if actual != expected {
616 return Err(ExactSp3ValidationError::AgencyMismatch {
617 expected: expected.to_owned(),
618 actual: actual.to_owned(),
619 });
620 }
621 }
622
623 let header_cadence_s = product.header.epoch_interval_s;
624 if !header_cadence_s.is_finite() {
625 return Err(ExactSp3ValidationError::NonFiniteHeaderCadence);
626 }
627 if header_cadence_s <= 0.0 {
628 return Err(ExactSp3ValidationError::NonPositiveHeaderCadence {
629 actual_s: header_cadence_s,
630 });
631 }
632 if header_cadence_s >= SP3_MAX_EPOCH_INTERVAL_S {
633 return Err(ExactSp3ValidationError::UnsupportedHeaderCadence {
634 actual_s: header_cadence_s,
635 });
636 }
637 if !seconds_match(header_cadence_s, cadence_s as f64) {
638 return Err(ExactSp3ValidationError::CadenceMismatch {
639 requested_s: cadence_s as f64,
640 header_s: header_cadence_s,
641 });
642 }
643
644 if product.declared_num_epochs != product.epoch_j2000_s.len() as u64 {
645 return Err(ExactSp3ValidationError::DeclaredEpochCountMismatch {
646 declared: product.declared_num_epochs,
647 parsed: product.epoch_j2000_s.len(),
648 });
649 }
650
651 let requested_start_j2000_s = requested_start_j2000_s(request);
652 let declared_start_j2000_s = product
653 .declared_start_j2000_s
654 .ok_or(ExactSp3ValidationError::MissingDeclaredStart)?;
655 if !seconds_match(declared_start_j2000_s, requested_start_j2000_s) {
656 return Err(ExactSp3ValidationError::DeclaredStartMismatch {
657 requested_j2000_s: requested_start_j2000_s,
658 declared_j2000_s: declared_start_j2000_s,
659 });
660 }
661 validate_line2_start_metadata(product, requested_start_j2000_s)?;
662
663 let first_j2000_s = product
664 .epoch_j2000_s
665 .first()
666 .copied()
667 .ok_or(ExactSp3ValidationError::EmptyEpochGrid)?;
668 if !seconds_match(first_j2000_s, requested_start_j2000_s) {
669 return Err(ExactSp3ValidationError::FirstEpochMismatch {
670 requested_j2000_s: requested_start_j2000_s,
671 actual_j2000_s: first_j2000_s,
672 });
673 }
674
675 for (index, pair) in product.epoch_j2000_s.windows(2).enumerate() {
676 let actual_s = pair[1] - pair[0];
677 if !actual_s.is_finite() || !seconds_match(actual_s, cadence_s as f64) {
678 return Err(ExactSp3ValidationError::IrregularEpochGrid {
679 epoch_index: index + 1,
680 requested_s: cadence_s as f64,
681 actual_s,
682 });
683 }
684 }
685
686 if span_s % cadence_s != 0 {
687 return Err(ExactSp3ValidationError::SpanNotMultipleOfCadence { span_s, cadence_s });
688 }
689 let half_open = usize::try_from(span_s / cadence_s).unwrap_or(usize::MAX);
690 let inclusive = half_open.saturating_add(1);
691 match product.epoch_j2000_s.len() {
692 count if count == half_open => Ok(ExactSp3Coverage::HalfOpen),
693 count if count == inclusive => Ok(ExactSp3Coverage::Inclusive),
694 parsed => Err(ExactSp3ValidationError::SpanMismatch {
695 parsed,
696 half_open,
697 inclusive,
698 }),
699 }
700 .and_then(|coverage| {
701 validate_format_version(product.header.version, request.format_version.as_deref())?;
702 Ok(coverage)
703 })
704}
705
706fn validate_mandatory_structure(product: &Sp3) -> Result<(), ExactSp3ValidationError> {
707 if let Some(malformed) = product.terminal_record.first_malformed_record {
708 return Err(ExactSp3ValidationError::MalformedEofRecord {
709 line_number: malformed.line_number,
710 record_length: malformed.record_length,
711 });
712 }
713 if !product.terminal_record.had_valid_record {
714 return Err(ExactSp3ValidationError::MissingEof);
715 }
716 if product.terminal_record.had_trailing_content {
717 return Err(ExactSp3ValidationError::TrailingContentAfterEof);
718 }
719 if product.satellite_header_lines < 5 {
720 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
721 record: "+",
722 expected: 5,
723 actual: product.satellite_header_lines,
724 });
725 }
726 if product.accuracy_header_lines != product.satellite_header_lines {
727 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
728 record: "++",
729 expected: product.satellite_header_lines,
730 actual: product.accuracy_header_lines,
731 });
732 }
733 for (record, actual) in [
734 ("%c", product.time_system_header_lines),
735 ("%f", product.float_header_lines),
736 ("%i", product.integer_header_lines),
737 ] {
738 if actual != 2 {
739 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
740 record,
741 expected: 2,
742 actual,
743 });
744 }
745 }
746 if product.header_comment_lines < 4 {
747 return Err(ExactSp3ValidationError::MandatoryHeaderRecordCount {
748 record: "/*",
749 expected: 4,
750 actual: product.header_comment_lines,
751 });
752 }
753 let declared_count = product
754 .declared_satellite_count
755 .ok_or(ExactSp3ValidationError::MissingDeclaredSatelliteCount)?;
756 if declared_count != product.declared_satellite_tokens.len() {
757 return Err(ExactSp3ValidationError::DeclaredSatelliteCountMismatch {
758 declared: declared_count,
759 tokens: product.declared_satellite_tokens.len(),
760 });
761 }
762 for duplicate_index in 0..product.declared_satellite_tokens.len() {
763 if let Some(first_index) = product.declared_satellite_tokens[..duplicate_index]
764 .iter()
765 .position(|token| token == &product.declared_satellite_tokens[duplicate_index])
766 {
767 return Err(ExactSp3ValidationError::DuplicateDeclaredSatellite {
768 token: product.declared_satellite_tokens[duplicate_index].clone(),
769 first_index,
770 duplicate_index,
771 });
772 }
773 }
774 if product.header.satellites.is_empty() {
775 return Err(ExactSp3ValidationError::NoDeclaredSatellites);
776 }
777 for epoch_index in 0..product.epochs.len() {
778 let positions = product
779 .epoch_position_tokens
780 .get(epoch_index)
781 .cloned()
782 .unwrap_or_default();
783 if positions != product.declared_satellite_tokens {
784 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
785 record: "P",
786 epoch_index,
787 expected: product.declared_satellite_tokens.clone(),
788 actual: positions,
789 });
790 }
791 let velocities = product
792 .epoch_velocity_tokens
793 .get(epoch_index)
794 .cloned()
795 .unwrap_or_default();
796 let expected_velocities = match product.header.data_type {
797 Sp3DataType::Position => Vec::new(),
798 Sp3DataType::Velocity => product.declared_satellite_tokens.clone(),
799 };
800 if velocities != expected_velocities {
801 return Err(ExactSp3ValidationError::SatelliteRecordSequenceMismatch {
802 record: "V",
803 epoch_index,
804 expected: expected_velocities,
805 actual: velocities,
806 });
807 }
808 let mut expected_body = Vec::with_capacity(match product.header.data_type {
809 Sp3DataType::Position => product.declared_satellite_tokens.len(),
810 Sp3DataType::Velocity => product.declared_satellite_tokens.len() * 2,
811 });
812 for token in &product.declared_satellite_tokens {
813 expected_body.push(format!("P{token}"));
814 if matches!(product.header.data_type, Sp3DataType::Velocity) {
815 expected_body.push(format!("V{token}"));
816 }
817 }
818 let actual_body = product
819 .epoch_state_record_sequence
820 .get(epoch_index)
821 .map(|records| {
822 records
823 .iter()
824 .map(|(record, token)| format!("{record}{token}"))
825 .collect::<Vec<_>>()
826 })
827 .unwrap_or_default();
828 if actual_body != expected_body {
829 return Err(ExactSp3ValidationError::BodyRecordInterleavingMismatch {
830 epoch_index,
831 expected: expected_body,
832 actual: actual_body,
833 });
834 }
835 }
836 Ok(())
837}
838
839#[derive(Debug, Clone, Copy)]
840enum DurationField {
841 Span,
842 Sample,
843}
844
845fn parse_duration_token(token: &str, field: DurationField) -> Result<u64, ExactSp3ValidationError> {
846 let bytes = token.as_bytes();
847 let invalid = || match field {
848 DurationField::Span => ExactSp3ValidationError::UnsupportedSpanToken {
849 token: token.to_owned(),
850 },
851 DurationField::Sample => ExactSp3ValidationError::UnsupportedSampleToken {
852 token: token.to_owned(),
853 },
854 };
855 if bytes.len() != 3 || !bytes[0].is_ascii_digit() || !bytes[1].is_ascii_digit() {
856 return Err(invalid());
857 }
858 let amount = u64::from(bytes[0] - b'0') * 10 + u64::from(bytes[1] - b'0');
859 if amount == 0 {
860 return Err(invalid());
861 }
862 let unit_s = match bytes[2] {
863 b'S' => 1,
864 b'M' => 60,
865 b'H' => 3_600,
866 b'D' => 86_400,
867 b'W' if matches!(field, DurationField::Span) => 604_800,
868 _ => return Err(invalid()),
871 };
872 let canonical = match bytes[2] {
873 b'S' if amount % 60 == 0 => Some(format!("{:02}M", amount / 60)),
874 b'M' if amount % 60 == 0 => Some(format!("{:02}H", amount / 60)),
875 b'H' if amount % 24 == 0 => Some(format!("{:02}D", amount / 24)),
876 _ => None,
877 };
878 if let Some(canonical) = canonical {
879 return Err(match field {
880 DurationField::Span => ExactSp3ValidationError::NonCanonicalSpanToken {
881 token: token.to_owned(),
882 canonical,
883 },
884 DurationField::Sample => ExactSp3ValidationError::NonCanonicalSampleToken {
885 token: token.to_owned(),
886 canonical,
887 },
888 });
889 }
890 amount.checked_mul(unit_s).ok_or_else(invalid)
891}
892
893fn parse_issue(issue: Option<&str>) -> Result<(u8, u8), ExactSp3ValidationError> {
894 let Some(issue) = issue else {
895 return Ok((0, 0));
896 };
897 let bytes = issue.as_bytes();
898 if bytes.len() != 4 || !bytes.iter().all(u8::is_ascii_digit) {
899 return Err(ExactSp3ValidationError::InvalidIssue {
900 issue: issue.to_owned(),
901 });
902 }
903 let hour = (bytes[0] - b'0') * 10 + (bytes[1] - b'0');
904 let minute = (bytes[2] - b'0') * 10 + (bytes[3] - b'0');
905 if hour > 23 || minute > 59 {
906 return Err(ExactSp3ValidationError::InvalidIssue {
907 issue: issue.to_owned(),
908 });
909 }
910 Ok((hour, minute))
911}
912
913fn requested_start_j2000_s(request: &ExactSp3Request) -> f64 {
914 let (hour, minute) = parse_issue(request.issue.as_deref())
915 .expect("ExactSp3Request construction validates its issue token");
916 let filename_epoch_j2000_s = j2000_seconds(
917 request.date.year,
918 i32::from(request.date.month),
919 i32::from(request.date.day),
920 i32::from(hour),
921 i32::from(minute),
922 0.0,
923 );
924 filename_epoch_j2000_s + request.content_start_offset_s as f64
928}
929
930fn validate_line2_start_metadata(
937 product: &Sp3,
938 requested_start_j2000_s: f64,
939) -> Result<(), ExactSp3ValidationError> {
940 let requested_start_j2000_s = requested_start_j2000_s as i64;
943 let requested_mjd_total_s =
944 requested_start_j2000_s + J2000_MJD_DAY * SECONDS_PER_DAY_I64 + SECONDS_PER_DAY_I64 / 2;
945 let gps_zero_mjd_total_s = GPS_ZERO_MJD_DAY * SECONDS_PER_DAY_I64;
946 let since_gps_zero_s = requested_mjd_total_s - gps_zero_mjd_total_s;
947 if since_gps_zero_s < 0 {
948 return Err(ExactSp3ValidationError::RequestBeforeGpsEpoch);
949 }
950
951 let requested_week = since_gps_zero_s.div_euclid(SECONDS_PER_WEEK_I64);
952 let requested_sow_s = since_gps_zero_s.rem_euclid(SECONDS_PER_WEEK_I64) as f64;
953 if i64::from(product.header.gnss_week) != requested_week {
954 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
955 field: "gps_week",
956 requested: requested_week as f64,
957 actual: f64::from(product.header.gnss_week),
958 });
959 }
960
961 let header_sow_s = product.header.seconds_of_week;
962 if !header_sow_s.is_finite() {
963 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
964 field: "seconds_of_week",
965 });
966 }
967 if !(0.0..SECONDS_PER_WEEK_I64 as f64).contains(&header_sow_s) {
968 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
969 field: "seconds_of_week",
970 actual: header_sow_s,
971 });
972 }
973 if !seconds_match(header_sow_s, requested_sow_s) {
974 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
975 field: "seconds_of_week",
976 requested: requested_sow_s,
977 actual: header_sow_s,
978 });
979 }
980
981 let header_mjd_fraction = product.header.mjd_fraction;
982 if !header_mjd_fraction.is_finite() {
983 return Err(ExactSp3ValidationError::NonFiniteHeaderStartMetadata {
984 field: "mjd_fraction",
985 });
986 }
987 if !(0.0..1.0).contains(&header_mjd_fraction) {
988 return Err(ExactSp3ValidationError::InvalidHeaderStartMetadata {
989 field: "mjd_fraction",
990 actual: header_mjd_fraction,
991 });
992 }
993 let header_mjd_total_s =
994 i64::from(product.header.mjd) as f64 * 86_400.0 + header_mjd_fraction * 86_400.0;
995 if !seconds_match(header_mjd_total_s, requested_mjd_total_s as f64) {
996 return Err(ExactSp3ValidationError::HeaderStartMetadataMismatch {
997 field: "mjd",
998 requested: requested_mjd_total_s as f64 / 86_400.0,
999 actual: header_mjd_total_s / 86_400.0,
1000 });
1001 }
1002 Ok(())
1003}
1004
1005fn seconds_match(left: f64, right: f64) -> bool {
1006 (left - right).abs() <= WHOLE_SECOND_EPS_S
1007}
1008
1009fn validate_format_version(
1010 actual: Sp3Version,
1011 requested: Option<&str>,
1012) -> Result<(), ExactSp3ValidationError> {
1013 let Some(requested) = requested else {
1014 return Ok(());
1015 };
1016 let actual = match actual {
1017 Sp3Version::A => "SP3-a",
1018 Sp3Version::B => "SP3-b",
1019 Sp3Version::C => "SP3-c",
1020 Sp3Version::D => "SP3-d",
1021 };
1022 if requested.eq_ignore_ascii_case(actual) {
1023 Ok(())
1024 } else {
1025 Err(ExactSp3ValidationError::FormatVersionMismatch {
1026 requested: requested.to_owned(),
1027 actual: actual.to_owned(),
1028 })
1029 }
1030}