1#![forbid(unsafe_code)]
2
3const DECIMAL_RADIX: u32 = 10;
4
5#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
9pub enum Line {
10 Line1,
11 Line2,
12}
13
14#[non_exhaustive]
16#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
17pub enum Error {
18 InvalidLineSize(Line, usize),
22 Space(Line, char, usize),
26 SatlliteCatalogNumber(Line),
28 Classification(char),
36 InternationalDesignatorLaunchYear,
38 InternationalDesignatorLaunchNumber,
40 EpochYear,
42 EpochDay,
44 FirstDerivative,
46 SecondDerivative,
48 BStar,
50 EphemerisType(char),
52 ElementSetNumber,
54 Inclination,
56 RightAscension,
58 Eccentricty,
60 ArgumentOfPerigee,
62 MeanAnomaly,
64 MeanMotion,
66 RevolutionNumber,
68 Checksum(Line, char),
72 InvalidChecksum(Line, u8, u8),
78 LineNumber(Line, char),
83 SatelliteCatalogNumberMismatch(u32, u32),
89 ContainsNonAsciiCharacter(Line),
91}
92
93#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
94pub struct InternationalDesignator {
95 pub launch_year: u8,
96 pub launch_num: u16,
97 pub launch_piece: [char; 3],
98}
99
100#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
101pub enum Classification {
102 Unclassified,
103 Classified,
104 Secret,
105}
106
107#[derive(Debug, Clone, PartialEq, PartialOrd)]
111pub struct Tle {
112 pub satellite_catalog_number: u32,
113 pub classification: Classification,
114 pub international_designator: InternationalDesignator,
115 pub epoch_year: u8,
116 pub epoch_day_and_fractional_part: f64,
117 pub first_derivative_of_mean_motion: f32,
118 pub second_derivative_of_mean_motion: f32,
119 pub b_star: f32,
120 pub element_set_number: u16,
121 checksum_1: u8,
122 pub inclination: f32,
123 pub right_ascension_of_ascending_node: f32,
124 pub eccentricity: f32,
125 pub argument_of_perigee: f32,
126 pub mean_anomaly: f32,
127 pub mean_motion: f32,
128 pub revolution_number_at_epoch: u32,
129 checksum_2: u8,
130}
131
132macro_rules! split_space {
133 ($line_num:expr, $line:ident, $pos:literal) => {{
134 let (slice, line) = $line.split_at(1);
135 if slice[0] != ' ' {
136 return Err(Error::Space($line_num, slice[0], $pos));
137 }
138
139 line
140 }};
141}
142
143impl Tle {
144 const LINE_LEN: usize = 69;
145
146 #[allow(unused)]
151 fn parse_and_validate(line1: &[u8], line2: &[u8]) -> Result<Self, Error> {
152 let me = match Self::parse(line1, line2) {
153 Ok(me) => me,
154 Err(error) => return Err(error),
155 };
156
157 let line1 = tle_line(line1);
158 let calculated_checksum_1 = generate_checksum(line1.as_ref());
159 if me.checksum_1 != calculated_checksum_1 {
160 return Err(Error::InvalidChecksum(
161 Line::Line1,
162 me.checksum_1,
163 calculated_checksum_1,
164 ));
165 }
166
167 let line2 = tle_line(line2);
168 let calculated_checksum_2 = generate_checksum(line2.as_ref());
169 if me.checksum_2 != calculated_checksum_2 {
170 return Err(Error::InvalidChecksum(
171 Line::Line1,
172 me.checksum_2,
173 calculated_checksum_1,
174 ));
175 }
176
177 Ok(me)
178 }
179
180 pub fn parse(line1: &[u8], line2: &[u8]) -> Result<Self, Error> {
181 let line = match validate_line(line1, Line::Line1) {
182 Ok(l) => l,
183 Err(error) => return Err(error),
184 };
185
186 let (slice, line) = line.split_at(1);
187 if slice[0] != '1' {
188 return Err(Error::LineNumber(Line::Line1, line[0]));
189 }
190
191 let line = split_space!(Line::Line1, line, 1);
192
193 let (slice, line) = line.split_at(5);
194 let Some(satellite_catalog_number_1) = as_digits(slice) else {
195 return Err(Error::SatlliteCatalogNumber(Line::Line1));
196 };
197
198 let (slice, line) = line.split_at(1);
199 let classification = match slice[0] {
200 'U' => Classification::Unclassified,
201 'C' => Classification::Classified,
202 'S' => Classification::Secret,
203 found => return Err(Error::Classification(found)),
204 };
205
206 let line = split_space!(Line::Line1, line, 8);
207
208 let (slice, line) = line.split_at(2);
209 let Some(launch_year) = as_digits(slice) else {
210 return Err(Error::InternationalDesignatorLaunchYear);
211 };
212
213 let (slice, line) = line.split_at(3);
214 let Some(launch_num) = as_digits(slice) else {
215 return Err(Error::InternationalDesignatorLaunchNumber);
216 };
217
218 let (slice, line) = line.split_at(3);
219 let launch_piece = [slice[0], slice[1], slice[2]];
220 let internal_designator = InternationalDesignator {
221 launch_year: launch_year as u8,
222 launch_num: launch_num as u16,
223 launch_piece,
224 };
225
226 let line = split_space!(Line::Line1, line, 17);
227
228 let (slice, line) = line.split_at(2);
229 let Some(epoch_year) = as_digits(slice) else {
230 return Err(Error::EpochYear);
231 };
232 let (slice, line) = line.split_at(12);
233 let Ok(epoch_day_and_fractional_part) = String::from_iter(slice).parse::<f64>() else {
235 return Err(Error::EpochDay);
236 };
237
238 let line = split_space!(Line::Line1, line, 32);
239
240 let (slice, line) = line.split_at(10);
241 let slice = trim_leading_space(slice);
242 let Ok(first_derivative_of_mean_motion) = String::from_iter(slice).parse::<f32>() else {
243 return Err(Error::FirstDerivative);
244 };
245
246 let line = split_space!(Line::Line1, line, 43);
247
248 let (slice, line) = line.split_at(8);
249 let second_derivative_of_mean_motion = match parse_tle_f32(slice) {
250 Ok(s) => s,
251 Err(e) => return Err(e),
252 };
253
254 let line = split_space!(Line::Line1, line, 52);
255
256 let (mut slice, line) = line.split_at(8);
257 let mut sign = 1.0;
258 if slice[0] == '-' {
259 sign = -1.0;
260 let (_neg_sign, s) = slice.split_first().unwrap();
261 slice = s;
262 }
263
264 let b_star = match parse_tle_f32(slice) {
265 Ok(s) => s * sign,
266 Err(e) => return Err(e),
267 };
268
269 let line = split_space!(Line::Line1, line, 61);
270 let (slice, line) = line.split_at(1);
271 if slice[0] != '0' {
272 return Err(Error::EphemerisType(slice[0]));
273 }
274
275 let line = split_space!(Line::Line1, line, 63);
276
277 let (slice, line) = line.split_at(4);
278 let slice = trim_leading_space(slice);
279
280 let Some(element_set_number) = as_digits(slice) else {
281 return Err(Error::ElementSetNumber);
282 };
283
284 let Some(checksum_1) = as_digits(line) else {
285 return Err(Error::Checksum(Line::Line1, line[0]));
286 };
287 let checksum_1 = checksum_1 as u8;
288
289 let line = match validate_line(line2, Line::Line2) {
290 Ok(l) => l,
291 Err(error) => return Err(error),
292 };
293
294 let (slice, line) = line.split_first().unwrap();
295 if *slice != '2' {
296 return Err(Error::LineNumber(Line::Line2, *slice));
297 }
298
299 let line = split_space!(Line::Line1, line, 1);
300
301 let (slice, line) = line.split_at(5);
302 let Some(satellite_catalog_number_2) = as_digits(slice) else {
303 return Err(Error::SatlliteCatalogNumber(Line::Line2));
304 };
305
306 if satellite_catalog_number_1 != satellite_catalog_number_2 {
307 return Err(Error::SatelliteCatalogNumberMismatch(
308 satellite_catalog_number_1,
309 satellite_catalog_number_2,
310 ));
311 }
312 let satellite_catalog_number = satellite_catalog_number_1;
313
314 let line = split_space!(Line::Line2, line, 7);
315
316 let (slice, line) = line.split_at(8);
317 let slice = trim_leading_space(slice);
318 let Ok(inclination) = String::from_iter(slice).parse::<f32>() else {
319 return Err(Error::Inclination);
320 };
321
322 let line = split_space!(Line::Line2, line, 16);
323
324 let (slice, line) = line.split_at(8);
325 let Ok(right_ascension_of_ascending_node) = String::from_iter(slice).parse::<f32>() else {
326 return Err(Error::RightAscension);
327 };
328
329 let line = split_space!(Line::Line2, line, 25);
330
331 let (slice, line) = line.split_at(7);
332 let Some(eccentricity) = as_digits(slice) else {
333 return Err(Error::Eccentricty);
334 };
335 let Some(dig) = eccentricity.checked_ilog10() else {
336 return Err(Error::Eccentricty);
337 };
338 let leading_zeroes = dig as i32 - slice.len() as i32;
339 let eccentricity = (eccentricity as f32).powi(leading_zeroes);
340
341 let line = split_space!(Line::Line2, line, 33);
342
343 let (slice, line) = line.split_at(8);
344 let Ok(argument_of_perigee) = String::from_iter(slice).parse::<f32>() else {
345 return Err(Error::ArgumentOfPerigee);
346 };
347
348 let line = split_space!(Line::Line2, line, 42);
349
350 let (slice, line) = line.split_at(8);
351 let Ok(mean_anomaly) = String::from_iter(slice).parse::<f32>() else {
352 return Err(Error::MeanAnomaly);
353 };
354
355 let line = split_space!(Line::Line2, line, 51);
356
357 let (slice, line) = line.split_at(11);
358 let Ok(mean_motion) = String::from_iter(slice).parse::<f32>() else {
359 return Err(Error::MeanAnomaly);
360 };
361
362 let (slice, line) = line.split_at(5);
363 let Some(revolution_number_at_epoch) = as_digits(slice) else {
364 return Err(Error::MeanMotion);
365 };
366
367 let Some(checksum_2) = as_digits(line) else {
368 return Err(Error::Checksum(Line::Line2, line[0]));
369 };
370 let checksum_2 = checksum_2 as u8;
371
372 let me = Tle {
373 satellite_catalog_number,
374 classification,
375 international_designator: internal_designator,
376 epoch_year: epoch_year as u8,
377 epoch_day_and_fractional_part,
378 first_derivative_of_mean_motion,
379 second_derivative_of_mean_motion,
380 b_star,
381 element_set_number: element_set_number as u16,
382 checksum_1,
383 inclination,
384 right_ascension_of_ascending_node,
385 eccentricity,
386 argument_of_perigee,
387 mean_anomaly,
388 mean_motion,
389 revolution_number_at_epoch,
390 checksum_2,
391 };
392
393 Ok(me)
394 }
395}
396
397const fn trim_leading_space(line: &[char]) -> &[char] {
398 let mut blank = 0;
399 while blank <= line.len() {
400 if line[blank] == ' ' {
401 blank += 1;
402 } else {
403 break;
404 }
405 }
406 let (_trimmmed, slice) = line.split_at(blank);
407 slice
408}
409
410const fn generate_checksum(line: &[char]) -> u8 {
411 let mut i = 0;
412 let mut sum = 0;
413 while i < line.len() {
414 if line[i] == '-' {
415 sum += 1;
416 } else if let Some(dig) = line[i].to_digit(DECIMAL_RADIX) {
417 sum += dig;
418 }
419
420 i += 1;
421 }
422
423 (sum % 10) as u8
424}
425
426fn parse_tle_f32(line: &[char]) -> Result<f32, Error> {
427 let trimmed = trim_leading_space(line);
428
429 let mut idx = None;
430 let mut i = 0;
431 while i < trimmed.len() {
432 if trimmed[i] == '-' {
433 if idx.is_none() {
434 idx = Some(i);
435 } else {
436 return Err(Error::SecondDerivative);
437 }
438 }
439 i += 1;
440 }
441
442 let Some(idx) = idx else {
443 return Err(Error::SecondDerivative);
444 };
445 let (num, exp) = trimmed.split_at(idx);
446 let Some(num) = as_digits(num) else {
447 return Err(Error::SecondDerivative);
448 };
449 let Some((neg, exp)) = exp.split_first() else {
450 return Err(Error::SecondDerivative);
451 };
452 assert_eq!(*neg, '-');
453 let Some(exp) = as_digits(exp) else {
454 return Err(Error::SecondDerivative);
455 };
456
457 let val = (num as f32).powi(-(exp as i32));
458
459 Ok(val)
460}
461
462const fn validate_line(line: &[u8], line_num: Line) -> Result<[char; Tle::LINE_LEN], Error> {
463 if line.len() != Tle::LINE_LEN {
464 return Err(Error::InvalidLineSize(line_num, line.len()));
465 }
466
467 if !line.is_ascii() {
468 return Err(Error::ContainsNonAsciiCharacter(line_num));
469 }
470
471 Ok(tle_line(line))
472}
473
474const fn tle_line(line: &[u8]) -> [char; Tle::LINE_LEN] {
478 let mut arr = [char::MAX; Tle::LINE_LEN];
479 let mut i = 0;
480 while i < Tle::LINE_LEN {
481 arr[i] = line[i] as char;
482 i += 1;
483 }
484 arr
485}
486
487const fn as_digits(chars: &[char]) -> Option<u32> {
488 if chars.len() == 0 {
489 return None;
490 }
491
492 let mut val: u32 = 0;
493
494 let mut i = 0;
495
496 while i < chars.len() {
497 let Some(ascii_val) = chars[chars.len() - 1 - i].to_digit(DECIMAL_RADIX) else {
498 return None;
499 };
500 val += ascii_val * 10u32.pow(i as u32);
501 i += 1;
502 }
503
504 Some(val)
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn tle_test() {
513 let line1 = b"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
515 let line2 = b"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
516 let _ = Tle::parse(line1, line2).unwrap();
517 let line1 = b"1 23455U 94089A 97320.90946019 .00000140 00000-0 10191-3 0 2621";
519 let line2 = b"2 23455 99.0090 272.6745 0008546 223.1686 136.8816 14.11711747148495";
520 let _ = Tle::parse(line1, line2).unwrap();
521 }
522
523 #[test]
524 fn as_digits_is_valid() {
525 let x = ['1', '2', '3', '4'];
526 assert_eq!(as_digits(&x), Some(1234_u32));
527 let x = ['0', '2', '3', '4'];
528 assert_eq!(as_digits(&x), Some(234_u32));
529 let x = ['9', '0', '0', '9'];
530 assert_eq!(as_digits(&x), Some(9009_u32));
531 let x = ['8'];
532 assert_eq!(as_digits(&x), Some(8_u32));
533 }
534}