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",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"futures-util",
|
||||||
"google-walletobjects1",
|
"google-walletobjects1",
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
|
|
@ -10,6 +10,7 @@ anyhow = { version = "1.0.68", features = ["backtrace"] }
|
||||||
async-trait = "0.1.61"
|
async-trait = "0.1.61"
|
||||||
chrono = "0.4.23"
|
chrono = "0.4.23"
|
||||||
clap = { version = "4.0.32", features = ["cargo", "derive"] }
|
clap = { version = "4.0.32", features = ["cargo", "derive"] }
|
||||||
|
futures-util = "0.3.25"
|
||||||
google-walletobjects1 = "4.0.4"
|
google-walletobjects1 = "4.0.4"
|
||||||
http = "0.2.8"
|
http = "0.2.8"
|
||||||
hyper = "0.14.23"
|
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 std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use chrono::naive::NaiveDate;
|
|
||||||
use chrono::Datelike;
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use std::cmp::min;
|
|
||||||
|
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use google_walletobjects1::api::{FlightClass, FlightObject};
|
||||||
use async_trait::async_trait;
|
use serde::Deserialize;
|
||||||
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 tokio::try_join;
|
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)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None, propagate_version = true, subcommand_required = true, arg_required_else_help = true)]
|
#[command(author, version, about, long_about = None, propagate_version = true, subcommand_required = true, arg_required_else_help = true)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
@ -95,742 +81,11 @@ struct UploadScan {
|
||||||
image: PathBuf,
|
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> {
|
async fn load_thing<'a, T: Deserialize<'a>>(path: &PathBuf) -> Result<T> {
|
||||||
let data = tokio::fs::read(path).await?;
|
let data = tokio::fs::read(path).await?;
|
||||||
let mut deser = serde_json::Deserializer::from_reader(data.as_slice());
|
let mut deser = serde_json::Deserializer::from_reader(data.as_slice());
|
||||||
let parsed: T = T::deserialize(&mut deser)?;
|
let parsed: T = T::deserialize(&mut deser)?;
|
||||||
return Ok(parsed);
|
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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -839,17 +94,14 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let command = cli.command.unwrap();
|
let command = cli.command.unwrap();
|
||||||
|
|
||||||
match &command {
|
if let Commands::Scan { image } = &command {
|
||||||
Commands::Scan { image } => {
|
println!("{}", scan(image)?);
|
||||||
println!("{}", scan(&image)?);
|
return Ok(());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let service_account_file = cli
|
let service_account_file = cli
|
||||||
.service_account_file
|
.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 sa = load_service_account(&service_account_file).await?;
|
||||||
|
|
||||||
let hub = google_walletobjects1::Walletobjects::new(
|
let hub = google_walletobjects1::Walletobjects::new(
|
||||||
|
@ -861,7 +113,7 @@ async fn main() -> Result<()> {
|
||||||
.enable_http2()
|
.enable_http2()
|
||||||
.build(),
|
.build(),
|
||||||
),
|
),
|
||||||
sa.authenticator,
|
sa.authenticator.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
match &command {
|
match &command {
|
||||||
|
@ -900,49 +152,27 @@ async fn main() -> Result<()> {
|
||||||
class.review_status = Some("UNDER_REVIEW".to_string());
|
class.review_status = Some("UNDER_REVIEW".to_string());
|
||||||
|
|
||||||
// Try to create the class first...
|
// Try to create the class first...
|
||||||
let created_class: bool = hub.flightclass().insert_now(&class).await?;
|
insert_or_update_now(&hub.flightclass(), &class).await?;
|
||||||
if !created_class {
|
|
||||||
// Already exists.
|
|
||||||
hub.flightclass().update_now(&class).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then the pass...
|
// Then the pass...
|
||||||
let created_pass: bool = hub.flightobject().insert_now(&pass).await?;
|
insert_or_update_now(&hub.flightobject(), &pass).await?;
|
||||||
if !created_pass {
|
|
||||||
// Already exists.
|
|
||||||
hub.flightobject().update_now(&pass).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now generate the URL.
|
// Now generate the URL.
|
||||||
let claims: JWTClaims<AddToWalletClaims> = JWTClaims {
|
println!(
|
||||||
issued_at: None,
|
"{}",
|
||||||
expires_at: None,
|
AddToWallet {
|
||||||
invalid_before: None,
|
service_account_email: sa.service_account_name.to_string(),
|
||||||
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!("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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -950,88 +180,15 @@ async fn main() -> Result<()> {
|
||||||
let scanned = scan(&upload_scan.image)?;
|
let scanned = scan(&upload_scan.image)?;
|
||||||
let boarding_pass =
|
let boarding_pass =
|
||||||
BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?;
|
BoardingPass::parse(scanned.as_bytes().to_vec(), &Local::now().date_naive())?;
|
||||||
if boarding_pass.legs.len() > 1 {
|
let (classes, passes) = passes_from_barcode(&cli.issuer_id, boarding_pass)?;
|
||||||
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 id_prefix = format!(
|
// Create all the classes.
|
||||||
"{}.{}{}-{}-{}{}",
|
insert_or_update_many(&hub.flightclass(), classes).await?;
|
||||||
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);
|
|
||||||
|
|
||||||
let mut class = FlightClass::default();
|
// Create all the objects.
|
||||||
class.id = Some(class_id.clone());
|
insert_or_update_many(&hub.flightobject(), passes).await?;
|
||||||
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());
|
|
||||||
|
|
||||||
Ok(())
|
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