apsis-rs/crates/orbit/apsis-tle/src/checksum.rs

77 lines
2.9 KiB
Rust

//! Line checksums for the two-line element format.
//!
//! Each line ends with a single digit that is the checksum of the preceding
//! columns. The checksum is the sum of the decimal digits on the line, where
//! a minus sign contributes one and every other character contributes
//! nothing, reduced modulo ten.
//!
//! The checksum digit itself is the last character of the line and is not
//! part of the sum.
use crate::field::{ElementError, ElementErrorKind, Result};
/// Returns the checksum of the first 68 columns of a line.
///
/// The input may include the trailing checksum digit, which is ignored
/// because only the first 68 columns contribute to the sum. A line shorter
/// than 68 characters contributes only the columns it has.
///
/// A minus sign contributes one, a decimal digit contributes its value, and
/// every other character contributes nothing.
pub fn line_checksum(line: &str) -> u32 {
line.chars()
.take(68)
.map(|character| match character {
'-' => 1,
'0'..='9' => character as u32 - '0' as u32,
_ => 0,
})
.sum::<u32>()
% 10
}
/// Verifies that the last character of a line is its checksum.
///
/// The line must be at least 69 characters long, because the checksum
/// occupies column 69. The expected value is compared against
/// [`line_checksum`] of the same line.
///
/// `line_number` is the one based number used in any error that is
/// returned, so that a caller parsing a multi-line input can report which
/// line failed.
pub fn verify_line_checksum(line: &str, line_number: usize) -> Result<()> {
let checksum_columns = (69, 69);
// The line must contain at least 69 characters. Counting characters
// rather than bytes is deliberate: the format is ASCII, but a caller
// may hand in text with a non-ASCII character by mistake, and counting
// characters gives a clearer failure than slicing through one.
let characters: Vec<char> = line.chars().collect();
if characters.len() < 69 {
return Err(ElementError::new(
ElementErrorKind::TooShort,
line_number,
Some(checksum_columns),
)
.with_detail(format!("found {} characters, need 69", characters.len())));
}
let expected = characters[68];
let Some(expected) = expected.to_digit(10) else {
return Err(ElementError::new(
ElementErrorKind::InvalidField,
line_number,
Some(checksum_columns),
)
.with_detail(format!("checksum character is {expected:?}, not a digit")));
};
let actual = line_checksum(line);
if actual != expected {
return Err(ElementError::new(
ElementErrorKind::Checksum,
line_number,
Some(checksum_columns),
)
.with_detail(format!("computed {actual}, line states {expected}")));
}
Ok(())
}