Files
rsp6-decoder/src/payload.rs
2022-12-30 13:33:34 +00:00

263 lines
8.7 KiB
Rust

//! Decoding the actual inner decrypted payload bit.
use crate::{slice_base64, slice_bool};
use anyhow::anyhow;
use bitvec::field::BitField;
use bitvec::order::Msb0;
use bitvec::slice::BitSlice;
use bitvec::view::BitView;
use time::macros::datetime;
use time::{Date, Duration, PrimitiveDateTime, Time};
#[derive(Copy, Clone, Debug)]
pub enum CouponType {
Single = 0,
Season = 1,
ReturnOutbound = 2,
ReturnInbound = 3,
}
#[derive(Clone, Debug)]
pub struct TicketPurchaseDetails {
pub purchase_time: PrimitiveDateTime,
pub price_pence: u32,
pub purchase_reference: Option<String>,
pub days_of_validity: u16,
}
#[derive(Clone, Debug)]
pub struct Reservation {
pub retail_service_id: String,
pub coach: char,
pub seat_number: u8,
pub seat_letter: Option<char>,
}
impl Reservation {
pub fn decode(resv: &BitSlice<u8, Msb0>) -> anyhow::Result<Self> {
if resv.len() != 45 {
return Err(anyhow!("reservation length {}, not 45", resv.len()));
}
let rsid_1 = char::from(resv[0..6].load_be::<u8>() + 32);
let rsid_2 = char::from(resv[6..12].load_be::<u8>() + 32);
let rsid_nums: u16 = resv[12..26].load_be();
let retail_service_id = format!("{}{}{:04}", rsid_1, rsid_2, rsid_nums);
let coach = char::from(resv[26..32].load_be::<u8>() + 32);
let seat_letter = char::from(resv[32..38].load_be::<u8>() + 32);
let seat_letter = (seat_letter != ' ').then_some(seat_letter);
let seat_number: u8 = resv[38..45].load_be();
Ok(Self {
retail_service_id,
coach,
seat_number,
seat_letter,
})
}
}
#[derive(Clone, Debug)]
pub struct Rsp6Ticket {
pub manually_inspect: bool,
pub ticket_reference: String,
pub checksum: char,
pub version: u8,
pub standard_class: bool,
pub lennon_ticket_type: String,
pub fare: String,
pub origin_nlc: String,
pub destination_nlc: String,
pub retailer_id: String,
pub child_ticket: bool,
pub coupon_type: CouponType,
pub discount_code: u16,
pub route_code: u32,
pub start_date: Date,
pub depart_time: Option<Time>,
pub passenger_id: Option<String>,
pub passenger_name: Option<String>,
pub passenger_gender: Option<u8>,
pub restriction_code: Option<String>,
pub bidirectional: bool,
pub limited_duration: Option<Duration>,
pub purchase_details: Option<TicketPurchaseDetails>,
pub reservations: Vec<Reservation>,
pub free_text: Option<String>,
}
impl Rsp6Ticket {
fn base64(tkt: &[u8], from: usize, to: usize) -> String {
let chars = (to - from) / 6;
assert_eq!(chars * 6, to - from);
slice_base64(tkt, from, chars)
}
fn decode_limited_duration(dur: u8) -> Option<Duration> {
Some(match dur {
1 => Duration::minutes(15),
2 => Duration::minutes(30),
3 => Duration::minutes(45),
4 => Duration::hours(1),
5 => Duration::minutes(90),
6 => Duration::hours(2),
7 => Duration::hours(3),
8 => Duration::hours(4),
9 => Duration::hours(5),
10 => Duration::hours(6),
11 => Duration::hours(8),
12 => Duration::hours(10),
13 => Duration::hours(12),
14 => Duration::hours(18),
_ => return None,
})
}
fn decode_passenger_id(id: u32) -> Option<String> {
if id == 0 {
return None;
}
let prefix = match id / 10000 {
0 => return None,
1 => "CCD",
2 => "DCD",
3 => "PPT",
4 => "DLC",
5 => "AFC",
6 => "NIC",
7 => "NHS",
_ => "???",
};
Some(format!("{}{:04}", prefix, (id % 10000)))
}
pub fn decode(tkt: &[u8]) -> anyhow::Result<Self> {
let bit_tkt = tkt.view_bits::<Msb0>();
let manually_inspect = slice_bool(tkt, 0);
let ticket_reference = Self::base64(tkt, 8, 62);
let checksum = Self::base64(tkt, 62, 68).chars().next().unwrap();
let version: u8 = bit_tkt[68..72].load_be();
let standard_class = slice_bool(tkt, 72);
let lennon_ticket_type = Self::base64(tkt, 73, 91);
let fare = Self::base64(tkt, 91, 109);
let origin_nlc = Self::base64(tkt, 109, 133);
let destination_nlc = Self::base64(tkt, 133, 157);
let retailer_id = Self::base64(tkt, 157, 181);
let is_child = slice_bool(tkt, 181);
let coupon_type = match bit_tkt[182..184].load_be::<u8>() {
0 => CouponType::Single,
1 => CouponType::Season,
2 => CouponType::ReturnOutbound,
3 => CouponType::ReturnInbound,
_ => unreachable!(), // only 2-bit int
};
let discount_code: u16 = bit_tkt[184..194].load_be();
let route_code: u32 = bit_tkt[194..211].load_be();
let start_time_days: u32 = bit_tkt[211..225].load_be();
let start_time_secs: u32 = bit_tkt[225..236].load_be();
let start_time: PrimitiveDateTime =
CapitalismDateTime::new(start_time_days, start_time_secs).into();
let depart_time_flag: u8 = bit_tkt[236..238].load_be();
// FIXME
let depart_time = (depart_time_flag == 0).then_some(start_time.time());
let start_date = start_time.date();
let passenger_id = Self::decode_passenger_id(bit_tkt[238..255].load_be());
let passenger_name = Self::base64(tkt, 255, 327);
let passenger_name = (!passenger_name.trim().is_empty()).then_some(passenger_name);
let passenger_gender: u8 = bit_tkt[327..329].load_be();
let passenger_gender = (passenger_gender != 0).then_some(passenger_gender);
let restriction_code = Self::base64(tkt, 329, 347);
let restriction_code = (!restriction_code.trim().is_empty()).then_some(restriction_code);
let bidirectional = slice_bool(tkt, 372);
let limited_duration = Self::decode_limited_duration(bit_tkt[379..383].load_be());
let is_full_ticket = slice_bool(tkt, 384);
let purchase_details = if is_full_ticket {
let purchase_time_days: u32 = bit_tkt[390..404].load_be();
let purchase_time_secs: u32 = bit_tkt[404..415].load_be();
let purchase_time: PrimitiveDateTime =
CapitalismDateTime::new(purchase_time_days, purchase_time_secs).into();
let price_pence: u32 = bit_tkt[415..436].load_be();
let purchase_reference = Self::base64(tkt, 449, 497);
let purchase_reference =
(!purchase_reference.trim().is_empty()).then_some(purchase_reference);
let mut days_of_validity = bit_tkt[497..506].load_be();
if days_of_validity == 0 {
days_of_validity = 1;
}
Some(TicketPurchaseDetails {
purchase_time,
price_pence,
purchase_reference,
days_of_validity,
})
} else {
None
};
let reservations_start = if is_full_ticket { 512 } else { 390 };
let reservations_count: u8 = bit_tkt[386..390].load_be();
let reservations = (0..reservations_count)
.map(|x| {
let start = reservations_start + (45 * x) as usize;
let end = reservations_start + (45 * (1 + x)) as usize;
Reservation::decode(&bit_tkt[start..end])
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
manually_inspect,
ticket_reference,
checksum,
version,
standard_class,
lennon_ticket_type,
fare,
origin_nlc,
destination_nlc,
retailer_id,
child_ticket: is_child,
coupon_type,
discount_code,
route_code,
start_date,
depart_time,
passenger_id,
passenger_name,
passenger_gender,
restriction_code,
bidirectional,
limited_duration,
purchase_details,
reservations,
free_text: None,
})
}
}
#[derive(Copy, Clone, Debug)]
pub struct CapitalismDateTime {
days: u32,
minutes: u32,
}
impl CapitalismDateTime {
pub const PRIVATISATION_EPOCH: PrimitiveDateTime = datetime!(1997-01-01 00:00:00);
pub fn new(days: u32, minutes: u32) -> Self {
Self { days, minutes }
}
}
impl Into<PrimitiveDateTime> for CapitalismDateTime {
fn into(self) -> PrimitiveDateTime {
CapitalismDateTime::PRIVATISATION_EPOCH
+ Duration::days(self.days as _)
+ Duration::minutes(self.minutes as _)
}
}