1use 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!(), };
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!(), };
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}