passgen: restructure things into a lib and a main
This commit is contained in:
parent
6796dfad18
commit
73956a5f70
11 changed files with 1185 additions and 879 deletions
1
rust/passgen/Cargo.lock
generated
1
rust/passgen/Cargo.lock
generated
|
@ -1646,6 +1646,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures-util",
|
||||
"google-walletobjects1",
|
||||
"http",
|
||||
"hyper",
|
||||
|
|
|
@ -10,6 +10,7 @@ anyhow = { version = "1.0.68", features = ["backtrace"] }
|
|||
async-trait = "0.1.61"
|
||||
chrono = "0.4.23"
|
||||
clap = { version = "4.0.32", features = ["cargo", "derive"] }
|
||||
futures-util = "0.3.25"
|
||||
google-walletobjects1 = "4.0.4"
|
||||
http = "0.2.8"
|
||||
hyper = "0.14.23"
|
||||
|
|
93
rust/passgen/src/add_to_wallet.rs
Normal file
93
rust/passgen/src/add_to_wallet.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use crate::service_account::ServiceAccount;
|
||||
|
||||
use anyhow::Result;
|
||||
use jwt_simple::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddToWalletPayloadObject {
|
||||
id: String,
|
||||
|
||||
#[serde(rename = "classId", skip_serializing_if = "Option::is_none")]
|
||||
class_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddToWalletPayload {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", rename = "flightClasses")]
|
||||
flight_classes: Vec<AddToWalletPayloadObject>,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", rename = "flightObjects")]
|
||||
flight_objects: Vec<AddToWalletPayloadObject>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AddToWalletClaims {
|
||||
origins: Vec<String>,
|
||||
|
||||
#[serde(rename = "typ")]
|
||||
jwt_type: String,
|
||||
|
||||
payload: AddToWalletPayload,
|
||||
}
|
||||
|
||||
pub struct PassClassIdentifier {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
pub struct PassIdentifier {
|
||||
pub id: String,
|
||||
pub class_id: String,
|
||||
}
|
||||
|
||||
pub struct AddToWallet {
|
||||
pub service_account_email: String,
|
||||
|
||||
pub flight_classes: Vec<PassClassIdentifier>,
|
||||
pub flight_passes: Vec<PassIdentifier>,
|
||||
}
|
||||
|
||||
impl AddToWallet {
|
||||
pub fn to_claims(&self) -> JWTClaims<AddToWalletClaims> {
|
||||
JWTClaims {
|
||||
issued_at: None,
|
||||
expires_at: None,
|
||||
invalid_before: None,
|
||||
audiences: Some(jwt_simple::claims::Audiences::AsString(
|
||||
"google".to_string(),
|
||||
)),
|
||||
issuer: Some(self.service_account_email.to_string()),
|
||||
jwt_id: None,
|
||||
subject: None,
|
||||
nonce: None,
|
||||
custom: AddToWalletClaims {
|
||||
jwt_type: "savetowallet".to_string(),
|
||||
origins: vec!["www.lukegb.com".to_string()],
|
||||
payload: AddToWalletPayload {
|
||||
flight_classes: self
|
||||
.flight_classes
|
||||
.iter()
|
||||
.map(|c| AddToWalletPayloadObject {
|
||||
id: c.id.to_string(),
|
||||
class_id: None,
|
||||
})
|
||||
.collect(),
|
||||
flight_objects: self
|
||||
.flight_passes
|
||||
.iter()
|
||||
.map(|o| AddToWalletPayloadObject {
|
||||
id: o.id.to_string(),
|
||||
class_id: Some(o.class_id.to_string()),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_url(&self, sa: &ServiceAccount) -> Result<String> {
|
||||
let claims = self.to_claims();
|
||||
let token = sa.key_pair.sign(claims)?;
|
||||
Ok(format!("https://pay.google.com/gp/v/save/{}", token))
|
||||
}
|
||||
}
|
133
rust/passgen/src/insert_or_update.rs
Normal file
133
rust/passgen/src/insert_or_update.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures_util::future::try_join_all;
|
||||
use google_walletobjects1::api::{
|
||||
FlightClass, FlightObject, FlightclasMethods, FlightobjectMethods,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[async_trait]
|
||||
pub trait InsertOrUpdate<T: Clone> {
|
||||
async fn insert_now(&self, value: &T) -> Result<bool>;
|
||||
async fn update_now(&self, value: &T) -> Result<()>;
|
||||
}
|
||||
|
||||
fn is_already_exists(err: &google_walletobjects1::Error) -> bool {
|
||||
match err {
|
||||
google_walletobjects1::Error::BadRequest(v) => match v.pointer("/error/code") {
|
||||
Some(num) => num == 409,
|
||||
None => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> InsertOrUpdate<FlightClass> for FlightclasMethods<'_, S>
|
||||
where
|
||||
S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
|
||||
S::Response: hyper::client::connect::Connection
|
||||
+ tokio::io::AsyncRead
|
||||
+ tokio::io::AsyncWrite
|
||||
+ Send
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
S::Future: Send + Unpin + 'static,
|
||||
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
async fn insert_now(&self, value: &FlightClass) -> Result<bool> {
|
||||
self.insert(value.clone())
|
||||
.doit()
|
||||
.await
|
||||
.and(Ok(true))
|
||||
.or_else(|e| {
|
||||
if is_already_exists(&e) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn update_now(&self, value: &FlightClass) -> Result<()> {
|
||||
self.update(
|
||||
value.clone(),
|
||||
&value
|
||||
.id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("no id on FlightClass"))?,
|
||||
)
|
||||
.doit()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> InsertOrUpdate<FlightObject> for FlightobjectMethods<'_, S>
|
||||
where
|
||||
S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
|
||||
S::Response: hyper::client::connect::Connection
|
||||
+ tokio::io::AsyncRead
|
||||
+ tokio::io::AsyncWrite
|
||||
+ Send
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
S::Future: Send + Unpin + 'static,
|
||||
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
async fn insert_now(&self, value: &FlightObject) -> Result<bool> {
|
||||
self.insert(value.clone())
|
||||
.doit()
|
||||
.await
|
||||
.and(Ok(true))
|
||||
.or_else(|e| {
|
||||
if is_already_exists(&e) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn update_now(&self, value: &FlightObject) -> Result<()> {
|
||||
self.update(
|
||||
value.clone(),
|
||||
&value
|
||||
.id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("no id on FlightObject"))?,
|
||||
)
|
||||
.doit()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert_or_update_now<T: Clone>(into: &dyn InsertOrUpdate<T>, value: &T) -> Result<()> {
|
||||
let created: bool = into.insert_now(value).await?;
|
||||
if !created {
|
||||
into.update_now(value).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_or_update_now_arc<T: Clone>(
|
||||
into: Arc<&dyn InsertOrUpdate<T>>,
|
||||
value: &T,
|
||||
) -> Result<()> {
|
||||
insert_or_update_now(*into.as_ref(), value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_or_update_many<T: Clone>(
|
||||
into: &dyn InsertOrUpdate<T>,
|
||||
values: Vec<T>,
|
||||
) -> Result<()> {
|
||||
let i = Arc::new(into);
|
||||
try_join_all(
|
||||
values
|
||||
.iter()
|
||||
.map(|c| insert_or_update_now_arc(i.clone(), c)),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
7
rust/passgen/src/lib.rs
Normal file
7
rust/passgen/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod add_to_wallet;
|
||||
pub mod insert_or_update;
|
||||
pub mod resolution792;
|
||||
pub mod scanner;
|
||||
pub mod service_account;
|
||||
pub mod static_data;
|
||||
pub mod walletobjects;
|
|
@ -1,33 +1,19 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::naive::NaiveDate;
|
||||
use chrono::Datelike;
|
||||
use chrono::Local;
|
||||
use std::cmp::min;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use google_walletobjects1::api::{
|
||||
FlightClass, FlightObject, FlightclasMethods, FlightobjectMethods,
|
||||
};
|
||||
use google_walletobjects1::oauth2::authenticator::{
|
||||
Authenticator, DefaultHyperClient, HyperClientBuilder,
|
||||
};
|
||||
use google_walletobjects1::oauth2::{read_service_account_key, ServiceAccountAuthenticator};
|
||||
use jwt_simple::algorithms::{RS256KeyPair, RSAKeyPairLike};
|
||||
use jwt_simple::claims::JWTClaims;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use phf::phf_map;
|
||||
use google_walletobjects1::api::{FlightClass, FlightObject};
|
||||
use serde::Deserialize;
|
||||
use tokio::try_join;
|
||||
|
||||
use passgen::add_to_wallet::{AddToWallet, PassClassIdentifier, PassIdentifier};
|
||||
use passgen::insert_or_update::*;
|
||||
use passgen::resolution792::BoardingPass;
|
||||
use passgen::scanner::scan;
|
||||
use passgen::service_account::load_service_account;
|
||||
use passgen::walletobjects::*;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None, propagate_version = true, subcommand_required = true, arg_required_else_help = true)]
|
||||
struct Cli {
|
||||
|
@ -95,742 +81,11 @@ struct UploadScan {
|
|||
image: PathBuf,
|
||||
}
|
||||
|
||||
struct AirlineDefinition<'a> {
|
||||
iata_code: &'a str,
|
||||
name: &'a str,
|
||||
boarding_policy: &'a str,
|
||||
seat_class_policy: &'a str,
|
||||
|
||||
boarding_pass_background_colour: &'a str,
|
||||
|
||||
frequent_flyer_program_name: Option<&'a str>,
|
||||
|
||||
logo_url: &'a str,
|
||||
alliance_logo_url: Option<&'a str>,
|
||||
hero_image_logo_url: Option<&'a str>,
|
||||
boarding_privilege_logo_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
const TSA_PRECHECK_LOGO: &str = "https://p.lukegb.com/raw/MiserablyDirectPiglet.jpg";
|
||||
|
||||
static AIRLINE_DATA: phf::Map<&'static str, AirlineDefinition<'static>> = phf_map! {
|
||||
"VS" => AirlineDefinition{
|
||||
iata_code: "VS",
|
||||
name: "Virgin Atlantic",
|
||||
boarding_policy: "GROUP_BASED",
|
||||
seat_class_policy: "CABIN_BASED",
|
||||
|
||||
boarding_pass_background_colour: "#4f145b",
|
||||
|
||||
frequent_flyer_program_name: Some("Flying Club"),
|
||||
|
||||
logo_url: "https://p.lukegb.com/raw/VirtuallyCrispViper.png",
|
||||
alliance_logo_url: None,
|
||||
hero_image_logo_url: Some("https://p.lukegb.com/raw/FormerlyDistinctToad.png"),
|
||||
boarding_privilege_logo_url: Some("https://p.lukegb.com/raw/DefinitelyVerifiedTitmouse.png"),
|
||||
},
|
||||
};
|
||||
|
||||
fn scan(image_path: &PathBuf) -> Result<String> {
|
||||
let image_path_str = (*image_path).to_str().ok_or(anyhow!("invalid path"))?;
|
||||
rxing::helpers::detect_in_file(image_path_str, Some(rxing::BarcodeFormat::AZTEC))
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"could not parse Aztec barcode from image {}: {}",
|
||||
image_path_str,
|
||||
e
|
||||
)
|
||||
})
|
||||
.map(|r| r.getText().clone())
|
||||
}
|
||||
|
||||
struct ServiceAccount {
|
||||
service_account_name: String,
|
||||
authenticator: Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>,
|
||||
key_pair: RS256KeyPair,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ServiceAccountJSON {
|
||||
// We omit a lot of fields we don't care about.
|
||||
/// Service Account email
|
||||
client_email: String,
|
||||
|
||||
/// RSA private key
|
||||
private_key: String,
|
||||
}
|
||||
|
||||
async fn load_service_account(path: &PathBuf) -> Result<ServiceAccount> {
|
||||
let creds = read_service_account_key(path).await?;
|
||||
let sa = ServiceAccountAuthenticator::builder(creds).build().await?;
|
||||
|
||||
let sa_data = tokio::fs::read(path).await?;
|
||||
let sa_parsed: ServiceAccountJSON = serde_json::from_slice(&sa_data[..])?;
|
||||
let key_pair = RS256KeyPair::from_pem(&sa_parsed.private_key)?;
|
||||
|
||||
Ok(ServiceAccount {
|
||||
service_account_name: sa_parsed.client_email,
|
||||
authenticator: sa,
|
||||
key_pair: key_pair,
|
||||
})
|
||||
}
|
||||
|
||||
async fn load_thing<'a, T: Deserialize<'a>>(path: &PathBuf) -> Result<T> {
|
||||
let data = tokio::fs::read(path).await?;
|
||||
let mut deser = serde_json::Deserializer::from_reader(data.as_slice());
|
||||
let parsed: T = T::deserialize(&mut deser)?;
|
||||
return Ok(parsed);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait InsertOrUpdate<T: Clone> {
|
||||
async fn insert_now(&self, value: &T) -> Result<bool>;
|
||||
async fn update_now(&self, value: &T) -> Result<()>;
|
||||
}
|
||||
|
||||
fn is_already_exists(err: &google_walletobjects1::Error) -> bool {
|
||||
match err {
|
||||
google_walletobjects1::Error::BadRequest(v) => match v.pointer("/error/code") {
|
||||
Some(num) => num == 409,
|
||||
None => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> InsertOrUpdate<FlightClass> for FlightclasMethods<'_, S>
|
||||
where
|
||||
S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
|
||||
S::Response: hyper::client::connect::Connection
|
||||
+ tokio::io::AsyncRead
|
||||
+ tokio::io::AsyncWrite
|
||||
+ Send
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
S::Future: Send + Unpin + 'static,
|
||||
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
async fn insert_now(&self, value: &FlightClass) -> Result<bool> {
|
||||
self.insert(value.clone())
|
||||
.doit()
|
||||
.await
|
||||
.and(Ok(true))
|
||||
.or_else(|e| {
|
||||
if is_already_exists(&e) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn update_now(&self, value: &FlightClass) -> Result<()> {
|
||||
self.update(
|
||||
value.clone(),
|
||||
&value.id.clone().ok_or(anyhow!("no id on FlightClass"))?,
|
||||
)
|
||||
.doit()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> InsertOrUpdate<FlightObject> for FlightobjectMethods<'_, S>
|
||||
where
|
||||
S: tower_service::Service<http::Uri> + Clone + Send + Sync + 'static,
|
||||
S::Response: hyper::client::connect::Connection
|
||||
+ tokio::io::AsyncRead
|
||||
+ tokio::io::AsyncWrite
|
||||
+ Send
|
||||
+ Unpin
|
||||
+ 'static,
|
||||
S::Future: Send + Unpin + 'static,
|
||||
S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
async fn insert_now(&self, value: &FlightObject) -> Result<bool> {
|
||||
self.insert(value.clone())
|
||||
.doit()
|
||||
.await
|
||||
.and(Ok(true))
|
||||
.or_else(|e| {
|
||||
if is_already_exists(&e) {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
async fn update_now(&self, value: &FlightObject) -> Result<()> {
|
||||
self.update(
|
||||
value.clone(),
|
||||
&value.id.clone().ok_or(anyhow!("no id on FlightObject"))?,
|
||||
)
|
||||
.doit()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddToWalletPayloadObject {
|
||||
id: String,
|
||||
|
||||
#[serde(rename = "classId", skip_serializing_if = "Option::is_none")]
|
||||
class_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddToWalletPayload {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", rename = "flightClasses")]
|
||||
flight_classes: Vec<AddToWalletPayloadObject>,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", rename = "flightObjects")]
|
||||
flight_objects: Vec<AddToWalletPayloadObject>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddToWalletClaims {
|
||||
origins: Vec<String>,
|
||||
|
||||
#[serde(rename = "typ")]
|
||||
jwt_type: String,
|
||||
|
||||
payload: AddToWalletPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
enum PassengerStatus {
|
||||
NotCheckedIn = b'0',
|
||||
CheckedIn = b'1',
|
||||
BagsCheckedAndNotCheckedIn = b'2',
|
||||
BagsCheckedAndCheckedIn = b'3',
|
||||
PastSecurity = b'4',
|
||||
PastGate = b'5',
|
||||
Transit = b'6',
|
||||
Standby = b'7',
|
||||
BoardingDataRevalidated = b'8',
|
||||
OriginalBoardingLineUsedWhenTicketIssued = b'9',
|
||||
UpOrDownGradingPending = b'A',
|
||||
}
|
||||
|
||||
impl PassengerStatus {
|
||||
fn from(c: u8) -> Result<PassengerStatus> {
|
||||
for v in PassengerStatus::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid PassengerStatus value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
enum DocumentationVerificationStatus {
|
||||
NotRequired = b'0',
|
||||
Required = b'1',
|
||||
Performed = b'2',
|
||||
}
|
||||
|
||||
impl DocumentationVerificationStatus {
|
||||
fn from(c: u8) -> Result<DocumentationVerificationStatus> {
|
||||
for v in DocumentationVerificationStatus::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid DocumentationVerificationStatus value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
enum PassengerDescription {
|
||||
Adult = b'0',
|
||||
Male = b'1',
|
||||
Female = b'2',
|
||||
Child = b'3',
|
||||
Infant = b'4',
|
||||
NoPassenger = b'5', // cabin baggage
|
||||
AdultWithInfant = b'6',
|
||||
UnaccompaniedMinor = b'7',
|
||||
}
|
||||
|
||||
impl PassengerDescription {
|
||||
fn from(c: u8) -> Result<PassengerDescription> {
|
||||
for v in PassengerDescription::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid PassengerDescription value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
enum CheckInSource {
|
||||
Web = b'W',
|
||||
AirportKiosk = b'K',
|
||||
RemoteKiosk = b'R',
|
||||
MobileDevice = b'M',
|
||||
AirportAgent = b'O',
|
||||
TownAgent = b'T',
|
||||
ThirdPartyVendor = b'V',
|
||||
}
|
||||
|
||||
impl CheckInSource {
|
||||
fn from(c: u8) -> Result<CheckInSource> {
|
||||
for v in CheckInSource::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid CheckInSource value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BoardingPass {
|
||||
passenger_name: String,
|
||||
eticket_indicator: char,
|
||||
|
||||
version_number: Option<char>,
|
||||
passenger_description: Option<PassengerDescription>,
|
||||
checkin_source: Option<CheckInSource>,
|
||||
boarding_pass_source: Option<CheckInSource>,
|
||||
date_of_issue: Option<NaiveDate>,
|
||||
document_type: Option<char>,
|
||||
boarding_pass_issuing_airline: Option<String>,
|
||||
baggage_tag_plate_number: Option<String>,
|
||||
|
||||
legs: Vec<BoardingPassLeg>,
|
||||
|
||||
security_data_type: u8,
|
||||
security_data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn parse_boarding_pass_date(
|
||||
from_boarding_pass_str: &str,
|
||||
nearby_date: &NaiveDate,
|
||||
) -> Result<NaiveDate> {
|
||||
// Range is 0 to 9999 (although not really).
|
||||
let from_boarding_pass: u16 = from_boarding_pass_str.parse()?;
|
||||
|
||||
match from_boarding_pass_str.len() {
|
||||
4 => Ok(NaiveDate::from_yo_opt(
|
||||
(nearby_date.year() / 10 * 10) + ((from_boarding_pass / 1000) as i32),
|
||||
(from_boarding_pass % 1000).into(),
|
||||
)
|
||||
.ok_or(anyhow!("could not parse date {}", from_boarding_pass_str))?),
|
||||
3 => {
|
||||
// Find the one that's the fewest days away.
|
||||
let year = nearby_date.year();
|
||||
let day: u32 = (from_boarding_pass % 1000).into();
|
||||
let mut options = vec![
|
||||
NaiveDate::from_yo_opt(year, day),
|
||||
NaiveDate::from_yo_opt(year - 1, day),
|
||||
NaiveDate::from_yo_opt(year + 1, day),
|
||||
];
|
||||
options.sort_by(|a, b| {
|
||||
days_between(nearby_date, a)
|
||||
.unwrap_or(9999)
|
||||
.cmp(&days_between(nearby_date, b).unwrap_or(9999))
|
||||
});
|
||||
Ok(options[0].unwrap())
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"boarding pass date {} is wrong length",
|
||||
from_boarding_pass_str
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn days_between(nearby_date: &NaiveDate, maybe_date: &Option<NaiveDate>) -> Option<u64> {
|
||||
if maybe_date.is_none() {
|
||||
return None;
|
||||
}
|
||||
let is_date = maybe_date.as_ref().unwrap();
|
||||
let days_between = (*nearby_date - *is_date).num_days();
|
||||
Some(if days_between < 0 {
|
||||
(-days_between) as u64
|
||||
} else {
|
||||
days_between as u64
|
||||
})
|
||||
}
|
||||
|
||||
impl BoardingPass {
|
||||
fn parse(data: Vec<u8>, nearby_date: &NaiveDate) -> Result<BoardingPass> {
|
||||
if data[0] != b'M' {
|
||||
return Err(anyhow!(
|
||||
"format code was {} ({}), not M",
|
||||
data[0] as char,
|
||||
data[0]
|
||||
));
|
||||
}
|
||||
let leg_count = match data[1] {
|
||||
b'1' => Ok(1),
|
||||
b'2' => Ok(2),
|
||||
b'3' => Ok(3),
|
||||
b'4' => Ok(4),
|
||||
_ => Err(anyhow!(
|
||||
"leg count was {} ({}), should be 1 to 4",
|
||||
data[1] as char,
|
||||
data[1]
|
||||
)),
|
||||
}?;
|
||||
|
||||
let passenger_name: String = std::str::from_utf8(&data[2..22])?.trim_end().to_string();
|
||||
let eticket_indicator: char = data[22] as char;
|
||||
|
||||
// First leg mandatory data:
|
||||
let (first_leg, mut p) = BoardingPassLeg::parse_mandatory(&data[23..], nearby_date)?;
|
||||
p += 23;
|
||||
|
||||
// We now have enough to start composing the boardingpass.
|
||||
let mut boarding_pass = BoardingPass {
|
||||
passenger_name,
|
||||
eticket_indicator,
|
||||
|
||||
version_number: None,
|
||||
passenger_description: None,
|
||||
checkin_source: None,
|
||||
boarding_pass_source: None,
|
||||
date_of_issue: None,
|
||||
document_type: None,
|
||||
boarding_pass_issuing_airline: None,
|
||||
baggage_tag_plate_number: None,
|
||||
|
||||
legs: vec![first_leg],
|
||||
|
||||
security_data_type: 0xff,
|
||||
security_data: vec![],
|
||||
};
|
||||
|
||||
// Header + first leg optional data (why, IATA, why)
|
||||
let optional_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
|
||||
let mut optional_data = &data[p + 2..p + 2 + optional_data_size];
|
||||
p += 2 + optional_data_size;
|
||||
if optional_data.len() >= 1 {
|
||||
if optional_data[0] != b'>' {
|
||||
return Err(anyhow!("expected Beginning of version number ('>') at beginning of conditional fields, got {} ({})", optional_data[0], optional_data[0] as char));
|
||||
}
|
||||
optional_data = &optional_data[1..];
|
||||
}
|
||||
if optional_data.len() >= 1 {
|
||||
boarding_pass.version_number = Some(optional_data[0] as char);
|
||||
optional_data = &optional_data[1..];
|
||||
}
|
||||
if optional_data.len() >= 1 {
|
||||
let optional_data_unique_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&optional_data[0..2])?, 16)?;
|
||||
let mut optional_data_unique = &optional_data[2..optional_data_unique_size + 2];
|
||||
if optional_data_unique.len() >= 1 {
|
||||
boarding_pass.passenger_description = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(PassengerDescription::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 1 {
|
||||
boarding_pass.checkin_source = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(CheckInSource::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 1 {
|
||||
boarding_pass.boarding_pass_source = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(CheckInSource::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 4 {
|
||||
let date_of_issue_str = std::str::from_utf8(&optional_data_unique[0..4])?;
|
||||
boarding_pass.date_of_issue = if date_of_issue_str == " " {
|
||||
None
|
||||
} else {
|
||||
Some(parse_boarding_pass_date(date_of_issue_str, nearby_date)?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[4..];
|
||||
}
|
||||
// TODO: the above might fall through if we don't have 4 bytes of data
|
||||
if optional_data_unique.len() >= 1 {
|
||||
boarding_pass.document_type = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(optional_data_unique[0] as char)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 3 {
|
||||
let issuing_airline_str =
|
||||
std::str::from_utf8(&optional_data_unique[0..3])?.trim_end();
|
||||
boarding_pass.boarding_pass_issuing_airline = if issuing_airline_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(issuing_airline_str.to_string())
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[3..];
|
||||
}
|
||||
// TODO: the above might fall through if we don't have 3 bytes of data
|
||||
let bag_tags_str_len = min(13, optional_data_unique.len());
|
||||
let bag_tags_str =
|
||||
std::str::from_utf8(&optional_data_unique[0..bag_tags_str_len])?.trim_end();
|
||||
boarding_pass.baggage_tag_plate_number = if bag_tags_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(bag_tags_str.to_string())
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[bag_tags_str_len..];
|
||||
if optional_data_unique.len() != 0 {
|
||||
return Err(anyhow!(
|
||||
"trailing unique optional data ({} bytes)",
|
||||
optional_data_unique.len()
|
||||
));
|
||||
}
|
||||
optional_data = &optional_data[optional_data_unique_size + 2..];
|
||||
}
|
||||
|
||||
// Now for the repeated optional data for the first leg.
|
||||
boarding_pass.legs[0].add_optional(optional_data)?;
|
||||
|
||||
// Now any additional legs(!)
|
||||
for _legnum in 1..leg_count {
|
||||
let (mut leg, leg_bytes) = BoardingPassLeg::parse_mandatory(&data[p..], nearby_date)?;
|
||||
p += leg_bytes;
|
||||
|
||||
let optional_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
|
||||
let optional_data = &data[p + 2..p + 2 + optional_data_size];
|
||||
p += 2 + optional_data_size;
|
||||
|
||||
leg.add_optional(optional_data)?;
|
||||
boarding_pass.legs.push(leg);
|
||||
}
|
||||
|
||||
// What's left is the security data.
|
||||
if data[p] != b'^' {
|
||||
return Err(anyhow!(
|
||||
"expected security data beginning indicator (^) but got {} ({})",
|
||||
data[p],
|
||||
data[p] as char
|
||||
));
|
||||
}
|
||||
boarding_pass.security_data_type = data[p + 1];
|
||||
let security_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p + 2..p + 4])?, 16)?;
|
||||
boarding_pass.security_data = data[p + 4..p + 4 + security_data_size].to_vec();
|
||||
if data.len() > p + 4 + security_data_size {
|
||||
return Err(anyhow!(
|
||||
"trailing bytes after security data (expected {} bytes of security data)",
|
||||
security_data_size
|
||||
));
|
||||
}
|
||||
|
||||
Ok(boarding_pass)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BoardingPassLeg {
|
||||
operating_carrier_pnr: String, // 7, left justified w/ trailing blanks
|
||||
origin_airport: String, // 3
|
||||
destination_airport: String, // 3
|
||||
operating_carrier: String, // 3, left justified w/ trailing blanks
|
||||
flight_number: String, // 5, NNNN[a] (leading zeros + alpha/blank on last digit)
|
||||
date_of_flight: NaiveDate, // 3, leading zeros (this is the day in the year e.g. 001 = Jan 1st,
|
||||
// 365 == Dec 31st)
|
||||
compartment_code: char,
|
||||
seat_number: String, // 4, NNNa (leading zeros)
|
||||
check_in_sequence: String, // NNNN[f] (leading zeros + alpha/blank on last digit)
|
||||
passenger_status: PassengerStatus,
|
||||
|
||||
airline_numeric_code: Option<String>,
|
||||
document_serial_number: Option<String>,
|
||||
selectee_indicator: Option<char>,
|
||||
documentation_verification: Option<DocumentationVerificationStatus>,
|
||||
marketing_carrier: Option<String>,
|
||||
frequent_flyer_airline: Option<String>,
|
||||
frequent_flyer_number: Option<String>,
|
||||
id_ad_indicator: Option<char>,
|
||||
free_baggage_allowance: Option<String>,
|
||||
airline_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl BoardingPassLeg {
|
||||
fn parse_mandatory(data: &[u8], nearby_date: &NaiveDate) -> Result<(BoardingPassLeg, usize)> {
|
||||
let operating_carrier_pnr: String =
|
||||
std::str::from_utf8(&data[0..7])?.trim_end().to_string();
|
||||
let origin_airport: String = std::str::from_utf8(&data[7..10])?.to_string();
|
||||
let destination_airport: String = std::str::from_utf8(&data[10..13])?.to_string();
|
||||
let operating_carrier: String = std::str::from_utf8(&data[13..16])?.trim_end().to_string();
|
||||
let flight_number: String = std::str::from_utf8(&data[16..21])?
|
||||
.trim_end()
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let date_of_flight_str: &str = std::str::from_utf8(&data[21..24])?;
|
||||
let date_of_flight: NaiveDate = parse_boarding_pass_date(date_of_flight_str, nearby_date)?;
|
||||
let compartment_code: char = data[24] as char;
|
||||
let seat_number: String = std::str::from_utf8(&data[25..29])?
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let check_in_sequence: String = std::str::from_utf8(&data[29..34])?
|
||||
.trim_end()
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let passenger_status: PassengerStatus = PassengerStatus::from(data[34])?;
|
||||
Ok((
|
||||
BoardingPassLeg {
|
||||
operating_carrier_pnr,
|
||||
origin_airport,
|
||||
destination_airport,
|
||||
operating_carrier,
|
||||
flight_number,
|
||||
date_of_flight,
|
||||
compartment_code,
|
||||
seat_number,
|
||||
check_in_sequence,
|
||||
passenger_status,
|
||||
|
||||
airline_numeric_code: None,
|
||||
document_serial_number: None,
|
||||
selectee_indicator: None,
|
||||
documentation_verification: None,
|
||||
marketing_carrier: None,
|
||||
frequent_flyer_airline: None,
|
||||
frequent_flyer_number: None,
|
||||
id_ad_indicator: None,
|
||||
free_baggage_allowance: None,
|
||||
airline_data: None,
|
||||
},
|
||||
35,
|
||||
))
|
||||
}
|
||||
|
||||
fn add_optional(&mut self, data: &[u8]) -> Result<()> {
|
||||
let structured_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[0..2])?, 16)?;
|
||||
let mut structured_data = &data[2..structured_data_size + 2];
|
||||
|
||||
if structured_data.len() >= 3 {
|
||||
let airline_code_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.airline_numeric_code = if airline_code_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(airline_code_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 10 {
|
||||
let document_serial_number_str =
|
||||
std::str::from_utf8(&structured_data[0..10])?.trim_end();
|
||||
self.document_serial_number = if document_serial_number_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(document_serial_number_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[10..];
|
||||
}
|
||||
if structured_data.len() >= 1 {
|
||||
self.selectee_indicator = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(structured_data[0] as char)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if structured_data.len() >= 1 {
|
||||
self.documentation_verification = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(DocumentationVerificationStatus::from(structured_data[0])?)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let marketing_carrier_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.marketing_carrier = if marketing_carrier_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(marketing_carrier_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let frequent_flyer_airline_str =
|
||||
std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.frequent_flyer_airline = if frequent_flyer_airline_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(frequent_flyer_airline_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 16 {
|
||||
let frequent_flyer_number_str =
|
||||
std::str::from_utf8(&structured_data[0..16])?.trim_end();
|
||||
self.frequent_flyer_number = if frequent_flyer_number_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(frequent_flyer_number_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[16..];
|
||||
}
|
||||
if structured_data.len() >= 1 {
|
||||
self.id_ad_indicator = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(structured_data[0] as char)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let free_baggage_allowance_str =
|
||||
std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.free_baggage_allowance = if free_baggage_allowance_str == "" {
|
||||
None
|
||||
} else {
|
||||
Some(free_baggage_allowance_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() != 0 {
|
||||
return Err(anyhow!(
|
||||
"trailing data in structured data section ({} bytes)",
|
||||
structured_data.len()
|
||||
));
|
||||
}
|
||||
|
||||
let unstructured_data = &data[structured_data_size + 2..];
|
||||
if unstructured_data.len() > 0 {
|
||||
self.airline_data = Some(unstructured_data.to_vec());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -839,17 +94,14 @@ async fn main() -> Result<()> {
|
|||
|
||||
let command = cli.command.unwrap();
|
||||
|
||||
match &command {
|
||||
Commands::Scan { image } => {
|
||||
println!("{}", scan(&image)?);
|
||||
if let Commands::Scan { image } = &command {
|
||||
println!("{}", scan(image)?);
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let service_account_file = cli
|
||||
.service_account_file
|
||||
.ok_or(anyhow!("--service-account-file must be set"))?;
|
||||
.ok_or_else(|| anyhow!("--service-account-file must be set"))?;
|
||||
let sa = load_service_account(&service_account_file).await?;
|
||||
|
||||
let hub = google_walletobjects1::Walletobjects::new(
|
||||
|
@ -861,7 +113,7 @@ async fn main() -> Result<()> {
|
|||
.enable_http2()
|
||||
.build(),
|
||||
),
|
||||
sa.authenticator,
|
||||
sa.authenticator.clone(),
|
||||
);
|
||||
|
||||
match &command {
|
||||
|
@ -900,49 +152,27 @@ async fn main() -> Result<()> {
|
|||
class.review_status = Some("UNDER_REVIEW".to_string());
|
||||
|
||||
// Try to create the class first...
|
||||
let created_class: bool = hub.flightclass().insert_now(&class).await?;
|
||||
if !created_class {
|
||||
// Already exists.
|
||||
hub.flightclass().update_now(&class).await?;
|
||||
}
|
||||
insert_or_update_now(&hub.flightclass(), &class).await?;
|
||||
|
||||
// Then the pass...
|
||||
let created_pass: bool = hub.flightobject().insert_now(&pass).await?;
|
||||
if !created_pass {
|
||||
// Already exists.
|
||||
hub.flightobject().update_now(&pass).await?;
|
||||
}
|
||||
insert_or_update_now(&hub.flightobject(), &pass).await?;
|
||||
|
||||
// Now generate the URL.
|
||||
let claims: JWTClaims<AddToWalletClaims> = JWTClaims {
|
||||
issued_at: None,
|
||||
expires_at: None,
|
||||
invalid_before: None,
|
||||
audiences: Some(jwt_simple::claims::Audiences::AsString(
|
||||
"google".to_string(),
|
||||
)),
|
||||
issuer: Some(sa.service_account_name),
|
||||
jwt_id: None,
|
||||
subject: None,
|
||||
nonce: None,
|
||||
custom: AddToWalletClaims {
|
||||
jwt_type: "savetowallet".to_string(),
|
||||
origins: vec!["www.lukegb.com".to_string()],
|
||||
payload: AddToWalletPayload {
|
||||
flight_classes: vec![AddToWalletPayloadObject {
|
||||
id: class.id.clone().unwrap(),
|
||||
class_id: None,
|
||||
}],
|
||||
flight_objects: vec![AddToWalletPayloadObject {
|
||||
id: pass.id.unwrap(),
|
||||
class_id: Some(class.id.unwrap()),
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
let token = sa.key_pair.sign(claims)?;
|
||||
println!(
|
||||
"{}",
|
||||
AddToWallet {
|
||||
service_account_email: sa.service_account_name.to_string(),
|
||||
|
||||
println!("https://pay.google.com/gp/v/save/{}", token);
|
||||
flight_classes: vec![PassClassIdentifier {
|
||||
id: class.id.clone().unwrap(),
|
||||
}],
|
||||
flight_passes: vec![PassIdentifier {
|
||||
id: pass.id.unwrap(),
|
||||
class_id: class.id.unwrap(),
|
||||
}],
|
||||
}
|
||||
.to_url(&sa)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -950,88 +180,15 @@ async fn main() -> Result<()> {
|
|||
let scanned = scan(&upload_scan.image)?;
|
||||
let boarding_pass =
|
||||
BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?;
|
||||
if boarding_pass.legs.len() > 1 {
|
||||
return Err(anyhow!("multi-leg trips aren't supported yet"));
|
||||
}
|
||||
let leg = &boarding_pass.legs[0];
|
||||
let airline_data = AIRLINE_DATA.get(&leg.operating_carrier).ok_or(anyhow!(
|
||||
"no AIRLINE_DATA has been provided for {} yet",
|
||||
leg.operating_carrier
|
||||
))?;
|
||||
let (classes, passes) = passes_from_barcode(&cli.issuer_id, boarding_pass)?;
|
||||
|
||||
let id_prefix = format!(
|
||||
"{}.{}{}-{}-{}{}",
|
||||
cli.issuer_id,
|
||||
leg.operating_carrier,
|
||||
leg.flight_number,
|
||||
leg.date_of_flight,
|
||||
leg.origin_airport,
|
||||
leg.destination_airport
|
||||
);
|
||||
let class_id = &id_prefix;
|
||||
let pass_id = format!("{}-{}", &id_prefix, leg.check_in_sequence);
|
||||
// Create all the classes.
|
||||
insert_or_update_many(&hub.flightclass(), classes).await?;
|
||||
|
||||
let mut class = FlightClass::default();
|
||||
class.id = Some(class_id.clone());
|
||||
class.issuer_name = Some(airline_data.name.to_string());
|
||||
class.hex_background_color =
|
||||
Some(airline_data.boarding_pass_background_colour.to_string());
|
||||
class.review_status = Some("UNDER_REVIEW".to_string());
|
||||
class.boarding_and_seating_policy =
|
||||
Some(google_walletobjects1::api::BoardingAndSeatingPolicy {
|
||||
boarding_policy: Some(airline_data.boarding_policy.to_string()),
|
||||
seat_class_policy: Some(airline_data.seat_class_policy.to_string()),
|
||||
kind: None,
|
||||
});
|
||||
class.origin = Some(airport(&leg.origin_airport));
|
||||
class.destination = Some(airport(&leg.destination_airport));
|
||||
let mut flight_header = google_walletobjects1::api::FlightHeader::default();
|
||||
let mut flight_header_carrier = google_walletobjects1::api::FlightCarrier::default();
|
||||
flight_header_carrier.airline_alliance_logo =
|
||||
airline_data.alliance_logo_url.map(url_to_image);
|
||||
flight_header_carrier.airline_logo = Some(url_to_image(airline_data.logo_url));
|
||||
flight_header_carrier.airline_name =
|
||||
Some(to_localized_string("en-GB", airline_data.name));
|
||||
flight_header_carrier.carrier_iata_code = Some(leg.operating_carrier.to_string());
|
||||
flight_header.carrier = Some(flight_header_carrier);
|
||||
flight_header.flight_number = Some(leg.flight_number.to_string());
|
||||
class.flight_header = Some(flight_header);
|
||||
|
||||
let mut pass = FlightObject::default();
|
||||
pass.id = Some(pass_id.to_string());
|
||||
pass.class_id = Some(class_id.clone());
|
||||
// Create all the objects.
|
||||
insert_or_update_many(&hub.flightobject(), passes).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn airport(iata_code: &str) -> google_walletobjects1::api::AirportInfo {
|
||||
let mut info = google_walletobjects1::api::AirportInfo::default();
|
||||
info.airport_iata_code = Some(iata_code.to_string());
|
||||
info
|
||||
}
|
||||
|
||||
fn to_localized_string(locale: &str, s: &str) -> google_walletobjects1::api::LocalizedString {
|
||||
google_walletobjects1::api::LocalizedString {
|
||||
default_value: Some(google_walletobjects1::api::TranslatedString {
|
||||
kind: None,
|
||||
language: Some(locale.to_string()),
|
||||
value: Some(s.to_string()),
|
||||
}),
|
||||
kind: None,
|
||||
translated_values: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn url_to_image(url: &str) -> google_walletobjects1::api::Image {
|
||||
google_walletobjects1::api::Image {
|
||||
kind: None,
|
||||
content_description: None,
|
||||
source_uri: Some(google_walletobjects1::api::ImageUri {
|
||||
description: None,
|
||||
localized_description: None,
|
||||
uri: Some(url.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
723
rust/passgen/src/resolution792.rs
Normal file
723
rust/passgen/src/resolution792.rs
Normal file
|
@ -0,0 +1,723 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use chrono::naive::NaiveDate;
|
||||
use chrono::Datelike;
|
||||
use std::cmp::min;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
pub enum PassengerStatus {
|
||||
NotCheckedIn = b'0',
|
||||
CheckedIn = b'1',
|
||||
BagsCheckedAndNotCheckedIn = b'2',
|
||||
BagsCheckedAndCheckedIn = b'3',
|
||||
PastSecurity = b'4',
|
||||
PastGate = b'5',
|
||||
Transit = b'6',
|
||||
Standby = b'7',
|
||||
BoardingDataRevalidated = b'8',
|
||||
OriginalBoardingLineUsedWhenTicketIssued = b'9',
|
||||
UpOrDownGradingPending = b'A',
|
||||
}
|
||||
|
||||
impl PassengerStatus {
|
||||
fn from(c: u8) -> Result<PassengerStatus> {
|
||||
for v in PassengerStatus::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid PassengerStatus value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
pub enum DocumentationVerificationStatus {
|
||||
NotRequired = b'0',
|
||||
Required = b'1',
|
||||
Performed = b'2',
|
||||
}
|
||||
|
||||
impl DocumentationVerificationStatus {
|
||||
fn from(c: u8) -> Result<DocumentationVerificationStatus> {
|
||||
for v in DocumentationVerificationStatus::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid DocumentationVerificationStatus value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
pub enum PassengerDescription {
|
||||
Adult = b'0',
|
||||
Male = b'1',
|
||||
Female = b'2',
|
||||
Child = b'3',
|
||||
Infant = b'4',
|
||||
NoPassenger = b'5', // cabin baggage
|
||||
AdultWithInfant = b'6',
|
||||
UnaccompaniedMinor = b'7',
|
||||
}
|
||||
|
||||
impl PassengerDescription {
|
||||
fn from(c: u8) -> Result<PassengerDescription> {
|
||||
for v in PassengerDescription::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid PassengerDescription value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, EnumIter)]
|
||||
#[repr(u8)]
|
||||
pub enum CheckInSource {
|
||||
Web = b'W',
|
||||
AirportKiosk = b'K',
|
||||
RemoteKiosk = b'R',
|
||||
MobileDevice = b'M',
|
||||
AirportAgent = b'O',
|
||||
TownAgent = b'T',
|
||||
ThirdPartyVendor = b'V',
|
||||
}
|
||||
|
||||
impl CheckInSource {
|
||||
fn from(c: u8) -> Result<CheckInSource> {
|
||||
for v in CheckInSource::iter() {
|
||||
if v as u8 == c {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"{} ({}) is not a valid CheckInSource value",
|
||||
c,
|
||||
c as char
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoardingPass {
|
||||
pub passenger_name: String,
|
||||
pub eticket_indicator: char,
|
||||
|
||||
pub version_number: Option<char>,
|
||||
pub passenger_description: Option<PassengerDescription>,
|
||||
pub checkin_source: Option<CheckInSource>,
|
||||
pub boarding_pass_source: Option<CheckInSource>,
|
||||
pub date_of_issue: Option<NaiveDate>,
|
||||
pub document_type: Option<char>,
|
||||
pub boarding_pass_issuing_airline: Option<String>,
|
||||
pub baggage_tag_plate_number: Option<String>,
|
||||
|
||||
pub legs: Vec<BoardingPassLeg>,
|
||||
|
||||
pub security_data_type: u8,
|
||||
pub security_data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn parse_boarding_pass_date(
|
||||
from_boarding_pass_str: &str,
|
||||
nearby_date: &NaiveDate,
|
||||
) -> Result<NaiveDate> {
|
||||
// Range is 0 to 9999 (although not really).
|
||||
let from_boarding_pass: u16 = from_boarding_pass_str.parse()?;
|
||||
|
||||
match from_boarding_pass_str.len() {
|
||||
4 => {
|
||||
// We will look 5 years into the past and 4 years into the future.
|
||||
// e.g. if the current year is 2025, we will use 2020-2029.
|
||||
let year_digit = (from_boarding_pass / 1000) as i32;
|
||||
let nearby_year_digit = nearby_date.year() % 10;
|
||||
let mut year = nearby_date.year() - nearby_year_digit + year_digit;
|
||||
let diff = nearby_date.year() - year;
|
||||
|
||||
if diff > 5 {
|
||||
year += 10;
|
||||
} else if diff < -4 {
|
||||
year -= 10;
|
||||
}
|
||||
|
||||
return NaiveDate::from_yo_opt(year, (from_boarding_pass % 1000).into())
|
||||
.ok_or_else(|| anyhow!("could not parse date {}", from_boarding_pass_str));
|
||||
}
|
||||
3 => {
|
||||
// Find the one that's the fewest days away.
|
||||
let year = nearby_date.year();
|
||||
let day: u32 = (from_boarding_pass % 1000).into();
|
||||
let mut options = vec![
|
||||
NaiveDate::from_yo_opt(year, day),
|
||||
NaiveDate::from_yo_opt(year - 1, day),
|
||||
NaiveDate::from_yo_opt(year + 1, day),
|
||||
];
|
||||
options.sort_by(|a, b| {
|
||||
days_between(nearby_date, a)
|
||||
.unwrap_or(9999)
|
||||
.cmp(&days_between(nearby_date, b).unwrap_or(9999))
|
||||
});
|
||||
Ok(options[0].unwrap())
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"boarding pass date {} is wrong length",
|
||||
from_boarding_pass_str
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn days_between(nearby_date: &NaiveDate, maybe_date: &Option<NaiveDate>) -> Option<u64> {
|
||||
if maybe_date.is_none() {
|
||||
return None;
|
||||
}
|
||||
let is_date = maybe_date.as_ref().unwrap();
|
||||
let days_between = (*nearby_date - *is_date).num_days();
|
||||
Some(if days_between < 0 {
|
||||
(-days_between) as u64
|
||||
} else {
|
||||
days_between as u64
|
||||
})
|
||||
}
|
||||
|
||||
impl BoardingPass {
|
||||
pub fn parse(data: Vec<u8>, nearby_date: &NaiveDate) -> Result<BoardingPass> {
|
||||
if data[0] != b'M' {
|
||||
return Err(anyhow!(
|
||||
"format code was {} ({}), not M",
|
||||
data[0] as char,
|
||||
data[0]
|
||||
));
|
||||
}
|
||||
let leg_count = match data[1] {
|
||||
b'1' => Ok(1),
|
||||
b'2' => Ok(2),
|
||||
b'3' => Ok(3),
|
||||
b'4' => Ok(4),
|
||||
_ => Err(anyhow!(
|
||||
"leg count was {} ({}), should be 1 to 4",
|
||||
data[1] as char,
|
||||
data[1]
|
||||
)),
|
||||
}?;
|
||||
|
||||
let passenger_name: String = std::str::from_utf8(&data[2..22])?.trim_end().to_string();
|
||||
let eticket_indicator: char = data[22] as char;
|
||||
|
||||
// First leg mandatory data:
|
||||
let (first_leg, mut p) = BoardingPassLeg::parse_mandatory(&data[23..], nearby_date)?;
|
||||
p += 23;
|
||||
|
||||
// We now have enough to start composing the boardingpass.
|
||||
let mut boarding_pass = BoardingPass {
|
||||
passenger_name,
|
||||
eticket_indicator,
|
||||
|
||||
version_number: None,
|
||||
passenger_description: None,
|
||||
checkin_source: None,
|
||||
boarding_pass_source: None,
|
||||
date_of_issue: None,
|
||||
document_type: None,
|
||||
boarding_pass_issuing_airline: None,
|
||||
baggage_tag_plate_number: None,
|
||||
|
||||
legs: vec![first_leg],
|
||||
|
||||
security_data_type: 0xff,
|
||||
security_data: vec![],
|
||||
};
|
||||
|
||||
// Header + first leg optional data (why, IATA, why)
|
||||
let optional_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
|
||||
let mut optional_data = &data[p + 2..p + 2 + optional_data_size];
|
||||
p += 2 + optional_data_size;
|
||||
if !optional_data.is_empty() {
|
||||
if optional_data[0] != b'>' {
|
||||
return Err(anyhow!("expected Beginning of version number ('>') at beginning of conditional fields, got {} ({})", optional_data[0], optional_data[0] as char));
|
||||
}
|
||||
optional_data = &optional_data[1..];
|
||||
}
|
||||
if !optional_data.is_empty() {
|
||||
boarding_pass.version_number = Some(optional_data[0] as char);
|
||||
optional_data = &optional_data[1..];
|
||||
}
|
||||
if !optional_data.is_empty() {
|
||||
let optional_data_unique_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&optional_data[0..2])?, 16)?;
|
||||
let mut optional_data_unique = &optional_data[2..optional_data_unique_size + 2];
|
||||
if !optional_data_unique.is_empty() {
|
||||
boarding_pass.passenger_description = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(PassengerDescription::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if !optional_data_unique.is_empty() {
|
||||
boarding_pass.checkin_source = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(CheckInSource::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if !optional_data_unique.is_empty() {
|
||||
boarding_pass.boarding_pass_source = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(CheckInSource::from(optional_data_unique[0])?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 4 {
|
||||
let date_of_issue_str = std::str::from_utf8(&optional_data_unique[0..4])?;
|
||||
boarding_pass.date_of_issue = if date_of_issue_str == " " {
|
||||
None
|
||||
} else {
|
||||
Some(parse_boarding_pass_date(date_of_issue_str, nearby_date)?)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[4..];
|
||||
}
|
||||
// TODO: the above might fall through if we don't have 4 bytes of data
|
||||
if !optional_data_unique.is_empty() {
|
||||
boarding_pass.document_type = if optional_data_unique[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(optional_data_unique[0] as char)
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[1..];
|
||||
}
|
||||
if optional_data_unique.len() >= 3 {
|
||||
let issuing_airline_str =
|
||||
std::str::from_utf8(&optional_data_unique[0..3])?.trim_end();
|
||||
boarding_pass.boarding_pass_issuing_airline = if issuing_airline_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(issuing_airline_str.to_string())
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[3..];
|
||||
}
|
||||
// TODO: the above might fall through if we don't have 3 bytes of data
|
||||
let bag_tags_str_len = min(13, optional_data_unique.len());
|
||||
let bag_tags_str =
|
||||
std::str::from_utf8(&optional_data_unique[0..bag_tags_str_len])?.trim_end();
|
||||
boarding_pass.baggage_tag_plate_number = if bag_tags_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(bag_tags_str.to_string())
|
||||
};
|
||||
optional_data_unique = &optional_data_unique[bag_tags_str_len..];
|
||||
if !optional_data_unique.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"trailing unique optional data ({} bytes)",
|
||||
optional_data_unique.len()
|
||||
));
|
||||
}
|
||||
optional_data = &optional_data[optional_data_unique_size + 2..];
|
||||
}
|
||||
|
||||
// Now for the repeated optional data for the first leg.
|
||||
boarding_pass.legs[0].add_optional(optional_data)?;
|
||||
|
||||
// Now any additional legs(!)
|
||||
for _legnum in 1..leg_count {
|
||||
let (mut leg, leg_bytes) = BoardingPassLeg::parse_mandatory(&data[p..], nearby_date)?;
|
||||
p += leg_bytes;
|
||||
|
||||
let optional_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p..p + 2])?, 16)?;
|
||||
let optional_data = &data[p + 2..p + 2 + optional_data_size];
|
||||
p += 2 + optional_data_size;
|
||||
|
||||
leg.add_optional(optional_data)?;
|
||||
boarding_pass.legs.push(leg);
|
||||
}
|
||||
|
||||
// What's left is the security data.
|
||||
if data[p] != b'^' {
|
||||
return Err(anyhow!(
|
||||
"expected security data beginning indicator (^) but got {} ({})",
|
||||
data[p],
|
||||
data[p] as char
|
||||
));
|
||||
}
|
||||
boarding_pass.security_data_type = data[p + 1];
|
||||
let security_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[p + 2..p + 4])?, 16)?;
|
||||
boarding_pass.security_data = data[p + 4..p + 4 + security_data_size].to_vec();
|
||||
if data.len() > p + 4 + security_data_size {
|
||||
return Err(anyhow!(
|
||||
"trailing bytes after security data (expected {} bytes of security data)",
|
||||
security_data_size
|
||||
));
|
||||
}
|
||||
|
||||
Ok(boarding_pass)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoardingPassLeg {
|
||||
pub operating_carrier_pnr: String, // 7, left justified w/ trailing blanks
|
||||
pub origin_airport: String, // 3
|
||||
pub destination_airport: String, // 3
|
||||
pub operating_carrier: String, // 3, left justified w/ trailing blanks
|
||||
pub flight_number: String, // 5, NNNN[a] (leading zeros + alpha/blank on last digit)
|
||||
pub date_of_flight: NaiveDate, // 3, leading zeros (this is the day in the year e.g. 001 = Jan 1st,
|
||||
// 365 == Dec 31st)
|
||||
pub compartment_code: char,
|
||||
pub seat_number: String, // 4, NNNa (leading zeros)
|
||||
pub check_in_sequence: String, // NNNN[f] (leading zeros + alpha/blank on last digit)
|
||||
pub passenger_status: PassengerStatus,
|
||||
|
||||
pub airline_numeric_code: Option<String>,
|
||||
pub document_serial_number: Option<String>,
|
||||
pub selectee_indicator: Option<char>,
|
||||
pub documentation_verification: Option<DocumentationVerificationStatus>,
|
||||
pub marketing_carrier: Option<String>,
|
||||
pub frequent_flyer_airline: Option<String>,
|
||||
pub frequent_flyer_number: Option<String>,
|
||||
pub id_ad_indicator: Option<char>,
|
||||
pub free_baggage_allowance: Option<String>,
|
||||
pub airline_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl BoardingPassLeg {
|
||||
fn parse_mandatory(data: &[u8], nearby_date: &NaiveDate) -> Result<(BoardingPassLeg, usize)> {
|
||||
let operating_carrier_pnr: String =
|
||||
std::str::from_utf8(&data[0..7])?.trim_end().to_string();
|
||||
let origin_airport: String = std::str::from_utf8(&data[7..10])?.to_string();
|
||||
let destination_airport: String = std::str::from_utf8(&data[10..13])?.to_string();
|
||||
let operating_carrier: String = std::str::from_utf8(&data[13..16])?.trim_end().to_string();
|
||||
let flight_number: String = std::str::from_utf8(&data[16..21])?
|
||||
.trim_end()
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let date_of_flight_str: &str = std::str::from_utf8(&data[21..24])?;
|
||||
let date_of_flight: NaiveDate = parse_boarding_pass_date(date_of_flight_str, nearby_date)?;
|
||||
let compartment_code: char = data[24] as char;
|
||||
let seat_number: String = std::str::from_utf8(&data[25..29])?
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let check_in_sequence: String = std::str::from_utf8(&data[29..34])?
|
||||
.trim_end()
|
||||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
let passenger_status: PassengerStatus = PassengerStatus::from(data[34])?;
|
||||
Ok((
|
||||
BoardingPassLeg {
|
||||
operating_carrier_pnr,
|
||||
origin_airport,
|
||||
destination_airport,
|
||||
operating_carrier,
|
||||
flight_number,
|
||||
date_of_flight,
|
||||
compartment_code,
|
||||
seat_number,
|
||||
check_in_sequence,
|
||||
passenger_status,
|
||||
|
||||
airline_numeric_code: None,
|
||||
document_serial_number: None,
|
||||
selectee_indicator: None,
|
||||
documentation_verification: None,
|
||||
marketing_carrier: None,
|
||||
frequent_flyer_airline: None,
|
||||
frequent_flyer_number: None,
|
||||
id_ad_indicator: None,
|
||||
free_baggage_allowance: None,
|
||||
airline_data: None,
|
||||
},
|
||||
35,
|
||||
))
|
||||
}
|
||||
|
||||
fn add_optional(&mut self, data: &[u8]) -> Result<()> {
|
||||
let structured_data_size: usize =
|
||||
usize::from_str_radix(std::str::from_utf8(&data[0..2])?, 16)?;
|
||||
let mut structured_data = &data[2..structured_data_size + 2];
|
||||
|
||||
if structured_data.len() >= 3 {
|
||||
let airline_code_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.airline_numeric_code = if airline_code_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(airline_code_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 10 {
|
||||
let document_serial_number_str =
|
||||
std::str::from_utf8(&structured_data[0..10])?.trim_end();
|
||||
self.document_serial_number = if document_serial_number_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(document_serial_number_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[10..];
|
||||
}
|
||||
if !structured_data.is_empty() {
|
||||
self.selectee_indicator = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(structured_data[0] as char)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if !structured_data.is_empty() {
|
||||
self.documentation_verification = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(DocumentationVerificationStatus::from(structured_data[0])?)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let marketing_carrier_str = std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.marketing_carrier = if marketing_carrier_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(marketing_carrier_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let frequent_flyer_airline_str =
|
||||
std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.frequent_flyer_airline = if frequent_flyer_airline_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(frequent_flyer_airline_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if structured_data.len() >= 16 {
|
||||
let frequent_flyer_number_str =
|
||||
std::str::from_utf8(&structured_data[0..16])?.trim_end();
|
||||
self.frequent_flyer_number = if frequent_flyer_number_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(frequent_flyer_number_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[16..];
|
||||
}
|
||||
if !structured_data.is_empty() {
|
||||
self.id_ad_indicator = if structured_data[0] == b' ' {
|
||||
None
|
||||
} else {
|
||||
Some(structured_data[0] as char)
|
||||
};
|
||||
structured_data = &structured_data[1..];
|
||||
}
|
||||
if structured_data.len() >= 3 {
|
||||
let free_baggage_allowance_str =
|
||||
std::str::from_utf8(&structured_data[0..3])?.trim_end();
|
||||
self.free_baggage_allowance = if free_baggage_allowance_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(free_baggage_allowance_str.to_string())
|
||||
};
|
||||
structured_data = &structured_data[3..];
|
||||
}
|
||||
if !structured_data.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"trailing data in structured data section ({} bytes)",
|
||||
structured_data.len()
|
||||
));
|
||||
}
|
||||
|
||||
let unstructured_data = &data[structured_data_size + 2..];
|
||||
if !unstructured_data.is_empty() {
|
||||
self.airline_data = Some(unstructured_data.to_vec());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_boarding_pass_date_yearless_end_of_year() {
|
||||
let jan01st2022 = NaiveDate::from_ymd_opt(2022, 1, 1).unwrap();
|
||||
let dec31st2021 = NaiveDate::from_ymd_opt(2021, 12, 31).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("001", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("001", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("030", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 30).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("030", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 30).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("090", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 3, 31).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("090", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 3, 31).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("180", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 6, 29).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("180", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 6, 29).unwrap()
|
||||
);
|
||||
|
||||
// We snap back to 2021.
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("200", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2021, 7, 19).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("200", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2021, 7, 19).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("365", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("365", &dec31st2021).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boarding_pass_date_yearless_mid_year() {
|
||||
let jun15th2022 = NaiveDate::from_ymd_opt(2022, 6, 15).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("001", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("030", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 30).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("090", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 3, 31).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("180", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 6, 29).unwrap()
|
||||
);
|
||||
|
||||
// We snap back to 2021.
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("200", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 7, 19).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("365", &jun15th2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boarding_pass_date_yearful() {
|
||||
let jan01st2020 = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
|
||||
let jan01st2022 = NaiveDate::from_ymd_opt(2022, 1, 1).unwrap();
|
||||
let jan01st2025 = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
|
||||
let jan01st2029 = NaiveDate::from_ymd_opt(2029, 1, 1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("0001", &jan01st2020).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("0001", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("0001", &jan01st2025).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("0001", &jan01st2029).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2030, 1, 1).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("2001", &jan01st2020).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("2001", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("2001", &jan01st2025).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("2001", &jan01st2029).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2032, 1, 1).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("5001", &jan01st2020).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2015, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("5001", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("5001", &jan01st2025).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("5001", &jan01st2029).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("9001", &jan01st2020).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2019, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("9001", &jan01st2022).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2019, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("9001", &jan01st2025).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2029, 1, 1).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
parse_boarding_pass_date("9001", &jan01st2029).unwrap(),
|
||||
NaiveDate::from_ymd_opt(2029, 1, 1).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
18
rust/passgen/src/scanner.rs
Normal file
18
rust/passgen/src/scanner.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
pub fn scan(image_path: &PathBuf) -> Result<String> {
|
||||
let image_path_str = (*image_path)
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("invalid path"))?;
|
||||
rxing::helpers::detect_in_file(image_path_str, Some(rxing::BarcodeFormat::AZTEC))
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"could not parse Aztec barcode from image {}: {}",
|
||||
image_path_str,
|
||||
e
|
||||
)
|
||||
})
|
||||
.map(|r| r.getText().clone())
|
||||
}
|
42
rust/passgen/src/service_account.rs
Normal file
42
rust/passgen/src/service_account.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use google_walletobjects1::oauth2::authenticator::{
|
||||
Authenticator, DefaultHyperClient, HyperClientBuilder,
|
||||
};
|
||||
use google_walletobjects1::oauth2::{read_service_account_key, ServiceAccountAuthenticator};
|
||||
use jwt_simple::algorithms::RS256KeyPair;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
pub struct ServiceAccount {
|
||||
pub service_account_name: String,
|
||||
pub authenticator: Authenticator<<DefaultHyperClient as HyperClientBuilder>::Connector>,
|
||||
pub key_pair: RS256KeyPair,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ServiceAccountJSON {
|
||||
// We omit a lot of fields we don't care about.
|
||||
/// Service Account email
|
||||
client_email: String,
|
||||
|
||||
/// RSA private key
|
||||
private_key: String,
|
||||
}
|
||||
|
||||
pub async fn load_service_account(path: &PathBuf) -> Result<ServiceAccount> {
|
||||
let creds = read_service_account_key(path).await?;
|
||||
let sa = ServiceAccountAuthenticator::builder(creds).build().await?;
|
||||
|
||||
let sa_data = tokio::fs::read(path).await?;
|
||||
let sa_parsed: ServiceAccountJSON = serde_json::from_slice(&sa_data[..])?;
|
||||
let key_pair = RS256KeyPair::from_pem(&sa_parsed.private_key)?;
|
||||
|
||||
Ok(ServiceAccount {
|
||||
service_account_name: sa_parsed.client_email,
|
||||
authenticator: sa,
|
||||
key_pair,
|
||||
})
|
||||
}
|
37
rust/passgen/src/static_data.rs
Normal file
37
rust/passgen/src/static_data.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use phf::phf_map;
|
||||
|
||||
pub struct AirlineDefinition<'a> {
|
||||
pub iata_code: &'a str,
|
||||
pub name: &'a str,
|
||||
pub boarding_policy: &'a str,
|
||||
pub seat_class_policy: &'a str,
|
||||
|
||||
pub boarding_pass_background_colour: &'a str,
|
||||
|
||||
pub frequent_flyer_program_name: Option<&'a str>,
|
||||
|
||||
pub logo_url: &'a str,
|
||||
pub alliance_logo_url: Option<&'a str>,
|
||||
pub hero_image_logo_url: Option<&'a str>,
|
||||
pub boarding_privilege_logo_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub const TSA_PRECHECK_LOGO: &str = "https://p.lukegb.com/raw/MiserablyDirectPiglet.jpg";
|
||||
|
||||
pub static AIRLINE_DATA: phf::Map<&'static str, AirlineDefinition<'static>> = phf_map! {
|
||||
"VS" => AirlineDefinition{
|
||||
iata_code: "VS",
|
||||
name: "Virgin Atlantic",
|
||||
boarding_policy: "GROUP_BASED",
|
||||
seat_class_policy: "CABIN_BASED",
|
||||
|
||||
boarding_pass_background_colour: "#4f145b",
|
||||
|
||||
frequent_flyer_program_name: Some("Flying Club"),
|
||||
|
||||
logo_url: "https://p.lukegb.com/raw/VirtuallyCrispViper.png",
|
||||
alliance_logo_url: None,
|
||||
hero_image_logo_url: Some("https://p.lukegb.com/raw/FormerlyDistinctToad.png"),
|
||||
boarding_privilege_logo_url: Some("https://p.lukegb.com/raw/DefinitelyVerifiedTitmouse.png"),
|
||||
},
|
||||
};
|
94
rust/passgen/src/walletobjects.rs
Normal file
94
rust/passgen/src/walletobjects.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
pub use google_walletobjects1::api::*;
|
||||
|
||||
use crate::resolution792::BoardingPass;
|
||||
use crate::static_data::AIRLINE_DATA;
|
||||
|
||||
pub fn passes_from_barcode(
|
||||
issuer_id: &str,
|
||||
boarding_pass: BoardingPass,
|
||||
) -> Result<(Vec<FlightClass>, Vec<FlightObject>)> {
|
||||
if boarding_pass.legs.len() > 1 {
|
||||
return Err(anyhow!("multi-leg trips aren't supported yet"));
|
||||
}
|
||||
let leg = &boarding_pass.legs[0];
|
||||
let airline_data = AIRLINE_DATA.get(&leg.operating_carrier).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"no AIRLINE_DATA has been provided for {} yet",
|
||||
leg.operating_carrier
|
||||
)
|
||||
})?;
|
||||
|
||||
let id_prefix = format!(
|
||||
"{}.{}{}-{}-{}{}",
|
||||
issuer_id,
|
||||
leg.operating_carrier,
|
||||
leg.flight_number,
|
||||
leg.date_of_flight,
|
||||
leg.origin_airport,
|
||||
leg.destination_airport
|
||||
);
|
||||
let class_id = &id_prefix;
|
||||
let pass_id = format!("{}-{}", &id_prefix, leg.check_in_sequence);
|
||||
|
||||
let class = FlightClass {
|
||||
id: Some(class_id.clone()),
|
||||
issuer_name: Some(airline_data.name.to_string()),
|
||||
hex_background_color: Some(airline_data.boarding_pass_background_colour.to_string()),
|
||||
review_status: Some("UNDER_REVIEW".to_string()),
|
||||
boarding_and_seating_policy: Some(BoardingAndSeatingPolicy {
|
||||
boarding_policy: Some(airline_data.boarding_policy.to_string()),
|
||||
seat_class_policy: Some(airline_data.seat_class_policy.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
origin: Some(AirportInfo {
|
||||
airport_iata_code: Some(leg.origin_airport.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
destination: Some(AirportInfo {
|
||||
airport_iata_code: Some(leg.destination_airport.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
flight_header: Some(FlightHeader {
|
||||
carrier: Some(FlightCarrier {
|
||||
airline_alliance_logo: airline_data.alliance_logo_url.map(url_to_image),
|
||||
airline_logo: Some(url_to_image(airline_data.logo_url)),
|
||||
airline_name: Some(localized_string("en-GB", airline_data.name)),
|
||||
carrier_iata_code: Some(leg.operating_carrier.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
flight_number: Some(leg.flight_number.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let pass = FlightObject {
|
||||
id: Some(pass_id),
|
||||
class_id: Some(class_id.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok((vec![class], vec![pass]))
|
||||
}
|
||||
|
||||
pub fn localized_string(locale: &str, s: &str) -> LocalizedString {
|
||||
LocalizedString {
|
||||
default_value: Some(TranslatedString {
|
||||
language: Some(locale.to_string()),
|
||||
value: Some(s.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url_to_image(url: &str) -> Image {
|
||||
Image {
|
||||
source_uri: Some(ImageUri {
|
||||
uri: Some(url.to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue