mirror of
https://git.eta.st/eta/rsp6-decoder.git
synced 2025-12-30 13:06:24 +00:00
263 lines
8.7 KiB
Rust
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 _)
|
|
}
|
|
}
|