1use std::collections::HashMap;
52use std::fmt::Write as _;
53
54use crate::format::columns::{raw_field as field, raw_field_from as field_from};
55use crate::validate::{self, FieldError};
56use crate::{Error, Result};
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum CrinexVersion {
61 V1,
63 V3,
65}
66
67const MAX_ORDER: usize = 6;
71
72#[derive(Debug, Clone, PartialEq)]
88pub struct ObsStream {
89 pub version: CrinexVersion,
91 pub header: Vec<String>,
94 pub epochs: Vec<EpochRecord>,
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub enum EpochRecord {
101 Obs(ObsEpoch),
103 Event {
106 descriptor: String,
108 lines: Vec<String>,
110 },
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub struct ObsEpoch {
116 pub descriptor: String,
119 pub clock: Option<i64>,
122 pub sats: Vec<SatRecord>,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct SatRecord {
129 pub sv: String,
131 pub values: Vec<Option<i64>>,
133 pub flags: String,
135}
136
137const OBS_FIELD_WIDTH: usize = 16;
139const OBS_VALUE_WIDTH: usize = 14;
141
142pub fn decode(crinex_text: &str) -> Result<String> {
148 let mut out = String::with_capacity(crinex_text.len() * 4);
149 decode_to(crinex_text, |line| {
150 out.push_str(line);
151 out.push('\n');
152 })?;
153 Ok(out)
154}
155
156pub fn decode_to<W: FnMut(&str)>(crinex_text: &str, mut emit: W) -> Result<()> {
163 let mut decoder = Decoder::new();
164 let mut lines = crinex_text.lines();
165 decoder.read_crinex_header(&mut lines, &mut emit)?;
166 decoder.read_body(&mut lines, &mut emit)?;
167 Ok(())
168}
169
170pub fn encode_crinex(rinex_text: &str) -> Result<String> {
182 let stream = parse_rinex_obs(rinex_text)?;
183 Ok(encode_stream(&stream))
184}
185
186#[derive(Debug, Clone)]
194struct NumDiff {
195 m: usize,
197 level: usize,
199 buf: [i64; MAX_ORDER],
201}
202
203impl NumDiff {
204 fn new(data: i64, level: usize) -> Self {
207 let mut buf = [0i64; MAX_ORDER];
208 buf[0] = data;
209 Self { m: 0, level, buf }
210 }
211
212 fn force_init(&mut self, data: i64, level: usize) {
215 self.m = 0;
216 self.level = level;
217 self.rotate(data);
218 }
219
220 fn rotate(&mut self, data: i64) {
222 self.buf.copy_within(0..MAX_ORDER - 1, 1);
223 self.buf[0] = data;
224 }
225
226 fn decompress(&mut self, delta: i64) -> core::result::Result<i64, validate::ArithmeticError> {
229 let m = if self.m < self.level {
230 self.m + 1
231 } else {
232 self.m
233 };
234 let b = &self.buf;
235 let new = match m {
236 1 => checked_diff_sum(delta, &[(1, b[0])])?,
237 2 => checked_diff_sum(delta, &[(2, b[0]), (-1, b[1])])?,
238 3 => checked_diff_sum(delta, &[(3, b[0]), (-3, b[1]), (1, b[2])])?,
239 4 => checked_diff_sum(delta, &[(4, b[0]), (-6, b[1]), (4, b[2]), (-1, b[3])])?,
240 5 => checked_diff_sum(
241 delta,
242 &[(5, b[0]), (-10, b[1]), (10, b[2]), (-5, b[3]), (1, b[4])],
243 )?,
244 6 => checked_diff_sum(
245 delta,
246 &[
247 (6, b[0]),
248 (-15, b[1]),
249 (20, b[2]),
250 (-15, b[3]),
251 (6, b[4]),
252 (-1, b[5]),
253 ],
254 )?,
255 _ => checked_diff_sum(delta, &[(1, b[0])])?,
258 };
259 self.m = m;
260 self.rotate(new);
261 Ok(new)
262 }
263}
264
265fn checked_diff_sum(
266 delta: i64,
267 terms: &[(i64, i64)],
268) -> core::result::Result<i64, validate::ArithmeticError> {
269 const FIELD: &str = "crinex numeric difference";
270 let mut sum = delta;
271 for &(coefficient, value) in terms {
272 let term = validate::checked_i64_mul(coefficient.abs(), value, FIELD)?;
273 sum = if coefficient >= 0 {
274 validate::checked_i64_add(sum, term, FIELD)?
275 } else {
276 validate::checked_i64_sub(sum, term, FIELD)?
277 };
278 }
279 Ok(sum)
280}
281
282#[derive(Debug, Default, Clone)]
289struct TextDiff {
290 buffer: Vec<u8>,
291}
292
293impl TextDiff {
294 fn force_init(&mut self, data: &str) {
296 self.buffer = data.as_bytes().to_vec();
297 }
298
299 fn decompress(&mut self, data: &str) -> String {
302 let bytes = data.as_bytes();
303 if bytes.len() > self.buffer.len() {
304 self.buffer.extend_from_slice(&bytes[self.buffer.len()..]);
305 }
306 for (i, &byte) in bytes.iter().enumerate() {
307 if byte == b' ' {
308 continue;
309 }
310 if let Some(slot) = self.buffer.get_mut(i) {
311 *slot = if byte == b'&' { b' ' } else { byte };
312 }
313 }
314 String::from_utf8_lossy(&self.buffer).into_owned()
317 }
318}
319
320struct Decoder {
322 version: CrinexVersion,
323 obs_count: HashMap<char, usize>,
326 default_system: Option<char>,
329 epoch_diff: TextDiff,
331 clock_diff: Option<NumDiff>,
333 obs_diff: HashMap<String, Vec<Option<NumDiff>>>,
335 flag_diff: HashMap<String, TextDiff>,
337}
338
339impl Decoder {
340 fn new() -> Self {
341 Self {
342 version: CrinexVersion::V3,
343 obs_count: HashMap::new(),
344 default_system: None,
345 epoch_diff: TextDiff::default(),
346 clock_diff: None,
347 obs_diff: HashMap::new(),
348 flag_diff: HashMap::new(),
349 }
350 }
351
352 fn read_crinex_header<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
357 where
358 I: Iterator<Item = &'a str>,
359 W: FnMut(&str),
360 {
361 let l1 = lines
363 .next()
364 .ok_or_else(|| Error::Parse("CRINEX stream is empty".into()))?;
365 let crx_ver = field(l1, 0, 20).trim();
366 self.version = match crx_ver {
367 v if v.starts_with("1.0") || v.starts_with("1.") => CrinexVersion::V1,
368 v if v.starts_with("3.0") || v.starts_with("3.") => CrinexVersion::V3,
369 other => {
370 return Err(Error::Parse(format!(
371 "unsupported CRINEX version {other:?} (expected 1.0 or 3.0)"
372 )))
373 }
374 };
375 if !l1.contains("CRINEX VERS") {
376 return Err(Error::Parse(
377 "missing CRINEX VERS / TYPE header line".into(),
378 ));
379 }
380 lines
383 .next()
384 .ok_or_else(|| Error::Parse("CRINEX header missing PROG / DATE line".into()))?;
385
386 let mut saw_end = false;
388 for raw in lines.by_ref() {
389 let line = raw.trim_end_matches(['\r', '\n']);
390 emit(line);
391
392 let label = field(line, 60, 80).trim();
393 self.classify_header_label(line, label)?;
394 if label == "END OF HEADER" {
395 saw_end = true;
396 break;
397 }
398 }
399 if !saw_end {
400 return Err(Error::Parse(
401 "CRINEX embedded RINEX header has no END OF HEADER".into(),
402 ));
403 }
404 Ok(())
405 }
406
407 fn classify_header_label(&mut self, line: &str, label: &str) -> Result<()> {
412 match label {
413 "RINEX VERSION / TYPE" => {
414 let sys_field = field(line, 40, 41).trim();
417 if let Some(c) = sys_field.chars().next() {
418 if c != 'M' {
419 self.default_system = Some(c);
420 }
421 }
422 }
423 "# / TYPES OF OBSERV" => {
424 let n = strict_obs_count(line, 0, 6, "rinex2.obs_type_count")?;
426 if let Some(sys) = self.default_system {
427 self.obs_count.insert(sys, n);
428 }
429 self.obs_count.entry(' ').or_insert(n);
432 }
433 "SYS / # / OBS TYPES" => {
434 let sys_field = field(line, 0, 1).trim();
435 if let Some(c) = sys_field.chars().next() {
436 let n = strict_obs_count(line, 3, 6, "rinex3.obs_type_count")?;
437 self.obs_count.insert(c, n);
438 }
439 }
441 _ => {}
442 }
443 Ok(())
444 }
445
446 fn scan_rinex_header<'a, I>(&mut self, lines: &mut I, header: &mut Vec<String>) -> Result<()>
451 where
452 I: Iterator<Item = &'a str>,
453 {
454 let mut saw_version = false;
455 let mut saw_end = false;
456 for raw in lines.by_ref() {
457 let line = raw.trim_end_matches(['\r', '\n']);
458 let label = field(line, 60, 80).trim();
459 if label == "RINEX VERSION / TYPE" {
460 let version = field(line, 0, 9).trim();
461 self.version = match version.chars().next() {
462 Some('2') => CrinexVersion::V1,
463 Some('3') => CrinexVersion::V3,
464 _ => {
465 return Err(Error::Parse(format!(
466 "unsupported RINEX version {version:?} (expected 2 or 3)"
467 )))
468 }
469 };
470 saw_version = true;
471 }
472 self.classify_header_label(line, label)?;
473 header.push(line.to_string());
474 if label == "END OF HEADER" {
475 saw_end = true;
476 break;
477 }
478 }
479 if !saw_version {
480 return Err(Error::Parse(
481 "plain RINEX header missing RINEX VERSION / TYPE".into(),
482 ));
483 }
484 if !saw_end {
485 return Err(Error::Parse(
486 "plain RINEX observation header has no END OF HEADER".into(),
487 ));
488 }
489 Ok(())
490 }
491
492 fn read_body<'a, I, W>(&mut self, lines: &mut I, emit: &mut W) -> Result<()>
494 where
495 I: Iterator<Item = &'a str>,
496 W: FnMut(&str),
497 {
498 let version = self.version;
499 loop {
500 let record = match version {
501 CrinexVersion::V3 => self.next_epoch_v3(lines)?,
502 CrinexVersion::V1 => self.next_epoch_v1(lines)?,
503 };
504 let Some(record) = record else { break };
505 match version {
506 CrinexVersion::V3 => serialize_rinex_epoch_v3(&record, emit),
507 CrinexVersion::V1 => serialize_rinex_epoch_v1(&record, emit),
508 }
509 }
510 Ok(())
511 }
512
513 fn next_epoch_v3<'a, I>(&mut self, lines: &mut I) -> Result<Option<EpochRecord>>
519 where
520 I: Iterator<Item = &'a str>,
521 {
522 let raw = loop {
523 match lines.next() {
524 None => return Ok(None),
525 Some(raw) => {
526 let line = raw.trim_end_matches(['\r', '\n']);
527 if !line.is_empty() {
528 break line;
529 }
530 }
531 }
532 };
533
534 let descriptor = if raw.starts_with('>') {
541 self.epoch_diff.force_init(raw);
542 self.epoch_diff.decompress("")
543 } else {
544 self.epoch_diff.decompress(raw)
545 };
546
547 let numsat = strict_int_field::<usize>(&descriptor, 32, 35, "v3.epoch.satellite_count")?;
552 let flag = strict_int_field::<u8>(&descriptor, 31, 32, "v3.epoch.flag")?;
553
554 if flag > 1 {
558 let mut event_lines = Vec::with_capacity(numsat);
559 for _ in 0..numsat {
560 let extra = lines
561 .next()
562 .ok_or_else(|| Error::Parse("CRINEX V3 event record truncated".into()))?;
563 event_lines.push(extra.trim_end_matches(['\r', '\n']).to_string());
564 }
565 return Ok(Some(EpochRecord::Event {
566 descriptor,
567 lines: event_lines,
568 }));
569 }
570
571 let clock_line = lines
573 .next()
574 .ok_or_else(|| Error::Parse("CRINEX V3 epoch missing clock line".into()))?
575 .trim_end_matches(['\r', '\n']);
576 let clock = self.decode_clock_value(clock_line)?;
577
578 let sv_list = self.sv_tokens_v3(&descriptor, numsat)?;
579 let mut sats = Vec::with_capacity(sv_list.len());
580 for sv in &sv_list {
581 let data_line = lines.next().ok_or_else(|| {
582 Error::Parse("CRINEX V3 epoch truncated: missing satellite line".into())
583 })?;
584 let n_obs = self.obs_count_for(sv)?;
585 let (values, flags) =
586 self.decode_sat_values(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
587 sats.push(SatRecord {
588 sv: sv.clone(),
589 values,
590 flags,
591 });
592 }
593 Ok(Some(EpochRecord::Obs(ObsEpoch {
594 descriptor,
595 clock,
596 sats,
597 })))
598 }
599
600 fn sv_tokens_v3(&self, descriptor: &str, numsat: usize) -> Result<Vec<String>> {
602 let list = field_from(descriptor, 41);
606 let bytes = list.as_bytes();
607 let mut out = Vec::with_capacity(numsat);
608 for i in 0..numsat {
609 out.push(fixed_sv_token(bytes, "V3", numsat, i)?.to_string());
610 }
611 Ok(out)
612 }
613
614 fn obs_count_for(&self, sv: &str) -> Result<usize> {
616 let sys = sv.chars().next().unwrap_or(' ');
617 let count = self
618 .obs_count
619 .get(&sys)
620 .or_else(|| self.obs_count.get(&' '))
621 .copied()
622 .ok_or_else(|| {
623 Error::Parse(format!(
624 "CRINEX satellite {sv:?} has no declared observation count"
625 ))
626 })?;
627 if count == 0 {
628 return Err(Error::Parse(format!(
629 "CRINEX satellite {sv:?} has zero declared observations"
630 )));
631 }
632 Ok(count)
633 }
634
635 fn decode_sat_values(
641 &mut self,
642 sv: &str,
643 line: &str,
644 n_obs: usize,
645 ) -> Result<(Vec<Option<i64>>, String)> {
646 let engines = self
650 .obs_diff
651 .entry(sv.to_string())
652 .or_insert_with(|| vec![None; n_obs]);
653 if engines.len() < n_obs {
654 engines.resize(n_obs, None);
655 }
656
657 let mut values: Vec<Option<i64>> = Vec::with_capacity(n_obs);
658 let mut cursor = 0usize;
659 let bytes = line.as_bytes();
660
661 for obs_index in 0..n_obs {
662 if obs_index > 0 {
666 if cursor < bytes.len() && bytes[cursor] == b' ' {
667 cursor += 1;
668 } else if cursor >= bytes.len() {
669 values.push(None);
671 continue;
672 }
673 }
674 if cursor >= bytes.len() || bytes[cursor] == b' ' {
677 values.push(None);
678 continue;
679 }
680 let tok_start = cursor;
682 while cursor < bytes.len() && bytes[cursor] != b' ' {
683 cursor += 1;
684 }
685 let token = &line[tok_start..cursor];
686 let recovered = self.apply_obs_token(sv, obs_index, token)?;
687 values.push(Some(recovered));
688 }
689
690 let flag_raw = if cursor < bytes.len() {
693 let rest = &line[cursor..];
694 rest.strip_prefix(' ').unwrap_or(rest)
695 } else {
696 ""
697 };
698 let flags = self
699 .flag_diff
700 .entry(sv.to_string())
701 .or_default()
702 .decompress(flag_raw);
703
704 Ok((values, flags))
705 }
706
707 fn apply_obs_token(&mut self, sv: &str, obs_index: usize, token: &str) -> Result<i64> {
710 let engines = self.obs_diff.get_mut(sv).expect("engines inserted above");
711 let slot = &mut engines[obs_index];
712 if let Some((order, value)) = parse_reset(token)? {
713 match slot {
714 Some(e) => e.force_init(value, order),
715 None => *slot = Some(NumDiff::new(value, order)),
716 }
717 Ok(value)
718 } else {
719 let delta = token.trim().parse::<i64>().map_err(|_| {
720 Error::Parse(format!(
721 "CRINEX observation delta {token:?} is not an integer"
722 ))
723 })?;
724 let Some(engine) = slot else {
725 return Err(Error::Parse(format!(
726 "CRINEX observation {sv}[{obs_index}] has a delta before any arc init"
727 )));
728 };
729 engine.decompress(delta).map_err(map_arithmetic_error)
730 }
731 }
732
733 fn decode_clock_value(&mut self, line: &str) -> Result<Option<i64>> {
738 let token = line.trim();
739 if token.is_empty() {
740 return Ok(None);
741 }
742 let value = if let Some((order, v)) = parse_reset(token)? {
743 match &mut self.clock_diff {
744 Some(e) => e.force_init(v, order),
745 None => self.clock_diff = Some(NumDiff::new(v, order)),
746 }
747 v
748 } else {
749 let delta = token.parse::<i64>().map_err(|_| {
750 Error::Parse(format!("CRINEX clock delta {token:?} is not an integer"))
751 })?;
752 match &mut self.clock_diff {
753 Some(e) => e.decompress(delta).map_err(map_arithmetic_error)?,
754 None => {
755 return Err(Error::Parse(
756 "CRINEX clock delta before any clock arc init".into(),
757 ))
758 }
759 }
760 };
761 Ok(Some(value))
762 }
763
764 fn next_epoch_v1<'a, I>(&mut self, lines: &mut I) -> Result<Option<EpochRecord>>
769 where
770 I: Iterator<Item = &'a str>,
771 {
772 let raw = loop {
773 match lines.next() {
774 None => return Ok(None),
775 Some(raw) => {
776 let line = raw.trim_end_matches(['\r', '\n']);
777 if !line.is_empty() {
778 break line;
779 }
780 }
781 }
782 };
783
784 let descriptor = if let Some(stripped) = raw.strip_prefix('&') {
791 self.epoch_diff.force_init(&format!(" {stripped}"));
792 self.epoch_diff.decompress("")
793 } else {
794 self.epoch_diff.decompress(raw)
795 };
796
797 let numsat = strict_int_field::<usize>(&descriptor, 29, 32, "v1.epoch.satellite_count")?;
801 let flag = strict_int_field::<u8>(&descriptor, 26, 29, "v1.epoch.flag")?;
802
803 if flag > 1 {
805 let mut event_lines = Vec::with_capacity(numsat);
806 for _ in 0..numsat {
807 let extra = lines
808 .next()
809 .ok_or_else(|| Error::Parse("CRINEX V1 event record truncated".into()))?;
810 event_lines.push(extra.trim_end_matches(['\r', '\n']).to_string());
811 }
812 return Ok(Some(EpochRecord::Event {
813 descriptor,
814 lines: event_lines,
815 }));
816 }
817
818 let clock_line = lines
820 .next()
821 .ok_or_else(|| Error::Parse("CRINEX V1 epoch missing clock line".into()))?
822 .trim_end_matches(['\r', '\n']);
823 let clock = self.decode_clock_value(clock_line)?;
824
825 let sv_list = self.sv_tokens_v1(&descriptor, numsat)?;
826 let mut sats = Vec::with_capacity(sv_list.len());
827 for sv in &sv_list {
828 let data_line = lines.next().ok_or_else(|| {
829 Error::Parse("CRINEX V1 epoch truncated: missing satellite line".into())
830 })?;
831 let n_obs = self.obs_count_for(sv)?;
832 let (values, flags) =
833 self.decode_sat_values(sv, data_line.trim_end_matches(['\r', '\n']), n_obs)?;
834 sats.push(SatRecord {
835 sv: sv.clone(),
836 values,
837 flags,
838 });
839 }
840 Ok(Some(EpochRecord::Obs(ObsEpoch {
841 descriptor,
842 clock,
843 sats,
844 })))
845 }
846
847 fn sv_tokens_v1(&self, descriptor: &str, numsat: usize) -> Result<Vec<String>> {
848 let list = field_from(descriptor, 32);
851 let bytes = list.as_bytes();
852 let mut out = Vec::with_capacity(numsat);
853 for i in 0..numsat {
854 let mut tok = fixed_sv_token(bytes, "V1", numsat, i)?.to_string();
855 if tok.starts_with(' ') {
856 if let Some(sys) = self.default_system {
857 let prn = tok.trim();
858 tok = format!("{sys}{prn:>2}");
859 }
860 }
861 out.push(tok);
862 }
863 Ok(out)
864 }
865
866 fn parse_rinex_epochs_v3<'a, I>(&self, lines: &mut I) -> Result<Vec<EpochRecord>>
871 where
872 I: Iterator<Item = &'a str>,
873 {
874 let mut epochs = Vec::new();
875 loop {
876 let Some(line) = next_nonblank(lines) else {
877 return Ok(epochs);
878 };
879 if !line.starts_with('>') {
880 return Err(Error::Parse(format!(
881 "RINEX-3 epoch line must start with '>': {line:?}"
882 )));
883 }
884 let flag = strict_int_field::<u8>(&line, 31, 32, "v3.epoch.flag")?;
885 let numsat = strict_int_field::<usize>(&line, 32, 35, "v3.epoch.satellite_count")?;
886
887 if flag > 1 {
888 let event_lines = read_event_lines(lines, numsat, "RINEX-3")?;
889 epochs.push(EpochRecord::Event {
890 descriptor: line,
891 lines: event_lines,
892 });
893 continue;
894 }
895
896 let clock = parse_clock_field(&line, 41, 56, 12, "v3.epoch.clock")?;
897 let mut sats = Vec::with_capacity(numsat);
898 let mut sv_tokens = Vec::with_capacity(numsat);
899 for _ in 0..numsat {
900 let raw = lines.next().ok_or_else(|| {
901 Error::Parse("RINEX-3 epoch truncated: missing satellite line".into())
902 })?;
903 let sat_line = raw.trim_end_matches(['\r', '\n']);
904 let sv = field(sat_line, 0, 3).to_string();
905 let n_obs = self.obs_count_for(&sv)?;
906 let (values, flags) = parse_sat_obs_v3(sat_line, n_obs)?;
907 sv_tokens.push(sv.clone());
908 sats.push(SatRecord { sv, values, flags });
909 }
910 let descriptor = build_descriptor_v3(&line, &sv_tokens);
911 epochs.push(EpochRecord::Obs(ObsEpoch {
912 descriptor,
913 clock,
914 sats,
915 }));
916 }
917 }
918
919 fn parse_rinex_epochs_v1<'a, I>(&self, lines: &mut I) -> Result<Vec<EpochRecord>>
923 where
924 I: Iterator<Item = &'a str>,
925 {
926 let mut epochs = Vec::new();
927 loop {
928 let Some(first) = next_nonblank(lines) else {
929 return Ok(epochs);
930 };
931 let flag = strict_int_field::<u8>(&first, 26, 29, "v1.epoch.flag")?;
932 let numsat = strict_int_field::<usize>(&first, 29, 32, "v1.epoch.satellite_count")?;
933
934 if flag > 1 {
935 let event_lines = read_event_lines(lines, numsat, "RINEX-2")?;
936 epochs.push(EpochRecord::Event {
937 descriptor: first,
938 lines: event_lines,
939 });
940 continue;
941 }
942
943 let clock = parse_clock_field(&first, 68, 80, 9, "v1.epoch.clock")?;
944
945 let mut sv_tokens: Vec<String> = Vec::with_capacity(numsat);
948 collect_sv_tokens_v1(&first, numsat.min(12), &mut sv_tokens);
949 while sv_tokens.len() < numsat {
950 let raw = lines.next().ok_or_else(|| {
951 Error::Parse("RINEX-2 epoch SV continuation truncated".into())
952 })?;
953 let cont = raw.trim_end_matches(['\r', '\n']);
954 let need = (numsat - sv_tokens.len()).min(12);
955 collect_sv_tokens_v1(cont, need, &mut sv_tokens);
956 }
957 let sv_tokens: Vec<String> = sv_tokens
958 .into_iter()
959 .map(|tok| self.normalize_v1_sv(tok))
960 .collect();
961
962 let mut sats = Vec::with_capacity(numsat);
963 for sv in &sv_tokens {
964 let n_obs = self.obs_count_for(sv)?;
965 let row_count = n_obs.div_ceil(5);
966 let mut obs_lines = Vec::with_capacity(row_count);
967 for _ in 0..row_count {
968 let raw = lines.next().ok_or_else(|| {
969 Error::Parse("RINEX-2 epoch truncated: missing observation line".into())
970 })?;
971 obs_lines.push(raw.trim_end_matches(['\r', '\n']).to_string());
972 }
973 let (values, flags) = parse_sat_obs_v1(&obs_lines, n_obs)?;
974 sats.push(SatRecord {
975 sv: sv.clone(),
976 values,
977 flags,
978 });
979 }
980 let descriptor = build_descriptor_v1(&first, &sv_tokens);
981 epochs.push(EpochRecord::Obs(ObsEpoch {
982 descriptor,
983 clock,
984 sats,
985 }));
986 }
987 }
988
989 fn normalize_v1_sv(&self, token: String) -> String {
992 if token.starts_with(' ') {
993 if let Some(sys) = self.default_system {
994 let prn = token.trim();
995 return format!("{sys}{prn:>2}");
996 }
997 }
998 token
999 }
1000}
1001
1002fn parse_rinex_obs(rinex_text: &str) -> Result<ObsStream> {
1009 let mut decoder = Decoder::new();
1010 let mut header: Vec<String> = Vec::new();
1011 let mut lines = rinex_text.lines();
1012 decoder.scan_rinex_header(&mut lines, &mut header)?;
1013
1014 let version = decoder.version;
1015 let epochs = match version {
1016 CrinexVersion::V3 => decoder.parse_rinex_epochs_v3(&mut lines)?,
1017 CrinexVersion::V1 => decoder.parse_rinex_epochs_v1(&mut lines)?,
1018 };
1019 Ok(ObsStream {
1020 version,
1021 header,
1022 epochs,
1023 })
1024}
1025
1026fn next_nonblank<'a, I>(lines: &mut I) -> Option<String>
1029where
1030 I: Iterator<Item = &'a str>,
1031{
1032 for raw in lines.by_ref() {
1033 let line = raw.trim_end_matches(['\r', '\n']);
1034 if !line.is_empty() {
1035 return Some(line.to_string());
1036 }
1037 }
1038 None
1039}
1040
1041fn read_event_lines<'a, I>(lines: &mut I, count: usize, revision: &str) -> Result<Vec<String>>
1043where
1044 I: Iterator<Item = &'a str>,
1045{
1046 let mut out = Vec::with_capacity(count);
1047 for _ in 0..count {
1048 let raw = lines
1049 .next()
1050 .ok_or_else(|| Error::Parse(format!("{revision} event record truncated")))?;
1051 out.push(raw.trim_end_matches(['\r', '\n']).to_string());
1052 }
1053 Ok(out)
1054}
1055
1056fn parse_clock_field(
1059 line: &str,
1060 start: usize,
1061 end: usize,
1062 decimals: usize,
1063 field_name: &'static str,
1064) -> Result<Option<i64>> {
1065 let text = field(line, start, end);
1066 if text.trim().is_empty() {
1067 Ok(None)
1068 } else {
1069 Ok(Some(parse_scaled_decimal(text, decimals, field_name)?))
1070 }
1071}
1072
1073fn parse_sat_obs_v3(line: &str, n_obs: usize) -> Result<(Vec<Option<i64>>, String)> {
1077 let mut values = Vec::with_capacity(n_obs);
1078 let mut flags = String::with_capacity(n_obs * 2);
1079 for i in 0..n_obs {
1080 let base = 3 + i * OBS_FIELD_WIDTH;
1081 read_obs_field(line, base, &mut values, &mut flags)?;
1082 }
1083 Ok((values, flags))
1084}
1085
1086fn parse_sat_obs_v1(obs_lines: &[String], n_obs: usize) -> Result<(Vec<Option<i64>>, String)> {
1089 let mut values = Vec::with_capacity(n_obs);
1090 let mut flags = String::with_capacity(n_obs * 2);
1091 for i in 0..n_obs {
1092 let line = obs_lines.get(i / 5).map_or("", String::as_str);
1093 let base = (i % 5) * OBS_FIELD_WIDTH;
1094 read_obs_field(line, base, &mut values, &mut flags)?;
1095 }
1096 Ok((values, flags))
1097}
1098
1099fn read_obs_field(
1102 line: &str,
1103 base: usize,
1104 values: &mut Vec<Option<i64>>,
1105 flags: &mut String,
1106) -> Result<()> {
1107 let value_text = field(line, base, base + OBS_VALUE_WIDTH);
1108 if value_text.trim().is_empty() {
1109 values.push(None);
1110 flags.push(' ');
1111 flags.push(' ');
1112 } else {
1113 values.push(Some(parse_scaled_decimal(value_text, 3, "observation")?));
1114 flags.push(char_at_or_space(line, base + OBS_VALUE_WIDTH));
1115 flags.push(char_at_or_space(line, base + OBS_VALUE_WIDTH + 1));
1116 }
1117 Ok(())
1118}
1119
1120fn parse_scaled_decimal(text: &str, decimals: usize, field_name: &'static str) -> Result<i64> {
1125 let trimmed = text.trim();
1126 let (negative, body) = trimmed.strip_prefix('-').map_or_else(
1127 || (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
1128 |rest| (true, rest),
1129 );
1130 let (integer_part, fraction_part) = match body.split_once('.') {
1131 Some((integer, fraction)) => (integer, fraction),
1132 None => (body, ""),
1133 };
1134 let integer_text = if integer_part.is_empty() {
1135 "0"
1136 } else {
1137 integer_part
1138 };
1139 let mut fraction = String::with_capacity(decimals);
1141 fraction.extend(fraction_part.chars().take(decimals));
1142 while fraction.len() < decimals {
1143 fraction.push('0');
1144 }
1145 let scale = 10i64.pow(decimals as u32);
1146 let integer_value = parse_scaled_component(integer_text, text, field_name)?;
1147 let fraction_value = if decimals == 0 {
1148 0
1149 } else {
1150 parse_scaled_component(&fraction, text, field_name)?
1151 };
1152 let magnitude = validate::checked_i64_mul(integer_value, scale, field_name)
1153 .and_then(|scaled| validate::checked_i64_add(scaled, fraction_value, field_name))
1154 .map_err(map_arithmetic_error)?;
1155 Ok(if negative { -magnitude } else { magnitude })
1156}
1157
1158fn parse_scaled_component(token: &str, text: &str, field_name: &'static str) -> Result<i64> {
1160 token
1161 .parse::<i64>()
1162 .map_err(|_| Error::Parse(format!("CRINEX invalid {field_name}: {text:?}")))
1163}
1164
1165fn build_descriptor_v3(epoch_line: &str, sv_tokens: &[String]) -> String {
1170 let mut descriptor = pad_to(field(epoch_line, 0, 35), 41);
1171 for token in sv_tokens {
1172 descriptor.push_str(token);
1173 }
1174 descriptor
1175}
1176
1177fn build_descriptor_v1(epoch_line: &str, sv_tokens: &[String]) -> String {
1181 let mut descriptor = pad_to(field(epoch_line, 0, 32), 32);
1182 for token in sv_tokens {
1183 descriptor.push_str(token);
1184 }
1185 descriptor
1186}
1187
1188fn collect_sv_tokens_v1(line: &str, count: usize, out: &mut Vec<String>) {
1190 for i in 0..count {
1191 let start = 32 + i * 3;
1192 out.push(field(line, start, start + 3).to_string());
1193 }
1194}
1195
1196fn char_at_or_space(line: &str, index: usize) -> char {
1199 line.as_bytes().get(index).map_or(' ', |&byte| byte as char)
1200}
1201
1202fn pad_to(text: &str, width: usize) -> String {
1204 let mut out = text.to_string();
1205 while out.len() < width {
1206 out.push(' ');
1207 }
1208 out
1209}
1210
1211pub fn parse_stream(crinex_text: &str) -> Result<ObsStream> {
1218 let mut decoder = Decoder::new();
1219 let mut lines = crinex_text.lines();
1220 let mut header: Vec<String> = Vec::new();
1221 decoder.read_crinex_header(&mut lines, &mut |line: &str| header.push(line.to_string()))?;
1222
1223 let version = decoder.version;
1224 let mut epochs = Vec::new();
1225 loop {
1226 let record = match version {
1227 CrinexVersion::V3 => decoder.next_epoch_v3(&mut lines)?,
1228 CrinexVersion::V1 => decoder.next_epoch_v1(&mut lines)?,
1229 };
1230 match record {
1231 Some(record) => epochs.push(record),
1232 None => break,
1233 }
1234 }
1235 Ok(ObsStream {
1236 version,
1237 header,
1238 epochs,
1239 })
1240}
1241
1242pub fn encode_stream(stream: &ObsStream) -> String {
1255 let mut out = String::new();
1256 let version_label = match stream.version {
1257 CrinexVersion::V3 => "3.0",
1258 CrinexVersion::V1 => "1.0",
1259 };
1260 push_crinex_line(
1261 &mut out,
1262 &labeled_crinex(version_label, "CRINEX VERS / TYPE"),
1263 );
1264 push_crinex_line(
1265 &mut out,
1266 &labeled_crinex("sidereon", "CRINEX PROG / DATE"),
1267 );
1268 for header_line in &stream.header {
1269 push_crinex_line(&mut out, header_line);
1270 }
1271
1272 let mut flag_state: HashMap<String, String> = HashMap::new();
1273 for epoch in &stream.epochs {
1274 encode_epoch(epoch, stream.version, &mut flag_state, &mut out);
1275 }
1276 out
1277}
1278
1279fn encode_epoch(
1281 epoch: &EpochRecord,
1282 version: CrinexVersion,
1283 flag_state: &mut HashMap<String, String>,
1284 out: &mut String,
1285) {
1286 match epoch {
1287 EpochRecord::Event { descriptor, lines } => {
1288 encode_descriptor(descriptor, version, out);
1289 for line in lines {
1290 push_crinex_line(out, line);
1291 }
1292 }
1293 EpochRecord::Obs(ObsEpoch {
1294 descriptor,
1295 clock,
1296 sats,
1297 }) => {
1298 encode_descriptor(descriptor, version, out);
1299 match clock {
1301 Some(value) => push_crinex_line(out, &format!("1&{value}")),
1302 None => push_crinex_line(out, ""),
1303 }
1304 for sat in sats {
1305 let previous = flag_state.entry(sat.sv.clone()).or_default();
1306 let delta = text_diff_delta(previous.as_str(), &sat.flags);
1307 previous.clone_from(&sat.flags);
1308 push_crinex_line(out, &encode_sat_line(&sat.values, &delta));
1309 }
1310 }
1311 }
1312}
1313
1314fn encode_descriptor(descriptor: &str, version: CrinexVersion, out: &mut String) {
1316 match version {
1317 CrinexVersion::V3 => push_crinex_line(out, descriptor),
1320 CrinexVersion::V1 => push_crinex_line(out, &format!("&{}", &descriptor[1..])),
1323 }
1324}
1325
1326fn encode_sat_line(values: &[Option<i64>], flag_delta: &str) -> String {
1329 let mut line = String::new();
1330 for (index, value) in values.iter().enumerate() {
1331 if index > 0 {
1332 line.push(' ');
1333 }
1334 if let Some(value) = value {
1335 let _ = write!(line, "1&{value}");
1336 }
1337 }
1338 line.push(' ');
1340 line.push_str(flag_delta);
1341 line
1342}
1343
1344fn text_diff_delta(previous: &str, current: &str) -> String {
1350 let prev = previous.as_bytes();
1351 let curr = current.as_bytes();
1352 let mut delta = Vec::with_capacity(curr.len());
1353 for (index, &byte) in curr.iter().enumerate() {
1354 let out = match prev.get(index) {
1355 Some(&previous_byte) if byte == previous_byte => b' ',
1356 Some(_) if byte == b' ' => b'&',
1357 _ => byte,
1360 };
1361 delta.push(out);
1362 }
1363 String::from_utf8(delta).unwrap_or_default()
1365}
1366
1367fn push_crinex_line(out: &mut String, line: &str) {
1369 out.push_str(line);
1370 out.push('\n');
1371}
1372
1373fn labeled_crinex(body: &str, label: &str) -> String {
1375 format!("{body:<60}{label}")
1376}
1377
1378fn serialize_rinex_epoch_v3<W: FnMut(&str)>(record: &EpochRecord, emit: &mut W) {
1380 match record {
1381 EpochRecord::Event { descriptor, lines } => {
1382 emit(trim_end(field(descriptor, 0, 35)));
1383 for line in lines {
1384 emit(line);
1385 }
1386 }
1387 EpochRecord::Obs(ObsEpoch {
1388 descriptor,
1389 clock,
1390 sats,
1391 }) => {
1392 let clock_text = format_clock_v3(*clock);
1393 let head = field(descriptor, 0, 35);
1398 let mut epoch_out = head.to_string();
1399 if !clock_text.is_empty() {
1400 while epoch_out.len() < 41 {
1401 epoch_out.push(' ');
1402 }
1403 }
1404 epoch_out.push_str(&clock_text);
1405 emit(trim_end(&epoch_out));
1406 for sat in sats {
1407 let out = format_sat_line(&sat.sv, &sat.values, &sat.flags);
1408 emit(trim_end(&out));
1409 }
1410 }
1411 }
1412}
1413
1414fn serialize_rinex_epoch_v1<W: FnMut(&str)>(record: &EpochRecord, emit: &mut W) {
1416 match record {
1417 EpochRecord::Event { descriptor, lines } => {
1418 emit(trim_end(field(descriptor, 0, 32)));
1419 for line in lines {
1420 emit(line);
1421 }
1422 }
1423 EpochRecord::Obs(ObsEpoch {
1424 descriptor,
1425 clock,
1426 sats,
1427 }) => {
1428 let clock_text = format_clock_v1(*clock);
1429 let sv_list: Vec<String> = sats.iter().map(|sat| sat.sv.clone()).collect();
1431 for line in &format_epoch_v1(descriptor, &sv_list, &clock_text) {
1432 emit(trim_end(line));
1433 }
1434 for sat in sats {
1435 for line in format_sat_lines_v1(&sat.values, &sat.flags) {
1436 emit(trim_end(&line));
1437 }
1438 }
1439 }
1440 }
1441}
1442
1443fn format_clock_v3(clock: Option<i64>) -> String {
1446 match clock {
1447 Some(value) => format!("{:15.12}", value as f64 / 1.0e12),
1448 None => String::new(),
1449 }
1450}
1451
1452fn format_clock_v1(clock: Option<i64>) -> String {
1455 match clock {
1456 Some(value) => format!("{:12.9}", value as f64 / 1.0e9),
1457 None => String::new(),
1458 }
1459}
1460
1461fn strict_obs_count(
1462 line: &str,
1463 start: usize,
1464 end: usize,
1465 field_name: &'static str,
1466) -> Result<usize> {
1467 let count = strict_int_field::<usize>(line, start, end, field_name)?;
1468 if count == 0 {
1469 return Err(Error::Parse(format!(
1470 "CRINEX invalid {field_name}: observation count must be positive in {line:?}"
1471 )));
1472 }
1473 Ok(count)
1474}
1475
1476fn strict_int_field<T>(line: &str, start: usize, end: usize, field_name: &'static str) -> Result<T>
1477where
1478 T: core::str::FromStr,
1479{
1480 strict_int_token(field(line, start, end), field_name, line)
1481}
1482
1483fn strict_int_token<T>(token: &str, field_name: &'static str, line: &str) -> Result<T>
1484where
1485 T: core::str::FromStr,
1486{
1487 validate::strict_int::<T>(token, field_name).map_err(|error| map_field_error(error, line))
1488}
1489
1490fn fixed_sv_token<'a>(
1491 sv_list: &'a [u8],
1492 crinex_version: &str,
1493 numsat: usize,
1494 index: usize,
1495) -> Result<&'a str> {
1496 let start = index * 3;
1497 let end = start + 3;
1498 if end > sv_list.len() {
1499 return Err(Error::Parse(format!(
1500 "CRINEX {crinex_version} epoch SV list shorter than {numsat} satellites"
1501 )));
1502 }
1503 let token = &sv_list[start..end];
1504 if !token.is_ascii() {
1505 return Err(Error::Parse(format!(
1506 "CRINEX {crinex_version} epoch SV token {} contains non-ASCII bytes",
1507 index + 1
1508 )));
1509 }
1510 std::str::from_utf8(token).map_err(|_| {
1511 Error::Parse(format!(
1512 "CRINEX {crinex_version} epoch SV token {} is not valid UTF-8",
1513 index + 1
1514 ))
1515 })
1516}
1517
1518fn map_field_error(error: FieldError, line: &str) -> Error {
1519 Error::Parse(format!(
1520 "CRINEX invalid {}: {error} in {line:?}",
1521 error.field()
1522 ))
1523}
1524
1525fn map_arithmetic_error(error: validate::ArithmeticError) -> Error {
1526 Error::Parse(format!("CRINEX {error}"))
1527}
1528
1529fn parse_reset(token: &str) -> Result<Option<(usize, i64)>> {
1533 let token = token.trim();
1534 if let Some(amp) = token.find('&') {
1535 let order = token[..amp]
1536 .parse::<usize>()
1537 .map_err(|_| Error::Parse(format!("CRINEX reset order in {token:?} invalid")))?;
1538 if order == 0 || order > MAX_ORDER {
1539 return Err(Error::Parse(format!(
1540 "CRINEX reset order {order} out of range 1..={MAX_ORDER}"
1541 )));
1542 }
1543 let value = token[amp + 1..]
1544 .parse::<i64>()
1545 .map_err(|_| Error::Parse(format!("CRINEX reset value in {token:?} invalid")))?;
1546 Ok(Some((order, value)))
1547 } else {
1548 Ok(None)
1549 }
1550}
1551
1552fn format_sat_line(sv: &str, values: &[Option<i64>], flags: &str) -> String {
1556 let mut out = String::with_capacity(3 + values.len() * OBS_FIELD_WIDTH);
1557 out.push_str(sv);
1558 let flag_bytes = flags.as_bytes();
1559 for (i, value) in values.iter().enumerate() {
1560 match value {
1561 Some(v) => out.push_str(&format_value(*v)),
1562 None => {
1563 for _ in 0..OBS_VALUE_WIDTH {
1564 out.push(' ');
1565 }
1566 }
1567 }
1568 let lli = flag_bytes.get(i * 2).copied().unwrap_or(b' ');
1570 let ssi = flag_bytes.get(i * 2 + 1).copied().unwrap_or(b' ');
1571 if value.is_some() {
1572 out.push(lli as char);
1573 out.push(ssi as char);
1574 } else {
1575 out.push(' ');
1576 out.push(' ');
1577 }
1578 }
1579 out
1580}
1581
1582fn format_value(scaled: i64) -> String {
1590 let negative = scaled < 0;
1591 let magnitude = scaled.unsigned_abs();
1592 let whole = magnitude / 1000;
1593 let frac = magnitude % 1000;
1594 let body = if negative && whole == 0 {
1595 format!("-.{frac:03}")
1596 } else {
1597 format!("{}{}.{:03}", if negative { "-" } else { "" }, whole, frac)
1598 };
1599 format!("{body:>14}")
1600}
1601
1602fn format_epoch_v1(descriptor: &str, sv_list: &[String], clock_text: &str) -> Vec<String> {
1605 let head = field(descriptor, 0, 32).to_string();
1608 let mut lines = Vec::new();
1609 let mut first = head;
1610 for sv in sv_list.iter().take(12) {
1611 first.push_str(sv);
1612 }
1613 if !clock_text.is_empty() {
1614 while first.len() < 68 {
1618 first.push(' ');
1619 }
1620 first.push_str(clock_text);
1621 }
1622 lines.push(first);
1623 let mut idx = 12;
1624 while idx < sv_list.len() {
1625 let chunk = sv_list[idx..(idx + 12).min(sv_list.len())].join("");
1626 lines.push(format!("{:32}{chunk}", ""));
1627 idx += 12;
1628 }
1629 lines
1630}
1631
1632fn format_sat_lines_v1(values: &[Option<i64>], flags: &str) -> Vec<String> {
1635 let flag_bytes = flags.as_bytes();
1636 let mut lines = Vec::new();
1637 let mut line = String::new();
1638 for (i, value) in values.iter().enumerate() {
1639 if i > 0 && i % 5 == 0 {
1640 lines.push(std::mem::take(&mut line));
1641 }
1642 match value {
1643 Some(v) => line.push_str(&format_value(*v)),
1644 None => {
1645 for _ in 0..OBS_VALUE_WIDTH {
1646 line.push(' ');
1647 }
1648 }
1649 }
1650 let lli = flag_bytes.get(i * 2).copied().unwrap_or(b' ');
1651 let ssi = flag_bytes.get(i * 2 + 1).copied().unwrap_or(b' ');
1652 if value.is_some() {
1653 line.push(lli as char);
1654 line.push(ssi as char);
1655 } else {
1656 line.push(' ');
1657 line.push(' ');
1658 }
1659 }
1660 lines.push(line);
1661 lines
1662}
1663
1664fn trim_end(line: &str) -> &str {
1667 line.trim_end_matches(' ')
1668}
1669
1670#[cfg(all(test, sidereon_repo_tests))]
1671mod tests;