rsp6_decoder/
payload.rs

1//! Decoding the actual inner decrypted payload bit.
2
3use anyhow::anyhow;
4use bitvec::field::BitField;
5use bitvec::order::Msb0;
6use bitvec::slice::BitSlice;
7use bitvec::view::BitView;
8use serde::Serialize;
9use time::macros::datetime;
10use time::{Date, Duration, PrimitiveDateTime, Time};
11
12#[derive(Copy, Clone, Debug, Serialize)]
13pub enum CouponType {
14    Single = 0,
15    Season = 1,
16    ReturnOutbound = 2,
17    ReturnInbound = 3,
18}
19
20#[derive(Copy, Clone, Debug, Serialize)]
21pub enum DepartTimeFlag {
22    Mystery = 1,
23    Specific = 2,
24    Suggested = 3,
25}
26
27#[derive(Clone, Debug, Serialize)]
28pub struct TicketPurchaseDetails {
29    pub purchase_time: PrimitiveDateTime,
30    pub price_pence: u32,
31    pub purchase_reference: Option<String>,
32    pub days_of_validity: u16,
33}
34
35#[derive(Clone, Debug, Serialize)]
36pub struct Reservation {
37    pub retail_service_id: String,
38    pub coach: char,
39    pub seat_number: u8,
40    pub seat_letter: Option<char>,
41}
42
43impl Reservation {
44    pub fn decode(resv: &BitSlice<u8, Msb0>) -> anyhow::Result<Self> {
45        if resv.len() != 45 {
46            return Err(anyhow!("reservation length {}, not 45", resv.len()));
47        }
48        let rsid_1 = char::from(resv[0..6].load_be::<u8>() + 32);
49        let rsid_2 = char::from(resv[6..12].load_be::<u8>() + 32);
50        let rsid_nums: u16 = resv[12..26].load_be();
51        let retail_service_id = format!("{}{}{:04}", rsid_1, rsid_2, rsid_nums);
52        let coach = char::from(resv[26..32].load_be::<u8>() + 32);
53        let seat_letter = char::from(resv[32..38].load_be::<u8>() + 32);
54        let seat_letter = (seat_letter != ' ').then_some(seat_letter);
55        let seat_number: u8 = resv[38..45].load_be();
56        Ok(Self {
57            retail_service_id,
58            coach,
59            seat_number,
60            seat_letter,
61        })
62    }
63}
64
65#[derive(Clone, Debug, Serialize)]
66pub struct Rsp6Ticket {
67    pub manually_inspect: bool,
68    pub issuer_id: String,
69    pub ticket_reference: String,
70    pub sub_utn: String,
71    pub checksum: char,
72    pub version: u8,
73    pub standard_class: bool,
74    pub lennon_ticket_type: String,
75    pub fare: String,
76    pub origin_nlc: String,
77    pub destination_nlc: String,
78    pub retailer_id: String,
79    pub child_ticket: bool,
80    pub coupon_type: CouponType,
81    pub discount_code: u16,
82    pub route_code: u32,
83    pub start_date: Date,
84    pub depart_time_flag: Option<DepartTimeFlag>,
85    pub depart_time: Time,
86    pub passenger_id: Option<String>,
87    pub passenger_name: Option<String>,
88    pub passenger_gender: Option<u8>,
89    pub restriction_code: Option<String>,
90    pub bidirectional: bool,
91    pub limited_duration: Option<Duration>,
92    pub purchase_details: Option<TicketPurchaseDetails>,
93    pub reservations: Vec<Reservation>,
94    pub free_text: Option<String>,
95    pub osi_nlc: Option<String>,
96    pub mystery_flag: bool,
97    pub mystery_header: String,
98}
99
100impl Rsp6Ticket {
101    pub fn base64(tkt: &[u8], from: usize, to: usize) -> String {
102        let chars = (to - from) / 6;
103        assert_eq!(chars * 6, to - from);
104        tkt.view_bits::<Msb0>()[from..to]
105            .chunks(6)
106            .map(|x| char::from(x.load_be::<u8>() + 32))
107            .collect()
108    }
109
110    fn decode_limited_duration(dur: u8) -> Option<Duration> {
111        Some(match dur {
112            1 => Duration::minutes(15),
113            2 => Duration::minutes(30),
114            3 => Duration::minutes(45),
115            4 => Duration::hours(1),
116            5 => Duration::minutes(90),
117            6 => Duration::hours(2),
118            7 => Duration::hours(3),
119            8 => Duration::hours(4),
120            9 => Duration::hours(5),
121            10 => Duration::hours(6),
122            11 => Duration::hours(8),
123            12 => Duration::hours(10),
124            13 => Duration::hours(12),
125            14 => Duration::hours(18),
126            _ => return None,
127        })
128    }
129
130    fn decode_passenger_id(id: u32) -> Option<String> {
131        if id == 0 {
132            return None;
133        }
134        let prefix = match id / 10000 {
135            0 => return None,
136            1 => "CCD",
137            2 => "DCD",
138            3 => "PPT",
139            4 => "DLC",
140            5 => "AFC",
141            6 => "NIC",
142            7 => "NHS",
143            _ => "???",
144        };
145        Some(format!("{}{:04}", prefix, (id % 10000)))
146    }
147
148    pub fn decode(tkt: &[u8], issuer_id: String, sub_utn: String) -> anyhow::Result<Self> {
149        let bit_tkt = tkt.view_bits::<Msb0>();
150
151        let manually_inspect = bit_tkt[0];
152        let mystery_header: u8 = bit_tkt[1..8].load_be();
153        let mystery_header = format!("{:07b}", mystery_header);
154        let ticket_reference = Self::base64(tkt, 8, 62);
155        let checksum = Self::base64(tkt, 62, 68).chars().next().unwrap();
156        let version: u8 = bit_tkt[68..72].load_be();
157
158        let standard_class = bit_tkt[72];
159        let lennon_ticket_type = Self::base64(tkt, 73, 91);
160        let fare = Self::base64(tkt, 91, 109);
161        let origin_nlc = Self::base64(tkt, 109, 133);
162        let destination_nlc = Self::base64(tkt, 133, 157);
163        let retailer_id = Self::base64(tkt, 157, 181);
164
165        let is_child = bit_tkt[181];
166        let coupon_type = match bit_tkt[182..184].load_be::<u8>() {
167            0 => CouponType::Single,
168            1 => CouponType::Season,
169            2 => CouponType::ReturnOutbound,
170            3 => CouponType::ReturnInbound,
171            _ => unreachable!(), // only 2-bit int
172        };
173        let discount_code: u16 = bit_tkt[184..194].load_be();
174        let route_code: u32 = bit_tkt[194..211].load_be();
175
176        let start_time_days: u32 = bit_tkt[211..225].load_be();
177        let start_time_secs: u32 = bit_tkt[225..236].load_be();
178        let start_time: PrimitiveDateTime =
179            CapitalismDateTime::new(start_time_days, start_time_secs).into();
180        let depart_time_flag = match bit_tkt[236..238].load_be::<u8>() {
181            0 => None,
182            1 => Some(DepartTimeFlag::Mystery),
183            2 => Some(DepartTimeFlag::Specific),
184            3 => Some(DepartTimeFlag::Suggested),
185            _ => unreachable!(), // only 2-bit int
186        };
187        let depart_time = start_time.time();
188        let start_date = start_time.date();
189
190        let passenger_id = Self::decode_passenger_id(bit_tkt[238..255].load_be());
191        let passenger_name = Self::base64(tkt, 255, 327);
192        let passenger_name =
193            (!passenger_name.trim().is_empty()).then(|| passenger_name.trim().to_owned());
194        let passenger_gender: u8 = bit_tkt[327..329].load_be();
195        let passenger_gender = (passenger_gender != 0).then_some(passenger_gender);
196
197        let restriction_code = Self::base64(tkt, 329, 347);
198        let osi_nlc = Self::base64(tkt, 347, 371);
199        let osi_nlc = (!osi_nlc.trim().is_empty()).then(|| osi_nlc.trim().to_owned());
200        let mystery_flag = bit_tkt[371];
201        let restriction_code =
202            (!restriction_code.trim().is_empty()).then(|| restriction_code.trim().to_owned());
203        let bidirectional = bit_tkt[372];
204        let limited_duration = Self::decode_limited_duration(bit_tkt[379..383].load_be());
205
206        let is_full_ticket = bit_tkt[384];
207
208        let purchase_details = if is_full_ticket {
209            let purchase_time_days: u32 = bit_tkt[390..404].load_be();
210            let purchase_time_secs: u32 = bit_tkt[404..415].load_be();
211            let purchase_time: PrimitiveDateTime =
212                CapitalismDateTime::new(purchase_time_days, purchase_time_secs).into();
213            let price_pence: u32 = bit_tkt[415..436].load_be();
214            let purchase_reference = Self::base64(tkt, 449, 497);
215            let purchase_reference = (!purchase_reference.trim().is_empty())
216                .then(|| purchase_reference.trim().to_owned());
217            let mut days_of_validity = bit_tkt[497..506].load_be();
218            if days_of_validity == 0 {
219                days_of_validity = 1;
220            }
221            Some(TicketPurchaseDetails {
222                purchase_time,
223                price_pence,
224                purchase_reference,
225                days_of_validity,
226            })
227        } else {
228            None
229        };
230
231        let reservations_start = if is_full_ticket { 512 } else { 390 };
232        let reservations_count: u8 = bit_tkt[386..390].load_be();
233        let reservations = (0..reservations_count)
234            .map(|x| {
235                let start = reservations_start + (45 * x) as usize;
236                let end = reservations_start + (45 * (1 + x)) as usize;
237                Reservation::decode(&bit_tkt[start..end])
238            })
239            .collect::<Result<Vec<_>, _>>()?;
240
241        let has_free_text = bit_tkt[385];
242        let free_text_is_extended = bit_tkt[383];
243        let mut free_text = None;
244        if has_free_text {
245            let reservations_end = reservations_start + (45 * reservations_count as usize);
246            let end = if free_text_is_extended { 863 } else { 783 };
247            let length = 6 * ((end - reservations_end) / 6);
248            let text = Self::base64(tkt, reservations_end, reservations_end + length);
249            if !text.trim().is_empty() {
250                free_text = Some(text.trim().to_owned());
251            }
252        }
253
254        Ok(Self {
255            manually_inspect,
256            issuer_id,
257            ticket_reference,
258            checksum,
259            version,
260            standard_class,
261            lennon_ticket_type,
262            fare,
263            origin_nlc,
264            destination_nlc,
265            retailer_id,
266            child_ticket: is_child,
267            coupon_type,
268            discount_code,
269            route_code,
270            start_date,
271            depart_time,
272            depart_time_flag,
273            passenger_id,
274            passenger_name,
275            passenger_gender,
276            restriction_code,
277            bidirectional,
278            limited_duration,
279            purchase_details,
280            reservations,
281            free_text,
282            osi_nlc,
283            mystery_flag,
284            mystery_header,
285            sub_utn,
286        })
287    }
288}
289
290#[derive(Copy, Clone, Debug)]
291pub struct CapitalismDateTime {
292    days: u32,
293    minutes: u32,
294}
295
296impl CapitalismDateTime {
297    pub const PRIVATISATION_EPOCH: PrimitiveDateTime = datetime!(1997-01-01 00:00:00);
298
299    pub fn new(days: u32, minutes: u32) -> Self {
300        Self { days, minutes }
301    }
302}
303
304impl Into<PrimitiveDateTime> for CapitalismDateTime {
305    fn into(self) -> PrimitiveDateTime {
306        CapitalismDateTime::PRIVATISATION_EPOCH
307            + Duration::days(self.days as _)
308            + Duration::minutes(self.minutes as _)
309    }
310}