<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <style>html, body {
  margin: 0;
  padding: 0;
}

.app {
  margin: 10px;
  padding: 0;
}

.files-list {
  margin: 10px 0 0;
  width: 100%;
  border-collapse: collapse;
}
.files-list__head {
  border: 1px solid #999;
}
.files-list__head > tr > th {
  padding: 10px;
  border: 1px solid #999;
  text-align: left;
  font-weight: normal;
  background: #ddd;
}
.files-list__body {
}
.files-list__file {
  cursor: pointer;
}
.files-list__file:hover {
  background: #ccf;
}
.files-list__file > td {
  padding: 10px;
  border: 1px solid #999;
}
.files-list__file > td:first-child::before {
  content: '\01F4C4';
  margin-right: 1em;
}
.files-list__file_low {
  background: #fcc;
}
.files-list__file_medium {
  background: #ffc;
}
.files-list__file_high {
  background: #cfc;
}
.files-list__file_folder > td:first-child::before {
  content: '\01F4C1';
  margin-right: 1em;
}

.file-header {
  border: 1px solid #999;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.file-header__back {
  margin: 10px;
  cursor: pointer;
  flex-shrink: 0;
  flex-grow: 0;
  text-decoration: underline;
  color: #338;
}

.file-header__name {
  margin: 10px;
  flex-shrink: 2;
  flex-grow: 2;
}

.file-header__stat {
  margin: 10px;
  flex-shrink: 0;
  flex-grow: 0;
}

.file-content {
  margin: 10px 0 0;
  border: 1px solid #999;
  padding: 10px;
}

.code-line {
  margin: 0;
  padding: 0.3em;
  height: 1em;
}
.code-line_covered {
  background: #cfc;
}
.code-line_uncovered {
  background: #fcc;
}
</style>
</head>
<body>
    <div id="root"></div>
    <script>
        var data = {"files":[{"path":["/","home","core","prj","e3t","trifid","tfclient","src","main.rs"],"content":"fn main() {\n    println!(\"Hello, world!\");\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","build.rs"],"content":"// generated by `sqlx migrate build-script`\nfn main() {\n    // trigger recompilation when a new migration is added\n    println!(\"cargo:rerun-if-changed=migrations\");\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","auth.rs"],"content":"use rocket::http::Status;\nuse rocket::{Request};\nuse rocket::request::{FromRequest, Outcome};\nuse crate::tokens::{validate_auth_token, validate_session_token};\n\npub struct PartialUserInfo {\n    pub user_id: i32,\n    pub created_at: i64,\n    pub email: String,\n    pub has_totp_auth: bool,\n\n    pub session_id: String,\n    pub auth_id: Option\u003cString\u003e\n}\n\n#[derive(Debug)]\npub enum AuthenticationError {\n    MissingToken,\n    InvalidToken(usize),\n    DatabaseError,\n    RequiresTOTP\n}\n\n#[rocket::async_trait]\nimpl\u003c'r\u003e FromRequest\u003c'r\u003e for PartialUserInfo {\n    type Error = AuthenticationError;\n\n    async fn from_request(req: \u0026'r Request\u003c'_\u003e) -\u003e Outcome\u003cSelf, Self::Error\u003e {\n        let headers = req.headers();\n\n        // make sure the bearer token exists\n        if let Some(authorization) = headers.get_one(\"Authorization\") {\n            // parse bearer token\n            let components = authorization.split(' ').collect::\u003cVec\u003c\u0026str\u003e\u003e();\n\n            if components.len() != 2 \u0026\u0026 components.len() != 3 {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken));\n            }\n\n            if components[0] != \"Bearer\" {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(0)));\n            }\n\n            if components.len() == 2 \u0026\u0026 !components[1].starts_with(\"st-\") {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(1)));\n            }\n\n            let st: String;\n            let user_id: i64;\n            let at: Option\u003cString\u003e;\n\n            match \u0026components[1][..3] {\n                \"st-\" =\u003e {\n                    // validate session token\n                    st = components[1].to_string();\n                    match validate_session_token(st.clone(), req.rocket().state().unwrap()).await {\n                        Ok(uid) =\u003e user_id = uid,\n                        Err(_) =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(2)))\n                    }\n                },\n                _ =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(3)))\n            }\n\n            if components.len() == 3 {\n                match \u0026components[2][..3] {\n                    \"at-\" =\u003e {\n                        // validate auth token\n                        at = Some(components[2].to_string());\n                        match validate_auth_token(at.clone().unwrap().clone(), st.clone(), req.rocket().state().unwrap()).await {\n                            Ok(_) =\u003e (),\n                            Err(_) =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(4)))\n                        }\n                    },\n                    _ =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(5)))\n                }\n            } else {\n                at = None;\n            }\n\n            // this user is 100% valid and authenticated, fetch their info\n\n            let user = match sqlx::query!(\"SELECT * FROM users WHERE id = $1\", user_id.clone() as i32).fetch_one(req.rocket().state().unwrap()).await {\n                Ok(u) =\u003e u,\n                Err(_) =\u003e return Outcome::Failure((Status::InternalServerError, AuthenticationError::DatabaseError))\n            };\n\n            Outcome::Success(PartialUserInfo {\n                user_id: user_id as i32,\n                created_at: user.created_on as i64,\n                email: user.email,\n                has_totp_auth: at.is_some(),\n                session_id: st,\n                auth_id: at,\n            })\n        } else {\n            Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken))\n        }\n    }\n}\n\npub struct TOTPAuthenticatedUserInfo {\n    pub user_id: i32,\n    pub created_at: i64,\n    pub email: String,\n}\n#[rocket::async_trait]\nimpl\u003c'r\u003e FromRequest\u003c'r\u003e for TOTPAuthenticatedUserInfo {\n    type Error = AuthenticationError;\n\n    async fn from_request(request: \u0026'r Request\u003c'_\u003e) -\u003e Outcome\u003cSelf, Self::Error\u003e {\n        let userinfo = PartialUserInfo::from_request(request).await;\n        match userinfo {\n            Outcome::Failure(e) =\u003e Outcome::Failure(e),\n            Outcome::Forward(f) =\u003e Outcome::Forward(f),\n            Outcome::Success(s) =\u003e {\n                if s.has_totp_auth {\n                    Outcome::Success(Self {\n                        user_id: s.user_id,\n                        created_at: s.created_at,\n                        email: s.email,\n                    })\n                } else {\n                    Outcome::Failure((Status::Unauthorized, AuthenticationError::RequiresTOTP))\n                }\n            }\n        }\n    }\n}","traces":[{"line":28,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":29,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":32,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":34,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":36,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":37,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":40,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":41,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":44,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":45,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":48,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":49,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":50,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":52,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":53,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":55,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":56,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":57,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":58,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":61,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":64,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":65,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":66,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":68,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":69,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":70,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":71,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":74,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":77,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":82,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":83,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":84,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":87,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":88,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":89,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":90,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":91,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":92,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":93,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":96,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":110,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":111,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":112,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":113,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":114,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":115,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":116,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":117,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":118,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":119,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":120,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":123,"address":[],"length":0,"stats":{"Line":0},"fn_name":null}],"covered":0,"coverable":52},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","config.rs"],"content":"use serde::Deserialize;\nuse url::Url;\n\n#[derive(Deserialize)]\npub struct TFConfig {\n    pub listen_port: u16,\n    pub db_url: String,\n    pub base: Url,\n    pub web_root: Url,\n    pub magic_links_valid_for: i64,\n    pub session_tokens_valid_for: i64,\n    pub totp_verification_valid_for: i64,\n    pub data_key: String\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","crypto.rs"],"content":"use std::error::Error;\nuse aes_gcm::{Aes256Gcm, KeyInit, Nonce};\nuse aes_gcm::aead::{Aead, Payload};\nuse crate::config::TFConfig;\n\npub fn get_cipher_from_config(config: \u0026TFConfig) -\u003e Result\u003cAes256Gcm, Box\u003cdyn Error\u003e\u003e {\n    let key_slice = hex::decode(\u0026config.data_key)?;\n    Ok(Aes256Gcm::new_from_slice(\u0026key_slice)?)\n}\n\npub fn encrypt_with_nonce(plaintext: \u0026[u8], nonce: [u8; 12], cipher: \u0026Aes256Gcm) -\u003e Result\u003cVec\u003cu8\u003e, aes_gcm::Error\u003e {\n    let nonce = Nonce::from_slice(\u0026nonce);\n    let ciphertext = cipher.encrypt(nonce, plaintext)?;\n    Ok(ciphertext)\n}\n\npub fn decrypt_with_nonce(ciphertext: \u0026[u8], nonce: [u8; 12], cipher: \u0026Aes256Gcm) -\u003e Result\u003cVec\u003cu8\u003e, aes_gcm::Error\u003e {\n    let nonce = Nonce::from_slice(\u0026nonce);\n    let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?;\n    Ok(plaintext)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","db.rs"],"content":"","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","format.rs"],"content":"use std::fmt::{Display, Formatter};\nuse crate::format::PEMValidationError::{IncorrectSegmentLength, InvalidBase64Data, MissingStartSentinel};\nuse crate::util::base64decode;\n\npub const ED_PUBKEY_START_STR: \u0026str = \"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\";\npub const ED_PUBKEY_END_STR: \u0026str = \"-----END NEBULA ED25519 PUBLIC KEY-----\";\n\npub const DH_PUBKEY_START_STR: \u0026str = \"-----BEGIN NEBULA X25519 PUBLIC KEY-----\";\npub const DH_PUBKEY_END_STR: \u0026str = \"-----END NEBULA X25519 PUBLIC KEY-----\";\n\npub enum PEMValidationError {\n    MissingStartSentinel,\n    InvalidBase64Data,\n    MissingEndSentinel,\n    IncorrectSegmentLength(usize, usize)\n}\nimpl Display for PEMValidationError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::MissingEndSentinel =\u003e write!(f, \"Missing ending sentinel\"),\n            Self::MissingStartSentinel =\u003e write!(f, \"Missing starting sentinel\"),\n            Self::InvalidBase64Data =\u003e write!(f, \"invalid base64 data\"),\n            Self::IncorrectSegmentLength(expected, got) =\u003e write!(f, \"incorrect number of segments, expected {} got {}\", expected, got)\n        }\n    }\n}\n\npub fn validate_ed_pubkey_pem(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    let segments = pubkey.split('\\n');\n    let segs = segments.collect::\u003cVec\u003c\u0026str\u003e\u003e();\n    if segs.len() \u003c 3 {\n        return Err(IncorrectSegmentLength(3, segs.len()))\n    }\n    if segs[0] != ED_PUBKEY_START_STR {\n        return Err(MissingStartSentinel)\n    }\n    if base64decode(segs[1]).is_err() {\n        return Err(InvalidBase64Data)\n    }\n    if segs[2] != ED_PUBKEY_END_STR {\n        return Err(MissingStartSentinel)\n    }\n    Ok(())\n}\n\npub fn validate_dh_pubkey_pem(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    let segments = pubkey.split('\\n');\n    let segs = segments.collect::\u003cVec\u003c\u0026str\u003e\u003e();\n    if segs.len() \u003c 3 {\n        return Err(IncorrectSegmentLength(3, segs.len()))\n    }\n    if segs[0] != DH_PUBKEY_START_STR {\n        return Err(MissingStartSentinel)\n    }\n    if base64decode(segs[1]).is_err() {\n        return Err(InvalidBase64Data)\n    }\n    if segs[2] != DH_PUBKEY_END_STR {\n        return Err(MissingStartSentinel)\n    }\n    Ok(())\n}\n\npub fn validate_ed_pubkey_base64(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    match base64decode(pubkey) {\n        Ok(k) =\u003e validate_ed_pubkey_pem(match std::str::from_utf8(k.as_ref()) {\n            Ok(k) =\u003e k,\n            Err(_) =\u003e return Err(InvalidBase64Data)\n        }),\n        Err(_) =\u003e Err(InvalidBase64Data)\n    }\n}\n\npub fn validate_dh_pubkey_base64(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    match base64decode(pubkey) {\n        Ok(k) =\u003e validate_dh_pubkey_pem(match std::str::from_utf8(k.as_ref()) {\n            Ok(k) =\u003e k,\n            Err(_) =\u003e return Err(InvalidBase64Data)\n        }),\n        Err(_) =\u003e Err(InvalidBase64Data)\n    }\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","main.rs"],"content":"extern crate core;\n\nuse std::error::Error;\nuse std::fs;\nuse std::path::Path;\nuse dotenvy::dotenv;\nuse log::{error, info};\nuse rocket::{catchers, Request, Response, routes};\nuse rocket::fairing::{Fairing, Info, Kind};\nuse rocket::http::Header;\nuse sqlx::migrate::Migrator;\nuse sqlx::postgres::PgPoolOptions;\nuse crate::config::TFConfig;\n\npub mod format;\npub mod util;\npub mod db;\npub mod config;\npub mod tokens;\npub mod routes;\npub mod auth;\npub mod crypto;\npub mod org;\n\nstatic MIGRATOR: Migrator = sqlx::migrate!();\n\npub struct CORS;\n\n#[rocket::async_trait]\nimpl Fairing for CORS {\n    fn info(\u0026self) -\u003e Info {\n        Info {\n            name: \"Add CORS headers to responses\",\n            kind: Kind::Response\n        }\n    }\n\n    async fn on_response\u003c'r\u003e(\u0026self, _request: \u0026'r Request\u003c'_\u003e, response: \u0026mut Response\u003c'r\u003e) {\n        response.set_header(Header::new(\"Access-Control-Allow-Origin\", \"*\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Methods\", \"POST, GET, PATCH, OPTIONS\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Headers\", \"*\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Credentials\", \"true\"));\n    }\n}\n\n#[rocket::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    let _ = rocket::build();\n\n    info!(\"[tfapi] loading config\");\n\n    let _ = dotenv();\n\n    if std::env::var(\"CONFIG_FILE\").is_err() \u0026\u0026 !Path::new(\"config.toml\").exists() {\n        error!(\"[tfapi] fatal: the environment variable CONFIG_FILE is not set\");\n        error!(\"[tfapi]  help: try creating a .env file that sets it\");\n        error!(\"[tfapi]  help: or, create a file config.toml with your config, as it is loaded automatically\");\n        std::process::exit(1);\n    }\n\n    let config_file = if Path::new(\"config.toml\").exists() {\n        \"config.toml\".to_string()\n    } else {\n        std::env::var(\"CONFIG_FILE\").unwrap()\n    };\n\n    let config_data = match fs::read_to_string(\u0026config_file) {\n        Ok(d) =\u003e d,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to read config from {}\", config_file);\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    let config: TFConfig = match toml::from_str(\u0026config_data) {\n        Ok(c) =\u003e c,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to parse config from {}\", config_file);\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    info!(\"[tfapi] connecting to database pool\");\n\n    let pool = match PgPoolOptions::new().max_connections(5).connect(\u0026config.db_url).await {\n        Ok(p) =\u003e p,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to connect to database pool\");\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    info!(\"[tfapi] running database migrations\");\n\n    MIGRATOR.run(\u0026pool).await?;\n\n    info!(\"[tfapi] building rocket config\");\n\n    let figment = rocket::Config::figment().merge((\"port\", config.listen_port));\n\n    let _ = rocket::custom(figment)\n        .mount(\"/\", routes![\n            crate::routes::v1::auth::magic_link::magiclink_request,\n            crate::routes::v1::auth::magic_link::options,\n            crate::routes::v1::signup::signup_request,\n            crate::routes::v1::signup::options,\n            crate::routes::v1::auth::verify_magic_link::verify_magic_link,\n            crate::routes::v1::auth::verify_magic_link::options,\n            crate::routes::v1::totp_authenticators::totp_authenticators_request,\n            crate::routes::v1::totp_authenticators::options,\n            crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,\n            crate::routes::v1::verify_totp_authenticator::options,\n            crate::routes::v1::auth::totp::totp_request,\n            crate::routes::v1::auth::totp::options,\n            crate::routes::v1::auth::check_session::check_session,\n            crate::routes::v1::auth::check_session::check_session_auth,\n            crate::routes::v1::auth::check_session::options,\n            crate::routes::v1::auth::check_session::options_auth,\n            crate::routes::v2::whoami::whoami_request,\n            crate::routes::v2::whoami::options,\n\n            crate::routes::v1::organization::options,\n            crate::routes::v1::organization::orgidoptions,\n            crate::routes::v1::organization::orginfo_req,\n            crate::routes::v1::organization::orglist_req,\n            crate::routes::v1::organization::create_org\n        ])\n        .register(\"/\", catchers![\n            crate::routes::handler_400,\n            crate::routes::handler_401,\n            crate::routes::handler_403,\n            crate::routes::handler_404,\n            crate::routes::handler_422,\n\n            crate::routes::handler_500,\n            crate::routes::handler_501,\n            crate::routes::handler_502,\n            crate::routes::handler_503,\n            crate::routes::handler_504,\n            crate::routes::handler_505,\n        ])\n        .attach(CORS)\n        .manage(pool)\n        .manage(config)\n        .launch().await?;\n\n    Ok(())\n}","traces":[{"line":38,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":39,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":40,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":41,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":42,"address":[],"length":0,"stats":{"Line":0},"fn_name":null}],"covered":0,"coverable":5},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","org.rs"],"content":"use std::error::Error;\nuse rocket::form::validate::Contains;\nuse sqlx::PgPool;\n\npub async fn get_org_by_owner_id(user: i32, db: \u0026PgPool) -\u003e Result\u003cOption\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    Ok(match sqlx::query!(\"SELECT id FROM organizations WHERE owner = $1\", user).fetch_optional(db).await? {\n        Some(r) =\u003e Some(r.id),\n        None =\u003e None\n    })\n}\n\npub async fn get_orgs_by_assoc_id(user: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let res: Vec\u003c_\u003e = sqlx::query!(\"SELECT org_id FROM organization_authorized_users WHERE user_id = $1\", user).fetch_all(db).await?;\n\n    let mut ret = vec![];\n\n    for i in res {\n        ret.push(i.org_id);\n    }\n\n    Ok(ret)\n}\n\npub async fn get_users_by_assoc_org(org: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let res: Vec\u003c_\u003e = sqlx::query!(\"SELECT user_id FROM organization_authorized_users WHERE org_id = $1\", org).fetch_all(db).await?;\n\n    let mut ret = vec![];\n\n    for i in res {\n        ret.push(i.org_id);\n    }\n\n    Ok(ret)\n}\n\npub async fn get_associated_orgs(user: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let mut assoc_orgs = vec![];\n\n    if let Some(owned_org) = get_org_by_owner_id(user, db).await? {\n        assoc_orgs.push(owned_org);\n    }\n\n    assoc_orgs.append(\u0026mut get_orgs_by_assoc_id(user, db).await?);\n\n    Ok(assoc_orgs)\n}\n\npub async fn user_has_org_assoc(user: i32, org: i32, db: \u0026PgPool) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n    Ok(get_associated_orgs(user, db).await?.contains(org))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","mod.rs"],"content":"pub mod v1;\npub mod v2;\n\nuse rocket::catch;\nuse serde::{Serialize};\nuse rocket::http::Status;\n\npub const ERR_MSG_MALFORMED_REQUEST: \u0026str = \"unable to parse the request body - is it valid JSON, using correct types?\";\npub const ERR_MSG_MALFORMED_REQUEST_CODE: \u0026str = \"ERR_MALFORMED_REQUEST\";\n\n/*\nTODO:\n    /v1/auth/magic-link                         [done]\n    /v1/auth/totp                               [done]\n    /v1/auth/verify-magic-link                  [done]\n    /v1/hosts/host-{id}/enrollment-code\n    /v1/hosts/host-{id}/enrollment-code-check\n    /v1/hosts/host-{id}\n    /v1/roles/role-{id}\n    /v1/feature-flags\n    /v1/hosts\n    /v1/networks\n    /v1/roles\n    /v1/signup                                  [done]\n    /v1/totp-authenticators                     [done]\n    /v1/verify-totp-authenticator               [done]\n    /v1/dnclient\n    /v2/enroll\n    /v2/whoami                                  [in-progress]\n */\n\n#[derive(Serialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct APIError {\n    errors: Vec\u003cAPIErrorSingular\u003e\n}\n#[derive(Serialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct APIErrorSingular {\n    code: String,\n    message: String\n}\n\nmacro_rules! error_handler {\n    ($code: expr, $err: expr, $msg: expr) =\u003e {\n        ::paste::paste! {\n            #[catch($code)]\n            pub fn [\u003chandler_ $code\u003e]() -\u003e (Status, String) {\n                (Status::from_code($code).unwrap(), format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", $err, $msg))\n            }\n        }\n    };\n}\nerror_handler!(400, \"ERR_MALFORMED_REQUEST\", \"unable to parse the request body, is it properly formatted?\");\nerror_handler!(401, \"ERR_AUTHENTICATION_REQUIRED\", \"this endpoint requires authentication but it was not provided\");\nerror_handler!(403, \"ERR_UNAUTHORIZED\", \"authorization was provided but it is expired or invalid\");\nerror_handler!(404, \"ERR_NOT_FOUND\", \"resource not found\");\nerror_handler!(405, \"ERR_METHOD_NOT_ALLOWED\", \"method not allowed for this endpoint\");\nerror_handler!(422, \"ERR_MALFORMED_REQUEST\", \"unable to parse the request body, is it properly formatted?\");\n\nerror_handler!(500, \"ERR_QL_QUERY_FAILED\", \"graphql query timed out\");\nerror_handler!(501, \"ERR_NOT_IMPLEMENTED\", \"query not supported by this version of graphql\");\nerror_handler!(502, \"ERR_PROXY_ERR\", \"servers under load, please try again later\");\nerror_handler!(503, \"ERR_SERVER_OVERLOADED\", \"servers under load, please try again later\");\nerror_handler!(504, \"ERR_PROXY_TIMEOUT\", \"servers under load, please try again later\");\nerror_handler!(505, \"ERR_CLIENT_UNSUPPORTED\", \"your version of dnclient is out of date, please update\");\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","check_session.rs"],"content":"use rocket::{post, options};\nuse crate::auth::{PartialUserInfo, TOTPAuthenticatedUserInfo};\n\n#[options(\"/v1/auth/check_session\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/auth/check_session\")]\npub async fn check_session(_user: PartialUserInfo) -\u003e \u0026'static str {\n    \"ok\"\n}\n\n#[options(\"/v1/auth/check_auth\")]\npub async fn options_auth() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/check_auth\")]\npub async fn check_session_auth(_user: TOTPAuthenticatedUserInfo) -\u003e \u0026'static str {\n    \"ok\"\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","magic_link.rs"],"content":"use rocket::{post, State};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse rocket::http::{ContentType, Status};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::send_magic_link;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkRequest {\n    pub email: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkResponse {\n    pub data: Option\u003cString\u003e,\n    pub metadata: MagicLinkResponseMetadata,\n}\n\n\n#[options(\"/v1/auth/magic-link\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/auth/magic-link\", data = \"\u003creq\u003e\")]\npub async fn magiclink_request(req: Json\u003cMagicLinkRequest\u003e, pool: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cMagicLinkResponse\u003e), (Status, String)\u003e {\n    // figure out if the user already exists\n    let mut id = -1;\n    match sqlx::query!(\"SELECT id FROM users WHERE email = $1\", req.email.clone()).fetch_optional(pool.inner()).await {\n        Ok(res) =\u003e if let Some(r) = res { id = r.id as i64 },\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    }\n\n    if id == -1 {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"authorization was provided but it is expired or invalid\")))\n    }\n\n    // send magic link to email\n    match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    };\n\n    // this endpoint doesn't actually ever return an error? it will send you the magic link no matter what\n    // this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)\n    Ok((ContentType::JSON, Json(MagicLinkResponse {\n        data: None,\n        metadata: MagicLinkResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","mod.rs"],"content":"pub mod verify_magic_link;\npub mod magic_link;\npub mod totp;\npub mod check_session;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","totp.rs"],"content":"use rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse crate::auth::PartialUserInfo;\nuse serde::{Serialize, Deserialize};\nuse rocket::{post, State};\nuse sqlx::PgPool;\nuse rocket::options;\nuse crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp};\n\npub const TOTP_GENERIC_UNAUTHORIZED_ERROR: \u0026str = \"{\\\"errors\\\":[{\\\"code\\\":\\\"ERR_INVALID_TOTP_CODE\\\",\\\"message\\\":\\\"invalid TOTP code (maybe it expired?)\\\",\\\"path\\\":\\\"code\\\"}]}\";\npub const TOTP_NO_TOTP_ERROR: \u0026str = \"{\\\"errors\\\":[{\\\"code\\\":\\\"ERR_NO_TOTP\\\",\\\"message\\\":\\\"logged-in user does not have totp enabled\\\",\\\"path\\\":\\\"code\\\"}]}\";\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpRequest {\n    pub code: String\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponseData {\n    #[serde(rename = \"authToken\")]\n    auth_token: String\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponseMetadata {\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponse {\n    data: TotpResponseData,\n    metadata: TotpResponseMetadata\n}\n\n#[options(\"/v1/auth/totp\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/totp\", data = \"\u003creq\u003e\")]\npub async fn totp_request(req: Json\u003cTotpRequest\u003e, user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cTotpResponse\u003e), (Status, String)\u003e {\n    if !match user_has_totp(user.user_id, db.inner()).await {\n        Ok(b) =\u003e b,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    } {\n        return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string()))\n    }\n\n    if user.has_totp_auth {\n        return Err((Status::BadRequest, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_TOTP_ALREADY_AUTHED\", \"user already has valid totp authentication\")))\n    }\n\n    let totpmachine = match get_totpmachine(user.user_id, db.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    };\n\n    if !totpmachine.check_current(\u0026req.0.code).unwrap_or(false) {\n        return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string()))\n    }\n\n    Ok((ContentType::JSON, Json(TotpResponse {\n        data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {\n            Ok(t) =\u003e t,\n            Err(e) =\u003e { return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e))) }\n        } },\n        metadata: TotpResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","verify_magic_link.rs"],"content":"use std::time::{SystemTime, UNIX_EPOCH};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse rocket::{post, State};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::generate_session_token;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct VerifyMagicLinkRequest {\n    #[serde(rename = \"magicLinkToken\")]\n    pub magic_link_token: String,\n}\n\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponseData {\n    #[serde(rename = \"sessionToken\")]\n    pub session_token: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponse {\n    pub data: VerifyMagicLinkResponseData,\n    pub metadata: VerifyMagicLinkResponseMetadata,\n}\n\n#[options(\"/v1/auth/verify-magic-link\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/verify-magic-link\", data = \"\u003creq\u003e\")]\npub async fn verify_magic_link(req: Json\u003cVerifyMagicLinkRequest\u003e, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cVerifyMagicLinkResponse\u003e), (Status, String)\u003e {\n    // get the current time to check if the token is expired\n    let (user_id, expired_at) = match sqlx::query!(\"SELECT user_id, expires_on FROM magic_links WHERE id = $1\", req.0.magic_link_token).fetch_one(db.inner()).await {\n        Ok(row) =\u003e (row.user_id, row.expires_on),\n        Err(e) =\u003e {\n            return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"this token is invalid\", e)))\n        }\n    };\n    let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;\n    println!(\"expired on {}, currently {}\", expired_at, current_time);\n    if expired_at \u003c current_time {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"valid authorization was provided but it is expired\")))\n    }\n\n    // generate session token\n    let token = match generate_session_token(user_id as i64, db.inner(), config.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    };\n\n    // delete the token\n    match sqlx::query!(\"DELETE FROM magic_links WHERE id = $1\", req.0.magic_link_token).execute(db.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    }\n\n    Ok((ContentType::JSON, Json(VerifyMagicLinkResponse {\n        data: VerifyMagicLinkResponseData { session_token: token },\n        metadata: VerifyMagicLinkResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","mod.rs"],"content":"pub mod auth;\npub mod signup;\n\npub mod totp_authenticators;\npub mod verify_totp_authenticator;\npub mod organization;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","organization.rs"],"content":"use std::slice::from_raw_parts_mut;\nuse rocket::{get, post, options, State};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse sqlx::PgPool;\nuse crate::auth::TOTPAuthenticatedUserInfo;\nuse crate::config::TFConfig;\nuse crate::org::{get_associated_orgs, get_org_by_owner_id, get_users_by_assoc_org, user_has_org_assoc};\n\n#[options(\"/v1/orgs\")]\npub fn options() -\u003e \u0026'static str {\n    \"\"\n}\n#[options(\"/v1/org/\u003c_id\u003e\")]\npub fn orgidoptions(_id: i32) -\u003e \u0026'static str {\n    \"\"\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct OrglistStruct {\n    pub org_ids: Vec\u003ci32\u003e\n}\n\n\n#[get(\"/v1/orgs\")]\npub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrglistStruct\u003e), (Status, String)\u003e {\n    // this endpoint lists the associated organizations this user has access to\n    let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {\n        Ok(r) =\u003e r,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_DB_QUERY_FAILED\", \"an error occurred while running the database query\")))\n    };\n\n    Ok((ContentType::JSON, Json(OrglistStruct {\n        org_ids: associated_orgs\n    })))\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct OrginfoStruct {\n    pub org_id: i32,\n    pub owner_id: i32,\n    pub ca_crt: String,\n    pub authorized_users: Vec\u003ci32\u003e\n}\n\n#[get(\"/v1/org/\u003corgid\u003e\")]\npub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrginfoStruct\u003e), (Status, String)\u003e {\n    if !user_has_org_assoc(orgid, user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))? {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_MISSING_ORG_AUTHORIZATION\", \"this user does not have permission to access this org\")));\n    }\n\n    let org = sqlx::query!(\"SELECT id, owner, ca_crt FROM organizations WHERE id = $1\", orgid).fetch_one(orgid).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?;\n    let authorized_users = get_users_by_assoc_org(orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?;\n\n    Ok((ContentType::JSON, Json(\n        OrginfoStruct {\n            org_id: orgid,\n            owner_id: org.owner,\n            ca_crt: org.ca_crt,\n            authorized_users,\n        }\n    )))\n}\n\n#[post(\"/v1/org\")]\npub async fn create_org(user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrginfoStruct\u003e), (Status, String)\u003e {\n    if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?.is_some() {\n        return Err((Status::Conflict, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_USER_OWNS_ORG\", \"a user can only own one organization at a time\")))\n    }\n\n    // we need to generate a keypair and CA certificate for this new org\n\n    Ok((ContentType::JSON, Json(\n        OrginfoStruct {\n            org_id: orgid,\n            owner_id: org.owner,\n            ca_crt: org.ca_crt,\n            authorized_users,\n        }\n    )))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","signup.rs"],"content":"use rocket::{post, State};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse std::time::{SystemTime, UNIX_EPOCH};\nuse rocket::http::{ContentType, Status};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::send_magic_link;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupRequest {\n    pub email: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupResponse {\n    pub data: Option\u003cString\u003e,\n    pub metadata: SignupResponseMetadata,\n}\n/*\ncreated_on TIMESTAMP NOT NULL,\n\n    banned INTEGER NOT NULL,\n    ban_reason VARCHAR(1024) NOT NULL\n */\n#[options(\"/v1/signup\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/signup\", data = \"\u003creq\u003e\")]\npub async fn signup_request(req: Json\u003cSignupRequest\u003e, pool: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cSignupResponse\u003e), (Status, String)\u003e {\n    // figure out if the user already exists\n    let mut id = -1;\n    match sqlx::query!(\"SELECT id FROM users WHERE email = $1\", req.email.clone()).fetch_optional(pool.inner()).await {\n        Ok(res) =\u003e if let Some(r) = res { id = r.id as i64 },\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    }\n\n    if id == -1 {\n        let id_res = match sqlx::query!(\"INSERT INTO users (email, created_on, banned, ban_reason, totp_secret, totp_otpurl, totp_verified) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING RETURNING id;\", req.email, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 0, \"\", \"\", \"\", 0).fetch_one(pool.inner()).await {\n            Ok(row) =\u003e row.id,\n            Err(e) =\u003e {\n                return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n            }\n        };\n        id = id_res as i64;\n    }\n\n    // send magic link to email\n    match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    };\n\n    // this endpoint doesn't actually ever return an error? it will send you the magic link no matter what\n    // this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)\n    Ok((ContentType::JSON, Json(SignupResponse {\n        data: None,\n        metadata: SignupResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","totp_authenticators.rs"],"content":"use rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse rocket::{State, post};\nuse sqlx::PgPool;\nuse serde::{Serialize, Deserialize};\nuse crate::auth::PartialUserInfo;\nuse crate::config::TFConfig;\nuse crate::tokens::{create_totp_token, user_has_totp};\nuse rocket::options;\n\n#[derive(Deserialize)]\npub struct TotpAuthenticatorsRequest {}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponseMetadata {}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponseData {\n    #[serde(rename = \"totpToken\")]\n    pub totp_token: String,\n    pub secret: String,\n    pub url: String,\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponse {\n    pub data: TotpAuthenticatorsResponseData,\n    pub metadata: TotpAuthenticatorsResponseMetadata,\n}\n\n#[options(\"/v1/totp-authenticators\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/totp-authenticators\", data = \"\u003c_req\u003e\")]\npub async fn totp_authenticators_request(_req: Json\u003cTotpAuthenticatorsRequest\u003e, user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cTotpAuthenticatorsResponse\u003e), (Status, String)\u003e {\n    if match user_has_totp(user.user_id, db.inner()).await {\n        Ok(b) =\u003e b,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    } {\n        return Err((Status::BadRequest, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_TOTP_ALREADY_EXISTS\", \"this user already has a totp authenticator on their account\")))\n    }\n\n    // generate a totp token\n    let (totptoken, totpmachine) = match create_totp_token(user.email, db.inner(), config.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured issuing a totp token, try again later\", e)))\n    };\n\n    Ok((ContentType::JSON, Json(TotpAuthenticatorsResponse {\n        data: TotpAuthenticatorsResponseData {\n            totp_token: totptoken,\n            secret: totpmachine.get_secret_base32(),\n            url: totpmachine.get_url(),\n        },\n        metadata: TotpAuthenticatorsResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","verify_totp_authenticator.rs"],"content":"/*\n{\"totpToken\":\"totp-mH9eLzA9Q5WB-sg3Fq8CfkP13eTh3DxF25kVK2VEDOk\",\"code\":\"266242\"}\n{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_TOKEN\",\"message\":\"TOTP token does not exist (maybe it expired?)\",\"path\":\"totpToken\"}]}\n\n\n{\"totpToken\":\"totp-gaUDaxPrrIBc8GEQ6z0vPisT8k0MEP1fgI8FA2ztLMw\",\"code\":\"175543\"}\n{\"data\":{\"authToken\":\"auth-O7mugxdYta-RKtLMqDW4j8XCJ85EfZKKezeZZXBYtFQ\"},\"metadata\":{}}\n*/\n\nuse rocket::http::{ContentType, Status};\nuse crate::auth::PartialUserInfo;\nuse rocket::post;\nuse rocket::serde::json::Json;\nuse rocket::State;\nuse serde::{Serialize, Deserialize};\nuse sqlx::PgPool;\nuse crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token};\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorRequest {\n    #[serde(rename = \"totpToken\")]\n    pub totp_token: String,\n    pub code: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponseData {\n    #[serde(rename = \"authToken\")]\n    pub auth_token: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponse {\n    pub data: VerifyTotpAuthenticatorResponseData,\n    pub metadata: VerifyTotpAuthenticatorResponseMetadata,\n}\n\n#[options(\"/v1/verify-totp-authenticator\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/verify-totp-authenticator\", data = \"\u003creq\u003e\")]\npub async fn verify_totp_authenticator_request(req: Json\u003cVerifyTotpAuthenticatorRequest\u003e, db: \u0026State\u003cPgPool\u003e, user: PartialUserInfo) -\u003e Result\u003c(ContentType, Json\u003cVerifyTotpAuthenticatorResponse\u003e), (Status, String)\u003e {\n    let totpmachine = match verify_totp_token(req.0.totp_token.clone(), user.email.clone(), db.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"this token is invalid\", e)))\n    };\n    if !totpmachine.check_current(\u0026req.0.code).unwrap() {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\",\\\"path\\\":\\\"totpToken\\\"}}]}}\", \"ERR_INVALID_TOTP_CODE\", \"Invalid TOTP code\")))\n    }\n    match use_totp_token(req.0.totp_token, user.email, db.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    }\n    Ok((ContentType::JSON, Json(VerifyTotpAuthenticatorResponse {\n        data: VerifyTotpAuthenticatorResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {\n            Ok(at) =\u003e at,\n            Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        } },\n        metadata: VerifyTotpAuthenticatorResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v2","mod.rs"],"content":"pub mod whoami;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v2","whoami.rs"],"content":"use chrono::{NaiveDateTime, Utc};\nuse serde::{Serialize, Deserialize};\nuse rocket::{options, get, State};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse sqlx::PgPool;\nuse crate::auth::PartialUserInfo;\nuse crate::tokens::user_has_totp;\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiActor {\n    pub id: String,\n    #[serde(rename = \"organizationID\")]\n    pub organization_id: String,\n    pub email: String,\n    #[serde(rename = \"createdAt\")]\n    pub created_at: String,\n    #[serde(rename = \"hasTOTPAuthenticator\")]\n    pub has_totpauthenticator: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiData {\n    #[serde(rename = \"actorType\")]\n    pub actor_type: String,\n    pub actor: WhoamiActor,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiResponse {\n    pub data: WhoamiData,\n    pub metadata: WhoamiMetadata,\n}\n\n#[options(\"/v2/whoami\")]\npub fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[get(\"/v2/whoami\")]\npub async fn whoami_request(user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cWhoamiResponse\u003e), (Status, String)\u003e {\n    Ok((ContentType::JSON, Json(WhoamiResponse {\n        data: WhoamiData {\n            actor_type: \"user\".to_string(),\n            actor: WhoamiActor {\n                id: user.user_id.to_string(),\n                organization_id: \"TEMP_ORG_BECAUSE_THAT_ISNT_IMPLEMENTED_YET\".to_string(),\n                email: user.email,\n                created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),\n                has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {\n                    Ok(b) =\u003e b,\n                    Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_DBERROR\", \"an error occured trying to verify your user\", e)))\n                },\n            }\n        },\n        metadata: WhoamiMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","tokens.rs"],"content":"use std::error::Error;\nuse log::info;\nuse sqlx::PgPool;\nuse uuid::Uuid;\nuse crate::config::TFConfig;\nuse std::time::SystemTime;\nuse std::time::UNIX_EPOCH;\nuse totp_rs::{Secret, TOTP};\nuse crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP};\n\n// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com\u0026token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg\npub async fn send_magic_link(id: i64, email: String, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    let otp = format!(\"ml-{}\", Uuid::new_v4());\n    let otp_url = config.web_root.join(\u0026format!(\"/auth/magic-link?email={}\u0026token={}\", urlencoding::encode(\u0026email.clone()), otp.clone())).unwrap();\n    sqlx::query!(\"INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;\n    // TODO: send email\n    info!(\"sent magic link {} to {}, valid for {} seconds\", otp_url, email.clone(), config.magic_links_valid_for);\n    Ok(())\n}\n\npub async fn generate_session_token(user_id: i64, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n    let token = format!(\"st-{}\", Uuid::new_v4());\n    sqlx::query!(\"INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;\n    Ok(token)\n}\npub async fn validate_session_token(token: String, db: \u0026PgPool) -\u003e Result\u003ci64, Box\u003cdyn Error\u003e\u003e {\n    Ok(sqlx::query!(\"SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on \u003e $2\", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)\n}\n\npub async fn generate_auth_token(user_id: i64, session_id: String, db: \u0026PgPool) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n    let token = format!(\"at-{}\", Uuid::new_v4());\n    sqlx::query!(\"INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", token, session_id, user_id as i32).execute(db).await?;\n    Ok(token)\n}\npub async fn validate_auth_token(token: String, session_id: String, db: \u0026PgPool) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    validate_session_token(session_id.clone(), db).await?;\n    sqlx::query!(\"SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2\", token, session_id).fetch_one(db).await?;\n    Ok(())\n}\n\n\n/*\nCREATE TABLE totp_create_tokens (\n    id VARCHAR(39) NOT NULL PRIMARY KEY,\n    expires_on INTEGER NOT NULL,\n    totp_otpurl VARCHAR(3000) NOT NULL,\n    totp_secret VARCHAR(128) NOT NULL\n);\n */\n\npub async fn create_totp_token(email: String, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003c(String, TOTP), Box\u003cdyn Error\u003e\u003e {\n    // create the TOTP parameters\n\n    let secret = Secret::generate_secret();\n    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();\n    let otpurl = totpmachine.get_url();\n    let otpsecret = totpmachine.get_secret_base32();\n\n    let otpid = format!(\"totp-{}\", Uuid::new_v4());\n\n    sqlx::query!(\"INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);\", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;\n\n    Ok((otpid, totpmachine))\n}\n\npub async fn verify_totp_token(otpid: String, email: String, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let totprow = sqlx::query!(\"SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on \u003e $2\", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?;\n    let secret = Secret::Encoded(totprow.totp_secret);\n    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();\n\n    if totpmachine.get_url() != totprow.totp_otpurl {\n        return Err(\"OTPURLs do not match (email does not match?)\".into())\n    }\n\n    Ok(totpmachine)\n}\n\npub async fn use_totp_token(otpid: String, email: String, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let totpmachine = verify_totp_token(otpid.clone(), email.clone(), db).await?;\n    sqlx::query!(\"DELETE FROM totp_create_tokens WHERE id = $1\", otpid).execute(db).await?;\n    sqlx::query!(\"UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3\", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;\n    Ok(totpmachine)\n}\n\npub async fn get_totpmachine(user: i32, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let user = sqlx::query!(\"SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1\", user).fetch_one(db).await?;\n    let secret = Secret::Encoded(user.totp_secret);\n    Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())\n}\n\npub async fn user_has_totp(user: i32, db: \u0026PgPool) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n    Ok(sqlx::query!(\"SELECT totp_verified FROM users WHERE id = $1\", user).fetch_one(db).await?.totp_verified == 1)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","util.rs"],"content":"use base64::Engine;\nuse totp_rs::Algorithm;\n\npub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1;\npub const TOTP_DIGITS: usize = 6;\npub const TOTP_SKEW: u8 = 1;\npub const TOTP_STEP: u64 = 30;\npub const TOTP_ISSUER: \u0026str = \"trifidapi\";\n\npub fn base64decode(val: \u0026str) -\u003e Result\u003cVec\u003cu8\u003e, base64::DecodeError\u003e {\n    base64::engine::general_purpose::STANDARD.decode(val)\n}\npub fn base64encode(val: Vec\u003cu8\u003e) -\u003e String {\n    base64::engine::general_purpose::STANDARD.encode(val)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","ca.rs"],"content":"//! Structs to represent a pool of CA's and blacklisted certificates\n\nuse std::collections::HashMap;\nuse std::error::Error;\nuse std::fmt::{Display, Formatter};\nuse std::time::SystemTime;\nuse ed25519_dalek::VerifyingKey;\nuse crate::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate};\n\n/// A pool of trusted CA certificates, and certificates that should be blocked.\n/// This is equivalent to the `pki` section in a typical Nebula config.yml.\n#[derive(Default)]\npub struct NebulaCAPool {\n    /// The list of CA root certificates that should be trusted.\n    pub cas: HashMap\u003cString, NebulaCertificate\u003e,\n    /// The list of blocklisted certificate fingerprints\n    pub cert_blocklist: Vec\u003cString\u003e,\n    /// True if any of the member CAs certificates are expired. Must be handled.\n    pub expired: bool\n}\n\nimpl NebulaCAPool {\n    /// Create a new, blank CA pool\n    pub fn new() -\u003e Self {\n        Self::default()\n    }\n\n    /// Create a new CA pool from a set of PEM encoded CA certificates.\n    /// If any of the certificates are expired, the pool will **still be returned**, with the expired flag set.\n    /// This must be handled properly.\n    /// # Errors\n    /// This function will return an error if PEM data provided was invalid.\n    pub fn new_from_pem(bytes: \u0026[u8]) -\u003e Result\u003cSelf, Box\u003cdyn Error\u003e\u003e {\n        let pems = pem::parse_many(bytes)?;\n\n        let mut pool = Self::new();\n\n        for cert in pems {\n            match pool.add_ca_certificate(pem::encode(\u0026cert).as_bytes()) {\n                Ok(did_expire) =\u003e if did_expire { pool.expired = true },\n                Err(e) =\u003e return Err(e)\n            }\n        }\n\n        Ok(pool)\n    }\n\n    /// Add a given CA certificate to the CA pool. If the certificate is expired, it will **still be added** - the return value will be `true` instead of `false`\n    /// # Errors\n    /// This function will return an error if the certificate is invalid in any way.\n    pub fn add_ca_certificate(\u0026mut self, bytes: \u0026[u8]) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n        let cert = deserialize_nebula_certificate_from_pem(bytes)?;\n\n        if !cert.details.is_ca {\n            return Err(CaPoolError::NotACA.into())\n        }\n\n        if !cert.check_signature(\u0026VerifyingKey::from_bytes(\u0026cert.details.public_key)?)? {\n            return Err(CaPoolError::NotSelfSigned.into())\n        }\n\n        let fingerprint = cert.sha256sum()?;\n        let expired = cert.expired(SystemTime::now());\n\n        if expired { self.expired = true }\n\n        self.cas.insert(fingerprint, cert);\n\n       Ok(expired)\n    }\n\n    /// Blocklist the given certificate in the CA pool\n    pub fn blocklist_fingerprint(\u0026mut self, fingerprint: \u0026str) {\n        self.cert_blocklist.push(fingerprint.to_string());\n    }\n\n    /// Clears the list of blocklisted fingerprints\n    pub fn reset_blocklist(\u0026mut self) {\n        self.cert_blocklist = vec![];\n    }\n\n    /// Checks if the given certificate is blocklisted\n    pub fn is_blocklisted(\u0026self, cert: \u0026NebulaCertificate) -\u003e bool {\n        let Ok(h) = cert.sha256sum() else { return false };\n        self.cert_blocklist.contains(\u0026h)\n    }\n\n    /// Gets the CA certificate used to sign the given certificate\n    /// # Errors\n    /// This function will return an error if the certificate does not have an issuer attached (it is self-signed)\n    pub fn get_ca_for_cert(\u0026self, cert: \u0026NebulaCertificate) -\u003e Result\u003cOption\u003c\u0026NebulaCertificate\u003e, Box\u003cdyn Error\u003e\u003e {\n        if cert.details.issuer == String::new() {\n            return Err(CaPoolError::NoIssuer.into())\n        }\n\n        Ok(self.cas.get(\u0026cert.details.issuer))\n    }\n\n    /// Get a list of trusted CA fingerprints\n    pub fn get_fingerprints(\u0026self) -\u003e Vec\u003c\u0026String\u003e {\n        self.cas.keys().collect()\n    }\n}\n\n#[derive(Debug)]\n/// A list of errors that can happen when working with a CA Pool\npub enum CaPoolError {\n    /// Tried to add a non-CA cert to the CA pool\n    NotACA,\n    /// Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates)\n    NotSelfSigned,\n    /// Tried to look up a certificate that does not have an issuer field\n    NoIssuer\n}\nimpl Error for CaPoolError {}\n#[cfg(not(tarpaulin_include))]\nimpl Display for CaPoolError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::NotACA =\u003e write!(f, \"Tried to add a non-CA cert to the CA pool\"),\n            Self::NotSelfSigned =\u003e write!(f, \"Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates)\"),\n            Self::NoIssuer =\u003e write!(f, \"Tried to look up a certificate with a null issuer field\")\n        }\n    }\n}","traces":[{"line":24,"address":[453696],"length":1,"stats":{"Line":1},"fn_name":"new"},{"line":25,"address":[453704],"length":1,"stats":{"Line":1},"fn_name":null},{"line":33,"address":[454852,453728,454538],"length":1,"stats":{"Line":1},"fn_name":"new_from_pem"},{"line":34,"address":[453761,453895],"length":1,"stats":{"Line":1},"fn_name":null},{"line":36,"address":[453885],"length":1,"stats":{"Line":1},"fn_name":null},{"line":38,"address":[454117,454667,454023,454788],"length":1,"stats":{"Line":3},"fn_name":null},{"line":39,"address":[454339,454418,454500],"length":1,"stats":{"Line":3},"fn_name":null},{"line":40,"address":[454549,454759],"length":1,"stats":{"Line":2},"fn_name":null},{"line":41,"address":[454586],"length":1,"stats":{"Line":0},"fn_name":null},{"line":45,"address":[454793],"length":1,"stats":{"Line":1},"fn_name":null},{"line":51,"address":[456302,454880],"length":1,"stats":{"Line":1},"fn_name":"add_ca_certificate"},{"line":52,"address":[455110,454952],"length":1,"stats":{"Line":1},"fn_name":null},{"line":54,"address":[455090],"length":1,"stats":{"Line":1},"fn_name":null},{"line":55,"address":[455279,455190],"length":1,"stats":{"Line":2},"fn_name":null},{"line":58,"address":[455546,455183,455304],"length":1,"stats":{"Line":3},"fn_name":null},{"line":59,"address":[455715],"length":1,"stats":{"Line":0},"fn_name":null},{"line":62,"address":[455708,455791,455909],"length":1,"stats":{"Line":2},"fn_name":null},{"line":63,"address":[455881,456066],"length":1,"stats":{"Line":2},"fn_name":null},{"line":65,"address":[456216,456093],"length":1,"stats":{"Line":2},"fn_name":null},{"line":67,"address":[456230,456097],"length":1,"stats":{"Line":3},"fn_name":null},{"line":69,"address":[456246],"length":1,"stats":{"Line":1},"fn_name":null},{"line":73,"address":[456352],"length":1,"stats":{"Line":1},"fn_name":"blocklist_fingerprint"},{"line":74,"address":[456371],"length":1,"stats":{"Line":1},"fn_name":null},{"line":78,"address":[456480,456416],"length":1,"stats":{"Line":1},"fn_name":"reset_blocklist"},{"line":79,"address":[456512,456434],"length":1,"stats":{"Line":2},"fn_name":null},{"line":83,"address":[456544,456832,456804],"length":1,"stats":{"Line":2},"fn_name":"is_blocklisted"},{"line":84,"address":[456686,456566,456743],"length":1,"stats":{"Line":2},"fn_name":null},{"line":85,"address":[456765,456713],"length":1,"stats":{"Line":4},"fn_name":null},{"line":91,"address":[456848,456982],"length":1,"stats":{"Line":1},"fn_name":"get_ca_for_cert"},{"line":92,"address":[456881],"length":1,"stats":{"Line":1},"fn_name":null},{"line":93,"address":[457047],"length":1,"stats":{"Line":0},"fn_name":null},{"line":96,"address":[457014],"length":1,"stats":{"Line":1},"fn_name":null},{"line":100,"address":[457088],"length":1,"stats":{"Line":1},"fn_name":"get_fingerprints"},{"line":101,"address":[457107],"length":1,"stats":{"Line":1},"fn_name":null}],"covered":31,"coverable":34},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","cert.rs"],"content":"//! Manage Nebula PKI Certificates\n//! This is pretty much a direct port of nebula/cert/cert.go\n\nuse std::error::Error;\nuse std::fmt::{Display, Formatter};\nuse std::net::Ipv4Addr;\nuse std::ops::Add;\nuse std::time::{Duration, SystemTime, UNIX_EPOCH};\nuse ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};\nuse ipnet::{Ipv4Net};\nuse pem::Pem;\nuse quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};\nuse sha2::Sha256;\nuse crate::ca::NebulaCAPool;\nuse crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};\nuse sha2::Digest;\n\n/// The length, in bytes, of public keys\npub const PUBLIC_KEY_LENGTH: i32 = 32;\n\n/// The PEM banner for certificates\npub const CERT_BANNER: \u0026str = \"NEBULA CERTIFICATE\";\n/// The PEM banner for X25519 private keys\npub const X25519_PRIVATE_KEY_BANNER: \u0026str = \"NEBULA X25519 PRIVATE KEY\";\n/// The PEM banner for X25519 public keys\npub const X25519_PUBLIC_KEY_BANNER: \u0026str = \"NEBULA X25519 PUBLIC KEY\";\n/// The PEM banner for Ed25519 private keys\npub const ED25519_PRIVATE_KEY_BANNER: \u0026str = \"NEBULA ED25519 PRIVATE KEY\";\n/// The PEM banner for Ed25519 public keys\npub const ED25519_PUBLIC_KEY_BANNER: \u0026str = \"NEBULA ED25519 PUBLIC KEY\";\n\n/// A Nebula PKI certificate\n#[derive(Debug, Clone)]\npub struct NebulaCertificate {\n    /// The signed data of this certificate\n    pub details: NebulaCertificateDetails,\n    /// The Ed25519 signature of this certificate\n    pub signature: Vec\u003cu8\u003e\n}\n\n/// The signed details contained in a Nebula PKI certificate\n#[derive(Debug, Clone)]\npub struct NebulaCertificateDetails {\n    /// The name of the identity this certificate was issued for\n    pub name: String,\n    /// The IPv4 addresses issued to this node\n    pub ips: Vec\u003cIpv4Net\u003e,\n    /// The IPv4 subnets this node is responsible for routing\n    pub subnets: Vec\u003cIpv4Net\u003e,\n    /// The groups this node is a part of\n    pub groups: Vec\u003cString\u003e,\n    /// Certificate start date and time\n    pub not_before: SystemTime,\n    /// Certificate expiry date and time\n    pub not_after: SystemTime,\n    /// Public key\n    pub public_key: [u8; PUBLIC_KEY_LENGTH as usize],\n    /// Is this node a CA?\n    pub is_ca: bool,\n    /// SHA256 of issuer certificate. If blank, this cert is self-signed.\n    pub issuer: String\n}\n\n/// A list of errors that can occur parsing certificates\n#[derive(Debug)]\npub enum CertificateError {\n    /// Attempted to deserialize a certificate from an empty byte array\n    EmptyByteArray,\n    /// The encoded Details field is null\n    NilDetails,\n    /// The encoded Ips field is not formatted correctly\n    IpsNotPairs,\n    /// The encoded Subnets field is not formatted correctly\n    SubnetsNotPairs,\n    /// Signatures are expected to be 64 bytes but the signature on the certificate was a different length\n    WrongSigLength,\n    /// Public keys are expected to be 32 bytes but the public key on this cert is not\n    WrongKeyLength,\n    /// Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not\n    WrongPemTag,\n    /// This certificate either is not yet valid or has already expired\n    Expired,\n    /// The public key does not match the expected value\n    KeyMismatch\n}\n#[cfg(not(tarpaulin_include))]\nimpl Display for CertificateError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::EmptyByteArray =\u003e write!(f, \"Certificate bytearray is empty\"),\n            Self::NilDetails =\u003e write!(f, \"The encoded Details field is null\"),\n            Self::IpsNotPairs =\u003e write!(f, \"encoded IPs should be in pairs, an odd number was found\"),\n            Self::SubnetsNotPairs =\u003e write!(f, \"encoded subnets should be in pairs, an odd number was found\"),\n            Self::WrongSigLength =\u003e write!(f, \"Signature should be 64 bytes but is a different size\"),\n            Self::WrongKeyLength =\u003e write!(f, \"Public keys are expected to be 32 bytes but the public key on this cert is not\"),\n            Self::WrongPemTag =\u003e write!(f, \"Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not\"),\n            Self::Expired =\u003e write!(f, \"This certificate either is not yet valid or has already expired\"),\n            Self::KeyMismatch =\u003e write!(f, \"Key does not match expected value\")\n        }\n    }\n}\nimpl Error for CertificateError {}\n\nfn map_cidr_pairs(pairs: \u0026[u32]) -\u003e Result\u003cVec\u003cIpv4Net\u003e, Box\u003cdyn Error\u003e\u003e {\n    let mut res_vec = vec![];\n    for pair in pairs.chunks(2) {\n        res_vec.push(Ipv4Net::with_netmask(Ipv4Addr::from(pair[0]), Ipv4Addr::from(pair[1]))?);\n    }\n    Ok(res_vec)\n}\n\n#[cfg(not(tarpaulin_include))]\nimpl Display for NebulaCertificate {\n    #[allow(clippy::unwrap_used)]\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        writeln!(f, \"NebulaCertificate {{\")?;\n        writeln!(f, \"   Details {{\")?;\n        writeln!(f, \"       Name: {}\", self.details.name)?;\n        writeln!(f, \"       Ips: {:?}\", self.details.ips)?;\n        writeln!(f, \"       Subnets: {:?}\", self.details.subnets)?;\n        writeln!(f, \"       Groups: {:?}\", self.details.groups)?;\n        writeln!(f, \"       Not before: {:?}\", self.details.not_before)?;\n        writeln!(f, \"       Not after: {:?}\", self.details.not_after)?;\n        writeln!(f, \"       Is CA: {}\", self.details.is_ca)?;\n        writeln!(f, \"       Issuer: {}\", self.details.issuer)?;\n        writeln!(f, \"       Public key: {}\", hex::encode(self.details.public_key))?;\n        writeln!(f, \"   }}\")?;\n        writeln!(f, \"   Fingerprint: {}\", self.sha256sum().unwrap())?;\n        writeln!(f, \"   Signature: {}\", hex::encode(self.signature.clone()))?;\n        writeln!(f, \"}}\")\n    }\n}\n\n/// Given a protobuf-encoded certificate bytearray, deserialize it into a `NebulaCertificate` object.\n/// # Errors\n/// This function will return an error if there is a protobuf parsing error, or if the certificate data is invalid.\n/// # Panics\npub fn deserialize_nebula_certificate(bytes: \u0026[u8]) -\u003e Result\u003cNebulaCertificate, Box\u003cdyn Error\u003e\u003e {\n    if bytes.is_empty() {\n        return Err(CertificateError::EmptyByteArray.into())\n    }\n\n    let mut reader = BytesReader::from_bytes(bytes);\n\n    let raw_cert = RawNebulaCertificate::from_reader(\u0026mut reader, bytes)?;\n\n    let details = raw_cert.Details.ok_or(CertificateError::NilDetails)?;\n\n    if details.Ips.len() % 2 != 0 {\n        return Err(CertificateError::IpsNotPairs.into())\n    }\n\n    if details.Subnets.len() % 2 != 0 {\n        return Err(CertificateError::SubnetsNotPairs.into())\n    }\n\n    let mut nebula_cert;\n    #[allow(clippy::cast_sign_loss)]\n    {\n        nebula_cert = NebulaCertificate {\n            details: NebulaCertificateDetails {\n                name: details.Name.to_string(),\n                ips: map_cidr_pairs(\u0026details.Ips)?,\n                subnets: map_cidr_pairs(\u0026details.Subnets)?,\n                groups: details.Groups.iter().map(std::string::ToString::to_string).collect(),\n                not_before: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotBefore as u64)),\n                not_after: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotAfter as u64)),\n                public_key: [0u8; 32],\n                is_ca: details.IsCA,\n                issuer: hex::encode(details.Issuer),\n            },\n            signature: vec![],\n        };\n    }\n\n    nebula_cert.signature = raw_cert.Signature;\n\n    if details.PublicKey.len() != 32 {\n        return Err(CertificateError::WrongKeyLength.into())\n    }\n\n    #[allow(clippy::unwrap_used)] { nebula_cert.details.public_key = details.PublicKey.try_into().unwrap(); }\n\n    Ok(nebula_cert)\n}\n\n/// A list of errors that can occur parsing keys\n#[derive(Debug)]\npub enum KeyError {\n    /// Keys should have their associated PEM tags but this had the wrong one\n    WrongPemTag,\n    /// Ed25519 private keys are 64 bytes\n    Not64Bytes,\n    /// X25519 private keys are 32 bytes\n    Not32Bytes\n}\n#[cfg(not(tarpaulin_include))]\nimpl Display for KeyError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::WrongPemTag =\u003e write!(f, \"Keys should have their associated PEM tags but this had the wrong one\"),\n            Self::Not64Bytes =\u003e write!(f, \"Ed25519 private keys are 64 bytes\"),\n            Self::Not32Bytes =\u003e write!(f, \"X25519 private keys are 32 bytes\")\n        }\n    }\n}\nimpl Error for KeyError {}\n\n\n/// Deserialize the first PEM block in the given byte array into a `NebulaCertificate`\n/// # Errors\n/// This function will return an error if the PEM data is invalid, or if there is an error parsing the certificate (see `deserialize_nebula_certificate`)\npub fn deserialize_nebula_certificate_from_pem(bytes: \u0026[u8]) -\u003e Result\u003cNebulaCertificate, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != CERT_BANNER {\n        return Err(CertificateError::WrongPemTag.into())\n    }\n    deserialize_nebula_certificate(\u0026pem.contents)\n}\n\n/// Simple helper to PEM encode an X25519 private key\npub fn serialize_x25519_private(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: X25519_PRIVATE_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Simple helper to PEM encode an X25519 public key\npub fn serialize_x25519_public(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: X25519_PUBLIC_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Attempt to deserialize a PEM encoded X25519 private key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_x25519_private(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != X25519_PRIVATE_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 32 {\n        return Err(KeyError::Not32Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Attempt to deserialize a PEM encoded X25519 public key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_x25519_public(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != X25519_PUBLIC_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 32 {\n        return Err(KeyError::Not32Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Simple helper to PEM encode an Ed25519 private key\npub fn serialize_ed25519_private(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: ED25519_PRIVATE_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Simple helper to PEM encode an Ed25519 public key\npub fn serialize_ed25519_public(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: ED25519_PUBLIC_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Attempt to deserialize a PEM encoded Ed25519 private key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_ed25519_private(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != ED25519_PRIVATE_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 64 {\n        return Err(KeyError::Not64Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Attempt to deserialize a PEM encoded Ed25519 public key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_ed25519_public(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != ED25519_PUBLIC_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 64 {\n        return Err(KeyError::Not64Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\nimpl NebulaCertificate {\n    /// Sign a nebula certificate with the provided private key\n    /// # Errors\n    /// This function will return an error if the certificate could not be serialized or signed.\n    pub fn sign(\u0026mut self, key: \u0026SigningKey) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n        let mut out = Vec::new();\n        let mut writer = Writer::new(\u0026mut out);\n        self.get_raw_details().write_message(\u0026mut writer)?;\n\n        self.signature = key.sign(\u0026out).to_vec();\n        Ok(())\n    }\n\n    /// Verify the signature on a certificate with the provided public key\n    /// # Errors\n    /// This function will return an error if the certificate could not be serialized or the signature could not be checked.\n    pub fn check_signature(\u0026self, key: \u0026VerifyingKey) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n        let mut out = Vec::new();\n        let mut writer = Writer::new(\u0026mut out);\n        self.get_raw_details().write_message(\u0026mut writer)?;\n\n        let sig = Signature::from_slice(\u0026self.signature)?;\n\n        Ok(key.verify(\u0026out, \u0026sig).is_ok())\n    }\n\n    /// Returns true if the signature is too young or too old compared to the provided time\n    pub fn expired(\u0026self, time: SystemTime) -\u003e bool {\n        self.details.not_before \u003e time || self.details.not_after \u003c time\n    }\n\n    /// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)\n    /// # Errors\n    /// This function will return an error if there is an error parsing the cert or the CA pool.\n    pub fn verify(\u0026self, time: SystemTime, ca_pool: \u0026NebulaCAPool) -\u003e Result\u003cCertificateValidity, Box\u003cdyn Error\u003e\u003e {\n        if ca_pool.is_blocklisted(self) {\n            return Ok(CertificateValidity::Blocklisted);\n        }\n\n        let Some(signer) = ca_pool.get_ca_for_cert(self)? else { return Ok(CertificateValidity::NotSignedByThisCAPool) };\n\n        if signer.expired(time) {\n            return Ok(CertificateValidity::RootCertExpired)\n        }\n\n        if self.expired(time) {\n            return Ok(CertificateValidity::CertExpired)\n        }\n\n        if !self.check_signature(\u0026VerifyingKey::from_bytes(\u0026signer.details.public_key)?)? {\n            return Ok(CertificateValidity::BadSignature)\n        }\n\n        Ok(self.check_root_constraints(signer))\n    }\n\n    /// Make sure that this certificate does not break any of the constraints set by the signing certificate\n    pub fn check_root_constraints(\u0026self, signer: \u0026Self) -\u003e CertificateValidity {\n        // Make sure this cert doesn't expire after the signer\n        println!(\"{:?} {:?}\", signer.details.not_before, self.details.not_before);\n        if signer.details.not_before \u003c self.details.not_before {\n            return CertificateValidity::CertExpiresAfterSigner;\n        }\n\n        // Make sure this cert doesn't come into validity before the root\n        if signer.details.not_before \u003e self.details.not_before {\n            return CertificateValidity::CertValidBeforeSigner;\n        }\n\n        // If the signer contains a limited set of groups, make sure this cert only has a subset of them\n        if !signer.details.groups.is_empty() {\n            println!(\"root groups: {:?}, child groups: {:?}\", signer.details.groups, self.details.groups);\n            for group in \u0026self.details.groups {\n                if !signer.details.groups.contains(group) {\n                    return CertificateValidity::GroupNotPresentOnSigner;\n                }\n            }\n        }\n\n        // If the signer contains a limited set of IP ranges, make sure the cert only contains a subset\n        if !signer.details.ips.is_empty() {\n            for ip in \u0026self.details.ips {\n                if !net_match(*ip, \u0026signer.details.ips) {\n                    return CertificateValidity::IPNotPresentOnSigner;\n                }\n            }\n        }\n\n        // If the signer contains a limited set of subnets, make sure the cert only contains a subset\n        if !signer.details.subnets.is_empty() {\n            for subnet in \u0026self.details.subnets {\n                if !net_match(*subnet, \u0026signer.details.subnets) {\n                    return CertificateValidity::SubnetNotPresentOnSigner;\n                }\n            }\n        }\n\n        CertificateValidity::Ok\n    }\n\n    #[allow(clippy::unwrap_used)]\n    /// Verify if the given private key corresponds to the public key contained in this certificate\n    /// # Errors\n    /// This function will return an error if either keys are invalid.\n    /// # Panics\n    /// This function, while containing calls to unwrap, has proper bounds checking and will not panic.\n    pub fn verify_private_key(\u0026self, key: \u0026[u8]) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n        if self.details.is_ca {\n            // convert the keys\n            if key.len() != 64 {\n                return Err(\"key not 64-bytes long\".into())\n            }\n\n\n            let secret = SigningKey::from_keypair_bytes(key.try_into().unwrap())?;\n            let pub_key = secret.verifying_key().to_bytes();\n            if pub_key != self.details.public_key {\n                return Err(CertificateError::KeyMismatch.into());\n            }\n\n            return Ok(());\n        }\n\n        if key.len() != 32 {\n            return Err(\"key not 32-bytes long\".into())\n        }\n\n        let pubkey_raw = SigningKey::from_bytes(key.try_into()?).verifying_key();\n        let pubkey = pubkey_raw.as_bytes();\n\n        println!(\"{} {}\", hex::encode(pubkey), hex::encode(self.details.public_key));\n        if *pubkey != self.details.public_key {\n            return Err(CertificateError::KeyMismatch.into());\n        }\n\n        Ok(())\n    }\n\n\n    /// Get a protobuf-ready raw struct, ready for serialization\n    #[allow(clippy::expect_used)]\n    #[allow(clippy::cast_possible_wrap)]\n    /// # Panics\n    /// This function will panic if time went backwards, or if the certificate contains extremely invalid data.\n    pub fn get_raw_details(\u0026self) -\u003e RawNebulaCertificateDetails {\n\n        let mut raw = RawNebulaCertificateDetails {\n            Name: self.details.name.clone(),\n            Ips: vec![],\n            Subnets: vec![],\n            Groups: self.details.groups.iter().map(std::convert::Into::into).collect(),\n            NotBefore: self.details.not_before.duration_since(UNIX_EPOCH).expect(\"Time went backwards\").as_secs() as i64,\n            NotAfter: self.details.not_after.duration_since(UNIX_EPOCH).expect(\"Time went backwards\").as_secs() as i64,\n            PublicKey: self.details.public_key.into(),\n            IsCA: self.details.is_ca,\n            Issuer: hex::decode(\u0026self.details.issuer).expect(\"Issuer was not a hex-encoded value\"),\n        };\n\n        for ip_net in \u0026self.details.ips {\n            raw.Ips.push(ip_net.addr().into());\n            raw.Ips.push(ip_net.netmask().into());\n        }\n\n        for subnet in \u0026self.details.subnets {\n            raw.Subnets.push(subnet.addr().into());\n            raw.Subnets.push(subnet.netmask().into());\n        }\n\n        raw\n    }\n\n    /// Will serialize this cert into a protobuf byte array.\n    /// # Errors\n    /// This function will return an error if protobuf was unable to serialize the data.\n    pub fn serialize(\u0026self) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n        let raw_cert = RawNebulaCertificate {\n            Details: Some(self.get_raw_details()),\n            Signature: self.signature.clone(),\n        };\n\n        let mut out = vec![];\n        let mut writer = Writer::new(\u0026mut out);\n        raw_cert.write_message(\u0026mut writer)?;\n\n        Ok(out)\n    }\n\n    /// Will serialize this cert into a PEM byte array.\n    /// # Errors\n    /// This function will return an error if protobuf was unable to serialize the data.\n    pub fn serialize_to_pem(\u0026self) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n        let pbuf_bytes = self.serialize()?;\n\n        Ok(pem::encode(\u0026Pem {\n            tag: CERT_BANNER.to_string(),\n            contents: pbuf_bytes,\n        }).as_bytes().to_vec())\n    }\n\n    /// Get the fingerprint of this certificate\n    /// # Errors\n    /// This functiom will return an error if protobuf was unable to serialize the cert.\n    pub fn sha256sum(\u0026self) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n        let pbuf_bytes = self.serialize()?;\n\n        let mut hasher = Sha256::new();\n        hasher.update(pbuf_bytes);\n\n        Ok(hex::encode(hasher.finalize()))\n    }\n}\n\n/// A list of possible errors that can happen validating a certificate\n#[derive(Eq, PartialEq, Debug)]\npub enum CertificateValidity {\n    /// There are no issues with this certificate\n    Ok,\n    /// This cert has been blocklisted in the given CA pool\n    Blocklisted,\n    /// The certificate that signed this cert is expired\n    RootCertExpired,\n    /// This cert is expired\n    CertExpired,\n    /// This cert's signature is invalid\n    BadSignature,\n    /// This cert was not signed by any CAs in the CA pool\n    NotSignedByThisCAPool,\n    /// This cert expires after the signer's cert expires\n    CertExpiresAfterSigner,\n    /// This cert enters validity before the signer's cert does\n    CertValidBeforeSigner,\n    /// A group present on this certificate is not present on the signer's certificate\n    GroupNotPresentOnSigner,\n    /// An IP present on this certificate is not present on the signer's certificate\n    IPNotPresentOnSigner,\n    /// A subnet on this certificate is not present on the signer's certificate\n    SubnetNotPresentOnSigner\n}\n\nfn net_match(cert_ip: Ipv4Net, root_ips: \u0026Vec\u003cIpv4Net\u003e) -\u003e bool {\n    for net in root_ips {\n        if net.contains(\u0026cert_ip) {\n            return true;\n        }\n    }\n    false\n}","traces":[{"line":104,"address":[548224,549155],"length":1,"stats":{"Line":1},"fn_name":"map_cidr_pairs"},{"line":105,"address":[548267],"length":1,"stats":{"Line":1},"fn_name":null},{"line":106,"address":[548589,548365,548304],"length":1,"stats":{"Line":3},"fn_name":null},{"line":107,"address":[548631,549150],"length":1,"stats":{"Line":4},"fn_name":null},{"line":109,"address":[548502],"length":1,"stats":{"Line":1},"fn_name":null},{"line":138,"address":[553592,557101,552064],"length":1,"stats":{"Line":1},"fn_name":"deserialize_nebula_certificate"},{"line":139,"address":[552141],"length":1,"stats":{"Line":3},"fn_name":null},{"line":140,"address":[552335],"length":1,"stats":{"Line":1},"fn_name":null},{"line":143,"address":[552219],"length":1,"stats":{"Line":1},"fn_name":null},{"line":145,"address":[552263,552449,552598],"length":1,"stats":{"Line":3},"fn_name":null},{"line":147,"address":[552824,553002,552509],"length":1,"stats":{"Line":3},"fn_name":null},{"line":149,"address":[552964,553149],"length":1,"stats":{"Line":3},"fn_name":null},{"line":150,"address":[553186],"length":1,"stats":{"Line":1},"fn_name":null},{"line":153,"address":[553159,553293],"length":1,"stats":{"Line":2},"fn_name":null},{"line":154,"address":[553326],"length":1,"stats":{"Line":1},"fn_name":null},{"line":160,"address":[555110],"length":1,"stats":{"Line":1},"fn_name":null},{"line":161,"address":[554782],"length":1,"stats":{"Line":1},"fn_name":null},{"line":162,"address":[553303],"length":1,"stats":{"Line":1},"fn_name":null},{"line":163,"address":[553603,553449,553713,553549],"length":1,"stats":{"Line":3},"fn_name":null},{"line":164,"address":[553667,553916,554067],"length":1,"stats":{"Line":2},"fn_name":null},{"line":165,"address":[554344,554021],"length":1,"stats":{"Line":2},"fn_name":null},{"line":166,"address":[554526,554437],"length":1,"stats":{"Line":2},"fn_name":null},{"line":167,"address":[554583],"length":1,"stats":{"Line":1},"fn_name":null},{"line":168,"address":[554667],"length":1,"stats":{"Line":1},"fn_name":null},{"line":169,"address":[554686],"length":1,"stats":{"Line":1},"fn_name":null},{"line":170,"address":[554697],"length":1,"stats":{"Line":1},"fn_name":null},{"line":172,"address":[555013],"length":1,"stats":{"Line":1},"fn_name":null},{"line":176,"address":[555185],"length":1,"stats":{"Line":1},"fn_name":null},{"line":178,"address":[555367],"length":1,"stats":{"Line":1},"fn_name":null},{"line":179,"address":[555468],"length":1,"stats":{"Line":1},"fn_name":null},{"line":184,"address":[556429],"length":1,"stats":{"Line":1},"fn_name":null},{"line":213,"address":[557869,557392],"length":1,"stats":{"Line":1},"fn_name":"deserialize_nebula_certificate_from_pem"},{"line":214,"address":[557425,557589],"length":1,"stats":{"Line":2},"fn_name":null},{"line":215,"address":[557721,557563],"length":1,"stats":{"Line":2},"fn_name":null},{"line":216,"address":[557753],"length":1,"stats":{"Line":1},"fn_name":null},{"line":218,"address":[557846,557727],"length":1,"stats":{"Line":2},"fn_name":null},{"line":222,"address":[557904,558115],"length":1,"stats":{"Line":1},"fn_name":"serialize_x25519_private"},{"line":223,"address":[558254,558042,558184],"length":1,"stats":{"Line":3},"fn_name":null},{"line":224,"address":[557947],"length":1,"stats":{"Line":1},"fn_name":null},{"line":225,"address":[557982],"length":1,"stats":{"Line":1},"fn_name":null},{"line":226,"address":[558001],"length":1,"stats":{"Line":0},"fn_name":null},{"line":230,"address":[558531,558320],"length":1,"stats":{"Line":1},"fn_name":"serialize_x25519_public"},{"line":231,"address":[558600,558458,558670],"length":1,"stats":{"Line":3},"fn_name":null},{"line":232,"address":[558363],"length":1,"stats":{"Line":1},"fn_name":null},{"line":233,"address":[558398],"length":1,"stats":{"Line":1},"fn_name":null},{"line":234,"address":[558417],"length":1,"stats":{"Line":0},"fn_name":null},{"line":240,"address":[559351,558736],"length":1,"stats":{"Line":1},"fn_name":"deserialize_x25519_private"},{"line":241,"address":[558933,558769],"length":1,"stats":{"Line":2},"fn_name":null},{"line":242,"address":[559065,558907],"length":1,"stats":{"Line":2},"fn_name":null},{"line":243,"address":[559092],"length":1,"stats":{"Line":1},"fn_name":null},{"line":245,"address":[559071,559176],"length":1,"stats":{"Line":2},"fn_name":null},{"line":246,"address":[559285],"length":1,"stats":{"Line":1},"fn_name":null},{"line":248,"address":[559187],"length":1,"stats":{"Line":1},"fn_name":null},{"line":254,"address":[560007,559392],"length":1,"stats":{"Line":1},"fn_name":"deserialize_x25519_public"},{"line":255,"address":[559425,559589],"length":1,"stats":{"Line":2},"fn_name":null},{"line":256,"address":[559563,559721],"length":1,"stats":{"Line":2},"fn_name":null},{"line":257,"address":[559748],"length":1,"stats":{"Line":1},"fn_name":null},{"line":259,"address":[559727,559832],"length":1,"stats":{"Line":2},"fn_name":null},{"line":260,"address":[559941],"length":1,"stats":{"Line":1},"fn_name":null},{"line":262,"address":[559843],"length":1,"stats":{"Line":1},"fn_name":null},{"line":266,"address":[560048,560259],"length":1,"stats":{"Line":1},"fn_name":"serialize_ed25519_private"},{"line":267,"address":[560398,560186,560328],"length":1,"stats":{"Line":3},"fn_name":null},{"line":268,"address":[560091],"length":1,"stats":{"Line":1},"fn_name":null},{"line":269,"address":[560126],"length":1,"stats":{"Line":1},"fn_name":null},{"line":270,"address":[560145],"length":1,"stats":{"Line":0},"fn_name":null},{"line":274,"address":[560675,560464],"length":1,"stats":{"Line":1},"fn_name":"serialize_ed25519_public"},{"line":275,"address":[560602,560814,560744],"length":1,"stats":{"Line":3},"fn_name":null},{"line":276,"address":[560507],"length":1,"stats":{"Line":1},"fn_name":null},{"line":277,"address":[560542],"length":1,"stats":{"Line":1},"fn_name":null},{"line":278,"address":[560561],"length":1,"stats":{"Line":0},"fn_name":null},{"line":284,"address":[561495,560880],"length":1,"stats":{"Line":1},"fn_name":"deserialize_ed25519_private"},{"line":285,"address":[561077,560913],"length":1,"stats":{"Line":2},"fn_name":null},{"line":286,"address":[561209,561051],"length":1,"stats":{"Line":2},"fn_name":null},{"line":287,"address":[561236],"length":1,"stats":{"Line":1},"fn_name":null},{"line":289,"address":[561215,561320],"length":1,"stats":{"Line":2},"fn_name":null},{"line":290,"address":[561429],"length":1,"stats":{"Line":1},"fn_name":null},{"line":292,"address":[561331],"length":1,"stats":{"Line":2},"fn_name":null},{"line":298,"address":[562151,561536],"length":1,"stats":{"Line":1},"fn_name":"deserialize_ed25519_public"},{"line":299,"address":[561733,561569],"length":1,"stats":{"Line":2},"fn_name":null},{"line":300,"address":[561707,561865],"length":1,"stats":{"Line":2},"fn_name":null},{"line":301,"address":[561892],"length":1,"stats":{"Line":1},"fn_name":null},{"line":303,"address":[561976,561871],"length":1,"stats":{"Line":2},"fn_name":null},{"line":304,"address":[562085],"length":1,"stats":{"Line":1},"fn_name":null},{"line":306,"address":[561987],"length":1,"stats":{"Line":1},"fn_name":null},{"line":313,"address":[562612,562192,562914],"length":1,"stats":{"Line":3},"fn_name":"sign"},{"line":314,"address":[562225],"length":1,"stats":{"Line":3},"fn_name":null},{"line":315,"address":[562249,562312],"length":1,"stats":{"Line":6},"fn_name":null},{"line":316,"address":[562325],"length":1,"stats":{"Line":3},"fn_name":null},{"line":318,"address":[562652],"length":1,"stats":{"Line":3},"fn_name":null},{"line":319,"address":[562889],"length":1,"stats":{"Line":4},"fn_name":null},{"line":325,"address":[563349,563794,562944],"length":1,"stats":{"Line":1},"fn_name":"check_signature"},{"line":326,"address":[562987],"length":1,"stats":{"Line":1},"fn_name":null},{"line":327,"address":[563074,563011],"length":1,"stats":{"Line":2},"fn_name":null},{"line":328,"address":[563087],"length":1,"stats":{"Line":1},"fn_name":null},{"line":330,"address":[563642,563381],"length":1,"stats":{"Line":2},"fn_name":null},{"line":332,"address":[563611,563856,563748],"length":1,"stats":{"Line":3},"fn_name":null},{"line":336,"address":[563952],"length":1,"stats":{"Line":1},"fn_name":"expired"},{"line":337,"address":[563974],"length":1,"stats":{"Line":1},"fn_name":null},{"line":343,"address":[564048],"length":1,"stats":{"Line":1},"fn_name":"verify"},{"line":344,"address":[564123],"length":1,"stats":{"Line":1},"fn_name":null},{"line":345,"address":[564200],"length":1,"stats":{"Line":1},"fn_name":null},{"line":348,"address":[564224,564142,564324,564357],"length":1,"stats":{"Line":5},"fn_name":null},{"line":350,"address":[564341],"length":1,"stats":{"Line":2},"fn_name":null},{"line":351,"address":[564403],"length":1,"stats":{"Line":1},"fn_name":null},{"line":354,"address":[564387],"length":1,"stats":{"Line":1},"fn_name":null},{"line":355,"address":[564479],"length":1,"stats":{"Line":1},"fn_name":null},{"line":358,"address":[564423,564671,564495],"length":1,"stats":{"Line":5},"fn_name":null},{"line":359,"address":[564781],"length":1,"stats":{"Line":0},"fn_name":null},{"line":362,"address":[564749],"length":1,"stats":{"Line":2},"fn_name":null},{"line":366,"address":[564800],"length":1,"stats":{"Line":2},"fn_name":"check_root_constraints"},{"line":368,"address":[564846],"length":1,"stats":{"Line":2},"fn_name":null},{"line":369,"address":[564972],"length":1,"stats":{"Line":2},"fn_name":null},{"line":370,"address":[565018],"length":1,"stats":{"Line":0},"fn_name":null},{"line":374,"address":[564999],"length":1,"stats":{"Line":2},"fn_name":null},{"line":375,"address":[565060],"length":1,"stats":{"Line":0},"fn_name":null},{"line":379,"address":[565040],"length":1,"stats":{"Line":2},"fn_name":null},{"line":380,"address":[565101],"length":1,"stats":{"Line":1},"fn_name":null},{"line":381,"address":[565240],"length":1,"stats":{"Line":1},"fn_name":null},{"line":382,"address":[565348],"length":1,"stats":{"Line":1},"fn_name":null},{"line":383,"address":[565384],"length":1,"stats":{"Line":1},"fn_name":null},{"line":389,"address":[565072],"length":1,"stats":{"Line":1},"fn_name":null},{"line":390,"address":[565428],"length":1,"stats":{"Line":1},"fn_name":null},{"line":391,"address":[565525],"length":1,"stats":{"Line":1},"fn_name":null},{"line":392,"address":[565588],"length":1,"stats":{"Line":1},"fn_name":null},{"line":398,"address":[565399],"length":1,"stats":{"Line":1},"fn_name":null},{"line":399,"address":[565613],"length":1,"stats":{"Line":1},"fn_name":null},{"line":400,"address":[565710],"length":1,"stats":{"Line":1},"fn_name":null},{"line":401,"address":[565773],"length":1,"stats":{"Line":1},"fn_name":null},{"line":406,"address":[565598],"length":1,"stats":{"Line":1},"fn_name":null},{"line":415,"address":[565792,566389],"length":1,"stats":{"Line":1},"fn_name":"verify_private_key"},{"line":416,"address":[565847],"length":1,"stats":{"Line":1},"fn_name":null},{"line":418,"address":[565887],"length":1,"stats":{"Line":1},"fn_name":null},{"line":419,"address":[565983],"length":1,"stats":{"Line":0},"fn_name":null},{"line":423,"address":[566145,566040,565909],"length":1,"stats":{"Line":2},"fn_name":null},{"line":424,"address":[566126,566270],"length":1,"stats":{"Line":2},"fn_name":null},{"line":425,"address":[566293],"length":1,"stats":{"Line":1},"fn_name":null},{"line":426,"address":[566328],"length":1,"stats":{"Line":1},"fn_name":null},{"line":429,"address":[566314],"length":1,"stats":{"Line":1},"fn_name":null},{"line":432,"address":[565864],"length":1,"stats":{"Line":1},"fn_name":null},{"line":433,"address":[566503],"length":1,"stats":{"Line":0},"fn_name":null},{"line":436,"address":[566541,566450],"length":1,"stats":{"Line":2},"fn_name":null},{"line":437,"address":[566701],"length":1,"stats":{"Line":1},"fn_name":null},{"line":439,"address":[566738],"length":1,"stats":{"Line":1},"fn_name":null},{"line":440,"address":[567118],"length":1,"stats":{"Line":1},"fn_name":null},{"line":441,"address":[567144],"length":1,"stats":{"Line":1},"fn_name":null},{"line":444,"address":[567127],"length":1,"stats":{"Line":1},"fn_name":null},{"line":453,"address":[567200,568321],"length":1,"stats":{"Line":6},"fn_name":"get_raw_details"},{"line":456,"address":[567239],"length":1,"stats":{"Line":6},"fn_name":null},{"line":457,"address":[567268],"length":1,"stats":{"Line":5},"fn_name":null},{"line":458,"address":[567327],"length":1,"stats":{"Line":5},"fn_name":null},{"line":459,"address":[567383,567474],"length":1,"stats":{"Line":12},"fn_name":null},{"line":460,"address":[567655,567575],"length":1,"stats":{"Line":12},"fn_name":null},{"line":461,"address":[567765],"length":1,"stats":{"Line":6},"fn_name":null},{"line":462,"address":[567911],"length":1,"stats":{"Line":6},"fn_name":null},{"line":463,"address":[567965],"length":1,"stats":{"Line":6},"fn_name":null},{"line":464,"address":[567978,568044],"length":1,"stats":{"Line":12},"fn_name":null},{"line":467,"address":[568400,568528,568294],"length":1,"stats":{"Line":14},"fn_name":null},{"line":468,"address":[568549],"length":1,"stats":{"Line":2},"fn_name":null},{"line":469,"address":[568649],"length":1,"stats":{"Line":2},"fn_name":null},{"line":472,"address":[568496,568751,568857],"length":1,"stats":{"Line":9},"fn_name":null},{"line":473,"address":[568878],"length":1,"stats":{"Line":1},"fn_name":null},{"line":474,"address":[568978],"length":1,"stats":{"Line":1},"fn_name":null},{"line":483,"address":[569072,569287],"length":1,"stats":{"Line":1},"fn_name":"serialize"},{"line":485,"address":[569115],"length":1,"stats":{"Line":1},"fn_name":null},{"line":486,"address":[569152],"length":1,"stats":{"Line":1},"fn_name":null},{"line":489,"address":[569268],"length":1,"stats":{"Line":1},"fn_name":null},{"line":490,"address":[569413,569355],"length":1,"stats":{"Line":6},"fn_name":null},{"line":491,"address":[569442,569613],"length":1,"stats":{"Line":4},"fn_name":null},{"line":493,"address":[569502],"length":1,"stats":{"Line":4},"fn_name":null},{"line":499,"address":[570127,569776],"length":1,"stats":{"Line":1},"fn_name":"serialize_to_pem"},{"line":500,"address":[569801,569918],"length":1,"stats":{"Line":1},"fn_name":null},{"line":502,"address":[570036,570202,570268],"length":1,"stats":{"Line":6},"fn_name":null},{"line":503,"address":[569886],"length":1,"stats":{"Line":1},"fn_name":null},{"line":504,"address":[570010],"length":1,"stats":{"Line":1},"fn_name":null},{"line":511,"address":[570881,570912,570368],"length":1,"stats":{"Line":1},"fn_name":"sha256sum"},{"line":512,"address":[570506,570392],"length":1,"stats":{"Line":1},"fn_name":null},{"line":514,"address":[570495],"length":1,"stats":{"Line":3},"fn_name":null},{"line":515,"address":[570606],"length":1,"stats":{"Line":2},"fn_name":null},{"line":517,"address":[570663],"length":1,"stats":{"Line":3},"fn_name":null},{"line":548,"address":[570928],"length":1,"stats":{"Line":1},"fn_name":"net_match"},{"line":549,"address":[570978,571042],"length":1,"stats":{"Line":2},"fn_name":null},{"line":550,"address":[571052],"length":1,"stats":{"Line":1},"fn_name":null},{"line":551,"address":[571069],"length":1,"stats":{"Line":1},"fn_name":null},{"line":554,"address":[571035],"length":1,"stats":{"Line":1},"fn_name":null}],"covered":175,"coverable":184},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","cert_codec.rs"],"content":"// Automatically generated rust module for 'cert_codec.proto' file\n\n#![allow(non_snake_case)]\n#![allow(non_upper_case_globals)]\n#![allow(non_camel_case_types)]\n#![allow(unused_imports)]\n#![allow(unknown_lints)]\n#![allow(clippy::all)]\n#![cfg_attr(rustfmt, rustfmt_skip)]\n#![allow(clippy::pedantic)]\n\nuse quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result};\nuse quick_protobuf::sizeofs::*;\nuse super::*;\n\n#[allow(clippy::derive_partial_eq_without_eq)]\n#[derive(Debug, Default, PartialEq, Clone)]\npub struct RawNebulaCertificate {\n    pub Details: Option\u003ccert_codec::RawNebulaCertificateDetails\u003e,\n    pub Signature: Vec\u003cu8\u003e,\n}\n\nimpl\u003c'a\u003e MessageRead\u003c'a\u003e for RawNebulaCertificate {\n    fn from_reader(r: \u0026mut BytesReader, bytes: \u0026'a [u8]) -\u003e Result\u003cSelf\u003e {\n        let mut msg = Self::default();\n        while !r.is_eof() {\n            match r.next_tag(bytes) {\n                Ok(10) =\u003e msg.Details = Some(r.read_message::\u003ccert_codec::RawNebulaCertificateDetails\u003e(bytes)?),\n                Ok(18) =\u003e msg.Signature = r.read_bytes(bytes)?.to_owned(),\n                Ok(t) =\u003e { r.read_unknown(bytes, t)?; }\n                Err(e) =\u003e return Err(e),\n            }\n        }\n        Ok(msg)\n    }\n}\n\nimpl MessageWrite for RawNebulaCertificate {\n    fn get_size(\u0026self) -\u003e usize {\n        0\n        + self.Details.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size()))\n        + if self.Signature.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.Signature).len()) }\n    }\n\n    fn write_message\u003cW: WriterBackend\u003e(\u0026self, w: \u0026mut Writer\u003cW\u003e) -\u003e Result\u003c()\u003e {\n        if let Some(ref s) = self.Details { w.write_with_tag(10, |w| w.write_message(s))?; }\n        if !self.Signature.is_empty() { w.write_with_tag(18, |w| w.write_bytes(\u0026**\u0026self.Signature))?; }\n        Ok(())\n    }\n}\n\n#[allow(clippy::derive_partial_eq_without_eq)]\n#[derive(Debug, Default, PartialEq, Clone)]\npub struct RawNebulaCertificateDetails {\n    pub Name: String,\n    pub Ips: Vec\u003cu32\u003e,\n    pub Subnets: Vec\u003cu32\u003e,\n    pub Groups: Vec\u003cString\u003e,\n    pub NotBefore: i64,\n    pub NotAfter: i64,\n    pub PublicKey: Vec\u003cu8\u003e,\n    pub IsCA: bool,\n    pub Issuer: Vec\u003cu8\u003e,\n}\n\nimpl\u003c'a\u003e MessageRead\u003c'a\u003e for RawNebulaCertificateDetails {\n    fn from_reader(r: \u0026mut BytesReader, bytes: \u0026'a [u8]) -\u003e Result\u003cSelf\u003e {\n        let mut msg = Self::default();\n        while !r.is_eof() {\n            match r.next_tag(bytes) {\n                Ok(10) =\u003e msg.Name = r.read_string(bytes)?.to_owned(),\n                Ok(18) =\u003e msg.Ips = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?,\n                Ok(26) =\u003e msg.Subnets = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?,\n                Ok(34) =\u003e msg.Groups.push(r.read_string(bytes)?.to_owned()),\n                Ok(40) =\u003e msg.NotBefore = r.read_int64(bytes)?,\n                Ok(48) =\u003e msg.NotAfter = r.read_int64(bytes)?,\n                Ok(58) =\u003e msg.PublicKey = r.read_bytes(bytes)?.to_owned(),\n                Ok(64) =\u003e msg.IsCA = r.read_bool(bytes)?,\n                Ok(74) =\u003e msg.Issuer = r.read_bytes(bytes)?.to_owned(),\n                Ok(t) =\u003e { r.read_unknown(bytes, t)?; }\n                Err(e) =\u003e return Err(e),\n            }\n        }\n        Ok(msg)\n    }\n}\n\nimpl MessageWrite for RawNebulaCertificateDetails {\n    fn get_size(\u0026self) -\u003e usize {\n        0\n        + if self.Name == String::default() { 0 } else { 1 + sizeof_len((\u0026self.Name).len()) }\n        + if self.Ips.is_empty() { 0 } else { 1 + sizeof_len(self.Ips.iter().map(|s| sizeof_varint(*(s) as u64)).sum::\u003cusize\u003e()) }\n        + if self.Subnets.is_empty() { 0 } else { 1 + sizeof_len(self.Subnets.iter().map(|s| sizeof_varint(*(s) as u64)).sum::\u003cusize\u003e()) }\n        + self.Groups.iter().map(|s| 1 + sizeof_len((s).len())).sum::\u003cusize\u003e()\n        + if self.NotBefore == 0i64 { 0 } else { 1 + sizeof_varint(*(\u0026self.NotBefore) as u64) }\n        + if self.NotAfter == 0i64 { 0 } else { 1 + sizeof_varint(*(\u0026self.NotAfter) as u64) }\n        + if self.PublicKey.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.PublicKey).len()) }\n        + if self.IsCA == false { 0 } else { 1 + sizeof_varint(*(\u0026self.IsCA) as u64) }\n        + if self.Issuer.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.Issuer).len()) }\n    }\n\n    fn write_message\u003cW: WriterBackend\u003e(\u0026self, w: \u0026mut Writer\u003cW\u003e) -\u003e Result\u003c()\u003e {\n        if self.Name != String::default() { w.write_with_tag(10, |w| w.write_string(\u0026**\u0026self.Name))?; }\n        w.write_packed_with_tag(18, \u0026self.Ips, |w, m| w.write_uint32(*m), \u0026|m| sizeof_varint(*(m) as u64))?;\n        w.write_packed_with_tag(26, \u0026self.Subnets, |w, m| w.write_uint32(*m), \u0026|m| sizeof_varint(*(m) as u64))?;\n        for s in \u0026self.Groups { w.write_with_tag(34, |w| w.write_string(\u0026**s))?; }\n        if self.NotBefore != 0i64 { w.write_with_tag(40, |w| w.write_int64(*\u0026self.NotBefore))?; }\n        if self.NotAfter != 0i64 { w.write_with_tag(48, |w| w.write_int64(*\u0026self.NotAfter))?; }\n        if !self.PublicKey.is_empty() { w.write_with_tag(58, |w| w.write_bytes(\u0026**\u0026self.PublicKey))?; }\n        if self.IsCA != false { w.write_with_tag(64, |w| w.write_bool(*\u0026self.IsCA))?; }\n        if !self.Issuer.is_empty() { w.write_with_tag(74, |w| w.write_bytes(\u0026**\u0026self.Issuer))?; }\n        Ok(())\n    }\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","lib.rs"],"content":"//! # trifid-pki\n//! trifid-pki is a crate for interacting with the Nebula PKI system. It was created to prevent the need to make constant CLI calls for signing operations in Nebula.\n//! It is designed to be interoperable with the original Go implementation and as such has some oddities with key management to ensure compatability.\n//!\n//! This crate has not received any format security audits, however the underlying crates used for actual cryptographic operations (ed25519-dalek and curve25519-dalek) have been audited with no major issues.\n\n#![warn(clippy::pedantic)]\n#![warn(clippy::nursery)]\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::expect_used)]\n#![deny(missing_docs)]\n#![deny(clippy::missing_errors_doc)]\n#![deny(clippy::missing_panics_doc)]\n#![deny(clippy::missing_safety_doc)]\n#![allow(clippy::must_use_candidate)]\n#![allow(clippy::too_many_lines)]\n#![allow(clippy::module_name_repetitions)]\n\n\nextern crate core;\n\npub mod ca;\npub mod cert;\n#[cfg(not(tarpaulin_include))]\npub(crate) mod cert_codec;\n#[cfg(test)]\n#[macro_use]\npub mod test;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","test.rs"],"content":"#![allow(clippy::unwrap_used)]\n#![allow(clippy::expect_used)]\n\nuse crate::netmask;\nuse std::net::Ipv4Addr;\nuse std::ops::{Add, Sub};\nuse std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};\nuse ipnet::Ipv4Net;\nuse crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public};\nuse std::str::FromStr;\nuse ed25519_dalek::{SigningKey, VerifyingKey};\nuse quick_protobuf::{MessageWrite, Writer};\nuse rand::rngs::OsRng;\nuse crate::ca::{NebulaCAPool};\nuse crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};\n\n/// This is a cert that we (e3team) actually use in production, and it's a known-good certificate.\npub const KNOWN_GOOD_CERT: \u0026[u8; 258] = b\"-----BEGIN NEBULA CERTIFICATE-----\\nCkkKF2UzdGVhbSBJbnRlcm5hbCBOZXR3b3JrKJWev5wGMJWFxKsGOiCvpwoHyKY5\\n8Q5+2XxDjtoCf/zlNY/EUdB8bwXQSwEo50ABEkB0Dx76lkMqc3IyH5+ml2dKjTyv\\nB4Jiw6x3abf5YZcf8rDuVEgQpvFdJmo3xJyIb3C9vKZ6kXsUxjw6s1JdWgkA\\n-----END NEBULA CERTIFICATE-----\";\n\n#[test]\nfn certificate_serialization() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let bytes = cert.serialize().unwrap();\n\n    let deserialized = deserialize_nebula_certificate(\u0026bytes).unwrap();\n\n    assert_eq!(cert.signature, deserialized.signature);\n    assert_eq!(cert.details.name, deserialized.details.name);\n    assert_eq!(cert.details.not_before, deserialized.details.not_before);\n    assert_eq!(cert.details.not_after, deserialized.details.not_after);\n    assert_eq!(cert.details.public_key, deserialized.details.public_key);\n    assert_eq!(cert.details.is_ca, deserialized.details.is_ca);\n\n    assert_eq!(cert.details.ips.len(), deserialized.details.ips.len());\n    for item in \u0026cert.details.ips {\n        assert!(deserialized.details.ips.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.subnets.len(), deserialized.details.subnets.len());\n    for item in \u0026cert.details.subnets {\n        assert!(deserialized.details.subnets.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.groups.len(), deserialized.details.groups.len());\n    for item in \u0026cert.details.groups {\n        assert!(deserialized.details.groups.contains(item), \"deserialized does not contain from source\");\n    }\n}\n\n#[test]\nfn certificate_serialization_pem() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let bytes = cert.serialize_to_pem().unwrap();\n\n    let deserialized = deserialize_nebula_certificate_from_pem(\u0026bytes).unwrap();\n\n    assert_eq!(cert.signature, deserialized.signature);\n    assert_eq!(cert.details.name, deserialized.details.name);\n    assert_eq!(cert.details.not_before, deserialized.details.not_before);\n    assert_eq!(cert.details.not_after, deserialized.details.not_after);\n    assert_eq!(cert.details.public_key, deserialized.details.public_key);\n    assert_eq!(cert.details.is_ca, deserialized.details.is_ca);\n\n    assert_eq!(cert.details.ips.len(), deserialized.details.ips.len());\n    for item in \u0026cert.details.ips {\n        assert!(deserialized.details.ips.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.subnets.len(), deserialized.details.subnets.len());\n    for item in \u0026cert.details.subnets {\n        assert!(deserialized.details.subnets.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.groups.len(), deserialized.details.groups.len());\n    for item in \u0026cert.details.groups {\n        assert!(deserialized.details.groups.contains(item), \"deserialized does not contain from source\");\n    }\n}\n\n#[test]\nfn cert_signing() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let mut csprng = OsRng;\n    let key = SigningKey::generate(\u0026mut csprng);\n\n    assert!(cert.check_signature(\u0026key.verifying_key()).is_err());\n    cert.sign(\u0026key).unwrap();\n    assert!(cert.check_signature(\u0026key.verifying_key()).unwrap());\n}\n\n#[test]\nfn cert_expiry() {\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: String::new(),\n            ips: vec![],\n            subnets: vec![],\n            groups: vec![],\n            not_before: SystemTime::now().sub(Duration::from_secs(60)),\n            not_after: SystemTime::now().add(Duration::from_secs(60)),\n            public_key: [0u8; 32],\n            is_ca: false,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n\n    assert!(cert.expired(SystemTime::now().add(Duration::from_secs(60 * 60 * 60))));\n    assert!(cert.expired(SystemTime::now().sub(Duration::from_secs(60 * 60 * 60))));\n    assert!(!cert.expired(SystemTime::now()));\n}\n\n#[test]\nfn cert_display() {\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: String::new(),\n            ips: vec![],\n            subnets: vec![],\n            groups: vec![],\n            not_before: SystemTime::now().sub(Duration::from_secs(60)),\n            not_after: SystemTime::now().add(Duration::from_secs(60)),\n            public_key: [0u8; 32],\n            is_ca: false,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n    println!(\"{cert}\");\n}\n\n#[test]\n#[should_panic]\nfn cert_deserialize_empty_bytes() {\n    deserialize_nebula_certificate(\u0026[]).unwrap();\n}\n\n#[test]\nfn cert_deserialize_unpaired_ips() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![0],\n            Subnets: vec![],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n}\n\n#[test]\nfn cert_deserialize_unpaired_subnets() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![],\n            Subnets: vec![0],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n}\n\n#[test]\nfn cert_deserialize_wrong_pubkey_len() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![],\n            Subnets: vec![],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![0u8; 31],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n\n    assert!(deserialize_nebula_certificate_from_pem(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn x25519_serialization() {\n    let bytes = [0u8; 32];\n    assert_eq!(deserialize_x25519_private(\u0026serialize_x25519_private(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_x25519_private(\u0026[0u8; 32]).is_err());\n    assert_eq!(deserialize_x25519_public(\u0026serialize_x25519_public(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_x25519_public(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn ed25519_serialization() {\n    let bytes = [0u8; 64];\n    assert_eq!(deserialize_ed25519_private(\u0026serialize_ed25519_private(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_ed25519_private(\u0026[0u8; 32]).is_err());\n    assert_eq!(deserialize_ed25519_public(\u0026serialize_ed25519_public(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_ed25519_public(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn cert_verify() {\n    let (ca_cert, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![], vec![], vec![\"groupa\".to_string()]);\n\n    let (cert, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, SystemTime::now(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![], vec![]);\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_cert.serialize_to_pem().unwrap()).unwrap();\n\n    let fingerprint = cert.sha256sum().unwrap();\n    ca_pool.blocklist_fingerprint(\u0026fingerprint);\n\n    assert!(matches!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Blocklisted));\n\n    ca_pool.reset_blocklist();\n\n    assert!(matches!(cert.verify(SystemTime::now() + Duration::from_secs(60 * 60 * 60), \u0026ca_pool).unwrap(), CertificateValidity::RootCertExpired));\n    assert!(matches!(cert.verify(SystemTime::now() + Duration::from_secs(60 * 60 * 6), \u0026ca_pool).unwrap(), CertificateValidity::CertExpired));\n\n    let (cert_with_bad_group, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), in_a_minute(), vec![], vec![], vec![\"group-not-present on parent\".to_string()]);\n    assert_eq!(cert_with_bad_group.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::GroupNotPresentOnSigner);\n\n    let (cert_with_good_group, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), in_a_minute(), vec![], vec![], vec![\"groupa\".to_string()]);\n    assert_eq!(cert_with_good_group.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n}\n\n#[test]\nfn cert_verify_ip() {\n    let ca_ip_1 = Ipv4Net::from_str(\"10.0.0.0/16\").unwrap();\n    let ca_ip_2 = Ipv4Net::from_str(\"192.168.0.0/24\").unwrap();\n\n    let (ca, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![ca_ip_1, ca_ip_2], vec![], vec![]);\n\n    let ca_pem = ca.serialize_to_pem().unwrap();\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_pem).unwrap();\n\n    // ip is outside the network\n    let cip1 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let cip2 = netmask!(\"192.198.0.1\", \"255.255.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is outside the network - reversed order from above\n    let cip1 = netmask!(\"192.198.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is within the network but mask is outside\n    let cip1 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is within the network but mask is outside - reversed order from above\n    let cip1 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip and mask are within the network\n    let cip1 = netmask!(\"10.0.1.0\", \"255.255.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.128\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_1, ca_ip_2], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_2, ca_ip_1], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed with just one\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_2], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n}\n\n#[test]\nfn cert_verify_subnet() {\n    let ca_ip_1 = Ipv4Net::from_str(\"10.0.0.0/16\").unwrap();\n    let ca_ip_2 = Ipv4Net::from_str(\"192.168.0.0/24\").unwrap();\n\n    let (ca, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![],vec![ca_ip_1, ca_ip_2], vec![]);\n\n    let ca_pem = ca.serialize_to_pem().unwrap();\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_pem).unwrap();\n\n    // ip is outside the network\n    let cip1 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let cip2 = netmask!(\"192.198.0.1\", \"255.255.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is outside the network - reversed order from above\n    let cip1 = netmask!(\"192.198.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is within the network but mask is outside\n    let cip1 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is within the network but mask is outside - reversed order from above\n    let cip1 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip and mask are within the network\n    let cip1 = netmask!(\"10.0.1.0\", \"255.255.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.128\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![ca_ip_1, ca_ip_2], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![ca_ip_2, ca_ip_1], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed with just one\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![ca_ip_2], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n}\n\n#[test]\nfn cert_private_key() {\n    let (ca, ca_key, _) = test_ca_cert(SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    ca.verify_private_key(\u0026ca_key.to_keypair_bytes()).unwrap();\n\n    let (_, ca_key2, _) = test_ca_cert(SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    ca.verify_private_key(\u0026ca_key2.to_keypair_bytes()).unwrap_err();\n\n    let (cert, priv_key, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    cert.verify_private_key(\u0026priv_key.to_bytes()).unwrap();\n\n    let (cert2, _, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    cert2.verify_private_key(\u0026priv_key.to_bytes()).unwrap_err();\n}\n\n#[test]\nfn capool_from_pem() {\n    let no_newlines = b\"# Current provisional, Remove once everything moves over to the real root.\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\n# root-ca01\n-----BEGIN NEBULA CERTIFICATE-----\nCkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG\nBVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf\n8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF\n-----END NEBULA CERTIFICATE-----\";\n    let with_newlines = b\"# Current provisional, Remove once everything moves over to the real root.\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\n# root-ca01\n-----BEGIN NEBULA CERTIFICATE-----\nCkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG\nBVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf\n8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF\n-----END NEBULA CERTIFICATE-----\n\n\";\n    let expired = b\"# expired certificate\n-----BEGIN NEBULA CERTIFICATE-----\nCjkKB2V4cGlyZWQouPmWjQYwufmWjQY6ILCRaoCkJlqHgv5jfDN4lzLHBvDzaQm4\nvZxfu144hmgjQAESQG4qlnZi8DncvD/LDZnLgJHOaX1DWCHHEh59epVsC+BNgTie\nWH1M9n4O7cFtGlM6sJJOS+rCVVEJ3ABS7+MPdQs=\n-----END NEBULA CERTIFICATE-----\";\n\n    let pool_a = NebulaCAPool::new_from_pem(no_newlines).unwrap();\n    assert_eq!(pool_a.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_a.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert!(!pool_a.expired);\n\n    let pool_b = NebulaCAPool::new_from_pem(with_newlines).unwrap();\n    assert_eq!(pool_b.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_b.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert!(!pool_b.expired);\n\n    let pool_c = NebulaCAPool::new_from_pem(expired).unwrap();\n    assert!(pool_c.expired);\n    assert_eq!(pool_c.cas[\"152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0\"].details.name, \"expired\");\n\n    let mut pool_d = NebulaCAPool::new_from_pem(with_newlines).unwrap();\n    pool_d.add_ca_certificate(expired).unwrap();\n    assert_eq!(pool_d.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_d.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert_eq!(pool_d.cas[\"152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0\"].details.name, \"expired\");\n    assert!(pool_d.expired);\n    assert_eq!(pool_d.get_fingerprints().len(), 3);\n}\n\n#[macro_export]\nmacro_rules! netmask {\n    ($ip:expr,$mask:expr) =\u003e {\n        Ipv4Net::with_netmask(Ipv4Addr::from_str($ip).unwrap(), Ipv4Addr::from_str($mask).unwrap()).unwrap()\n    };\n}\n\nfn round_systime_to_secs(time: SystemTime) -\u003e Result\u003cSystemTime, SystemTimeError\u003e {\n    let secs = time.duration_since(UNIX_EPOCH)?.as_secs();\n    Ok(SystemTime::UNIX_EPOCH.add(Duration::from_secs(secs)))\n}\n\nfn test_ca_cert(before: SystemTime, after: SystemTime, ips: Vec\u003cIpv4Net\u003e, subnets: Vec\u003cIpv4Net\u003e, groups: Vec\u003cString\u003e) -\u003e (NebulaCertificate, SigningKey, VerifyingKey) {\n    let mut csprng = OsRng;\n    let key = SigningKey::generate(\u0026mut csprng);\n    let pub_key = key.verifying_key();\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"TEST_CA\".to_string(),\n            ips,\n            subnets,\n            groups,\n            not_before: before,\n            not_after: after,\n            public_key: pub_key.to_bytes(),\n            is_ca: true,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n    cert.sign(\u0026key).unwrap();\n    (cert, key, pub_key)\n}\n\n#[test]\nfn test_deserialize_ed25519_private() {\n    let priv_key = b\"-----BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA ED25519 PRIVATE KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END NEBULA ED25519 PRIVATE KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NOT A NEBULA PRIVATE KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-END NEBULA ED25519 PRIVATE KEY-----\";\n\n    deserialize_ed25519_private(priv_key).unwrap();\n\n    deserialize_ed25519_private(short_key).unwrap_err();\n\n    deserialize_ed25519_private(invalid_banner).unwrap_err();\n\n    deserialize_ed25519_private(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_deserialize_x25519_private() {\n    let priv_key = b\"-----BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NEBULA X25519 PRIVATE KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA X25519 PRIVATE KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NOT A NEBULA PRIVATE KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-END NEBULA X25519 PRIVATE KEY-----\";\n\n    deserialize_x25519_private(priv_key).unwrap();\n\n    deserialize_x25519_private(short_key).unwrap_err();\n\n    deserialize_x25519_private(invalid_banner).unwrap_err();\n\n    deserialize_x25519_private(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_pem_deserialization() {\n    let good_cert = b\"# A good cert\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\";\n    let bad_banner = b\"-----BEGIN NOT A NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NOT A NEBULA CERTIFICATE-----\";\n    let invalid_pem = b\"# Not a valid PEM format\n-BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-END NEBULA CERTIFICATE----\";\n\n    // success\n    deserialize_nebula_certificate_from_pem(good_cert).unwrap();\n\n    // fail because invalid banner\n    deserialize_nebula_certificate_from_pem(bad_banner).unwrap_err();\n\n    // fail because nonsense pem\n    deserialize_nebula_certificate_from_pem(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_deserialize_ed25519_public() {\n    let priv_key = b\"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA ED25519 PUBLIC KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END NEBULA ED25519 PUBLIC KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NOT A NEBULA PUBLIC KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-END NEBULA ED25519 PUBLIC KEY-----\";\n\n    deserialize_ed25519_public(priv_key).unwrap();\n\n    deserialize_ed25519_public(short_key).unwrap_err();\n\n    deserialize_ed25519_public(invalid_banner).unwrap_err();\n\n    deserialize_ed25519_public(invalid_pem).unwrap_err();\n}\n\n#[test]\n// Ensure that trifid-pki produces the *exact same* bit-for-bit representation as nebula does\n// to ensure signature interoperability\n// We use an e3team production certificate for this as it is a known-good certificate.\nfn test_serialization_interoperability() {\n    let known_good_cert = hex::decode(\"0a650a08636f72652d74777212098184c4508080f8ff0f28ae9fbf9c06309485c4ab063a20304e6279d8722f0fd8d966faa70adaeec08c4649d486457bb038be243753a7474a2056860ded3d14b7f3b77ca2a062ee5d683dd06b35f87446d8d6a7e923b6a7783d1240eb5d6b688eda0d36e925219c098ff2799e42207a093f7d9b7d875823ca05b2e0d3a749dd2fc9cec811ed9a2865d71c4d53cfdfd1bf7e6d5058bd9ecd388ddf0d\").unwrap();\n\n    let cert = deserialize_nebula_certificate(\u0026known_good_cert).unwrap();\n    let reserialized_cert_pem = cert.serialize().unwrap();\n    assert_eq!(known_good_cert, reserialized_cert_pem);\n}\n\n#[test]\nfn test_deserialize_x25519_public() {\n    let priv_key = b\"-----BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NEBULA X25519 PUBLIC KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA X25519 PUBLIC KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NOT A NEBULA PUBLIC KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-END NEBULA X25519 PUBLIC KEY-----\";\n\n    deserialize_x25519_public(priv_key).unwrap();\n\n    deserialize_x25519_public(short_key).unwrap_err();\n\n    deserialize_x25519_public(invalid_banner).unwrap_err();\n\n    deserialize_x25519_public(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn ca_pool_add_non_ca() {\n    let mut ca_pool = NebulaCAPool::new();\n\n    let (ca, ca_key, _) = test_ca_cert(SystemTime::now(), SystemTime::now() + Duration::from_secs(3600), vec![], vec![], vec![]);\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n\n    ca_pool.add_ca_certificate(\u0026cert.serialize_to_pem().unwrap()).unwrap_err();\n}\n\nfn test_cert(ca: \u0026NebulaCertificate, key: \u0026SigningKey, before: SystemTime, after: SystemTime, ips: Vec\u003cIpv4Net\u003e, subnets: Vec\u003cIpv4Net\u003e, groups: Vec\u003cString\u003e) -\u003e (NebulaCertificate, SigningKey, VerifyingKey) {\n    let issuer = ca.sha256sum().unwrap();\n\n    let real_groups = if groups.is_empty() {\n        vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()]\n    } else {\n        groups\n    };\n\n    let real_ips = if ips.is_empty() {\n        vec![\n            netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n            netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n            netmask!(\"10.1.1.3\", \"255.0.0.0\")\n        ]\n    } else {\n        ips\n    };\n\n    let real_subnets = if subnets.is_empty() {\n        vec![\n            netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n            netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n            netmask!(\"9.1.1.3\", \"255.255.0.0\")\n        ]\n    } else {\n        subnets\n    };\n\n    let mut csprng = OsRng;\n    let cert_key = SigningKey::generate(\u0026mut csprng);\n    let pub_key = cert_key.verifying_key();\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"TEST_CA\".to_string(),\n            ips: real_ips,\n            subnets: real_subnets,\n            groups: real_groups,\n            not_before: before,\n            not_after: after,\n            public_key: pub_key.to_bytes(),\n            is_ca: false,\n            issuer,\n        },\n        signature: vec![],\n    };\n    cert.sign(key).unwrap();\n    (cert, cert_key, pub_key)\n}\n\n#[allow(dead_code)]\nfn in_a_minute() -\u003e SystemTime {\n    round_systime_to_secs(SystemTime::now().add(Duration::from_secs(60))).unwrap()\n}\n#[allow(dead_code)]\nfn a_minute_ago() -\u003e SystemTime {\n    round_systime_to_secs(SystemTime::now().sub(Duration::from_secs(60))).unwrap()\n}","traces":[],"covered":0,"coverable":0}]};
        var previousData = {"files":[{"path":["/","home","core","prj","e3t","trifid","tfclient","src","main.rs"],"content":"fn main() {\n    println!(\"Hello, world!\");\n}\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","build.rs"],"content":"// generated by `sqlx migrate build-script`\nfn main() {\n    // trigger recompilation when a new migration is added\n    println!(\"cargo:rerun-if-changed=migrations\");\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","auth.rs"],"content":"use rocket::http::Status;\nuse rocket::{Request};\nuse rocket::request::{FromRequest, Outcome};\nuse crate::tokens::{validate_auth_token, validate_session_token};\n\npub struct PartialUserInfo {\n    pub user_id: i32,\n    pub created_at: i64,\n    pub email: String,\n    pub has_totp_auth: bool,\n\n    pub session_id: String,\n    pub auth_id: Option\u003cString\u003e\n}\n\n#[derive(Debug)]\npub enum AuthenticationError {\n    MissingToken,\n    InvalidToken(usize),\n    DatabaseError,\n    RequiresTOTP\n}\n\n#[rocket::async_trait]\nimpl\u003c'r\u003e FromRequest\u003c'r\u003e for PartialUserInfo {\n    type Error = AuthenticationError;\n\n    async fn from_request(req: \u0026'r Request\u003c'_\u003e) -\u003e Outcome\u003cSelf, Self::Error\u003e {\n        let headers = req.headers();\n\n        // make sure the bearer token exists\n        if let Some(authorization) = headers.get_one(\"Authorization\") {\n            // parse bearer token\n            let components = authorization.split(' ').collect::\u003cVec\u003c\u0026str\u003e\u003e();\n\n            if components.len() != 2 \u0026\u0026 components.len() != 3 {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken));\n            }\n\n            if components[0] != \"Bearer\" {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(0)));\n            }\n\n            if components.len() == 2 \u0026\u0026 !components[1].starts_with(\"st-\") {\n                return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(1)));\n            }\n\n            let st: String;\n            let user_id: i64;\n            let at: Option\u003cString\u003e;\n\n            match \u0026components[1][..3] {\n                \"st-\" =\u003e {\n                    // validate session token\n                    st = components[1].to_string();\n                    match validate_session_token(st.clone(), req.rocket().state().unwrap()).await {\n                        Ok(uid) =\u003e user_id = uid,\n                        Err(_) =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(2)))\n                    }\n                },\n                _ =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(3)))\n            }\n\n            if components.len() == 3 {\n                match \u0026components[2][..3] {\n                    \"at-\" =\u003e {\n                        // validate auth token\n                        at = Some(components[2].to_string());\n                        match validate_auth_token(at.clone().unwrap().clone(), st.clone(), req.rocket().state().unwrap()).await {\n                            Ok(_) =\u003e (),\n                            Err(_) =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(4)))\n                        }\n                    },\n                    _ =\u003e return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(5)))\n                }\n            } else {\n                at = None;\n            }\n\n            // this user is 100% valid and authenticated, fetch their info\n\n            let user = match sqlx::query!(\"SELECT * FROM users WHERE id = $1\", user_id.clone() as i32).fetch_one(req.rocket().state().unwrap()).await {\n                Ok(u) =\u003e u,\n                Err(_) =\u003e return Outcome::Failure((Status::InternalServerError, AuthenticationError::DatabaseError))\n            };\n\n            Outcome::Success(PartialUserInfo {\n                user_id: user_id as i32,\n                created_at: user.created_on as i64,\n                email: user.email,\n                has_totp_auth: at.is_some(),\n                session_id: st,\n                auth_id: at,\n            })\n        } else {\n            Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken))\n        }\n    }\n}\n\npub struct TOTPAuthenticatedUserInfo {\n    pub user_id: i32,\n    pub created_at: i64,\n    pub email: String,\n}\n#[rocket::async_trait]\nimpl\u003c'r\u003e FromRequest\u003c'r\u003e for TOTPAuthenticatedUserInfo {\n    type Error = AuthenticationError;\n\n    async fn from_request(request: \u0026'r Request\u003c'_\u003e) -\u003e Outcome\u003cSelf, Self::Error\u003e {\n        let userinfo = PartialUserInfo::from_request(request).await;\n        match userinfo {\n            Outcome::Failure(e) =\u003e Outcome::Failure(e),\n            Outcome::Forward(f) =\u003e Outcome::Forward(f),\n            Outcome::Success(s) =\u003e {\n                if s.has_totp_auth {\n                    Outcome::Success(Self {\n                        user_id: s.user_id,\n                        created_at: s.created_at,\n                        email: s.email,\n                    })\n                } else {\n                    Outcome::Failure((Status::Unauthorized, AuthenticationError::RequiresTOTP))\n                }\n            }\n        }\n    }\n}","traces":[{"line":28,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":29,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":32,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":34,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":36,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":37,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":40,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":41,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":44,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":45,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":48,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":49,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":50,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":52,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":53,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":55,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":56,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":57,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":58,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":61,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":64,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":65,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":66,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":68,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":69,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":70,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":71,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":74,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":77,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":82,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":83,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":84,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":87,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":88,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":89,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":90,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":91,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":92,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":93,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":96,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":110,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":111,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":112,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":113,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":114,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":115,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":116,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":117,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":118,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":119,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":120,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":123,"address":[],"length":0,"stats":{"Line":0},"fn_name":null}],"covered":0,"coverable":52},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","config.rs"],"content":"use serde::Deserialize;\nuse url::Url;\n\n#[derive(Deserialize)]\npub struct TFConfig {\n    pub listen_port: u16,\n    pub db_url: String,\n    pub base: Url,\n    pub web_root: Url,\n    pub magic_links_valid_for: i64,\n    pub session_tokens_valid_for: i64,\n    pub totp_verification_valid_for: i64,\n    pub data_key: String\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","crypto.rs"],"content":"use std::error::Error;\nuse aes_gcm::{Aes256Gcm, KeyInit, Nonce};\nuse aes_gcm::aead::{Aead, Payload};\nuse crate::config::TFConfig;\n\npub fn get_cipher_from_config(config: \u0026TFConfig) -\u003e Result\u003cAes256Gcm, Box\u003cdyn Error\u003e\u003e {\n    let key_slice = hex::decode(\u0026config.data_key)?;\n    Ok(Aes256Gcm::new_from_slice(\u0026key_slice)?)\n}\n\npub fn encrypt_with_nonce(plaintext: \u0026[u8], nonce: [u8; 12], cipher: \u0026Aes256Gcm) -\u003e Result\u003cVec\u003cu8\u003e, aes_gcm::Error\u003e {\n    let nonce = Nonce::from_slice(\u0026nonce);\n    let ciphertext = cipher.encrypt(nonce, plaintext)?;\n    Ok(ciphertext)\n}\n\npub fn decrypt_with_nonce(ciphertext: \u0026[u8], nonce: [u8; 12], cipher: \u0026Aes256Gcm) -\u003e Result\u003cVec\u003cu8\u003e, aes_gcm::Error\u003e {\n    let nonce = Nonce::from_slice(\u0026nonce);\n    let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?;\n    Ok(plaintext)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","db.rs"],"content":"","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","format.rs"],"content":"use std::fmt::{Display, Formatter};\nuse crate::format::PEMValidationError::{IncorrectSegmentLength, InvalidBase64Data, MissingStartSentinel};\nuse crate::util::base64decode;\n\npub const ED_PUBKEY_START_STR: \u0026str = \"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\";\npub const ED_PUBKEY_END_STR: \u0026str = \"-----END NEBULA ED25519 PUBLIC KEY-----\";\n\npub const DH_PUBKEY_START_STR: \u0026str = \"-----BEGIN NEBULA X25519 PUBLIC KEY-----\";\npub const DH_PUBKEY_END_STR: \u0026str = \"-----END NEBULA X25519 PUBLIC KEY-----\";\n\npub enum PEMValidationError {\n    MissingStartSentinel,\n    InvalidBase64Data,\n    MissingEndSentinel,\n    IncorrectSegmentLength(usize, usize)\n}\nimpl Display for PEMValidationError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::MissingEndSentinel =\u003e write!(f, \"Missing ending sentinel\"),\n            Self::MissingStartSentinel =\u003e write!(f, \"Missing starting sentinel\"),\n            Self::InvalidBase64Data =\u003e write!(f, \"invalid base64 data\"),\n            Self::IncorrectSegmentLength(expected, got) =\u003e write!(f, \"incorrect number of segments, expected {} got {}\", expected, got)\n        }\n    }\n}\n\npub fn validate_ed_pubkey_pem(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    let segments = pubkey.split('\\n');\n    let segs = segments.collect::\u003cVec\u003c\u0026str\u003e\u003e();\n    if segs.len() \u003c 3 {\n        return Err(IncorrectSegmentLength(3, segs.len()))\n    }\n    if segs[0] != ED_PUBKEY_START_STR {\n        return Err(MissingStartSentinel)\n    }\n    if base64decode(segs[1]).is_err() {\n        return Err(InvalidBase64Data)\n    }\n    if segs[2] != ED_PUBKEY_END_STR {\n        return Err(MissingStartSentinel)\n    }\n    Ok(())\n}\n\npub fn validate_dh_pubkey_pem(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    let segments = pubkey.split('\\n');\n    let segs = segments.collect::\u003cVec\u003c\u0026str\u003e\u003e();\n    if segs.len() \u003c 3 {\n        return Err(IncorrectSegmentLength(3, segs.len()))\n    }\n    if segs[0] != DH_PUBKEY_START_STR {\n        return Err(MissingStartSentinel)\n    }\n    if base64decode(segs[1]).is_err() {\n        return Err(InvalidBase64Data)\n    }\n    if segs[2] != DH_PUBKEY_END_STR {\n        return Err(MissingStartSentinel)\n    }\n    Ok(())\n}\n\npub fn validate_ed_pubkey_base64(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    match base64decode(pubkey) {\n        Ok(k) =\u003e validate_ed_pubkey_pem(match std::str::from_utf8(k.as_ref()) {\n            Ok(k) =\u003e k,\n            Err(_) =\u003e return Err(InvalidBase64Data)\n        }),\n        Err(_) =\u003e Err(InvalidBase64Data)\n    }\n}\n\npub fn validate_dh_pubkey_base64(pubkey: \u0026str) -\u003e Result\u003c(), PEMValidationError\u003e {\n    match base64decode(pubkey) {\n        Ok(k) =\u003e validate_dh_pubkey_pem(match std::str::from_utf8(k.as_ref()) {\n            Ok(k) =\u003e k,\n            Err(_) =\u003e return Err(InvalidBase64Data)\n        }),\n        Err(_) =\u003e Err(InvalidBase64Data)\n    }\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","main.rs"],"content":"extern crate core;\n\nuse std::error::Error;\nuse std::fs;\nuse std::path::Path;\nuse dotenvy::dotenv;\nuse log::{error, info};\nuse rocket::{catchers, Request, Response, routes};\nuse rocket::fairing::{Fairing, Info, Kind};\nuse rocket::http::Header;\nuse sqlx::migrate::Migrator;\nuse sqlx::postgres::PgPoolOptions;\nuse crate::config::TFConfig;\n\npub mod format;\npub mod util;\npub mod db;\npub mod config;\npub mod tokens;\npub mod routes;\npub mod auth;\npub mod crypto;\npub mod org;\n\nstatic MIGRATOR: Migrator = sqlx::migrate!();\n\npub struct CORS;\n\n#[rocket::async_trait]\nimpl Fairing for CORS {\n    fn info(\u0026self) -\u003e Info {\n        Info {\n            name: \"Add CORS headers to responses\",\n            kind: Kind::Response\n        }\n    }\n\n    async fn on_response\u003c'r\u003e(\u0026self, _request: \u0026'r Request\u003c'_\u003e, response: \u0026mut Response\u003c'r\u003e) {\n        response.set_header(Header::new(\"Access-Control-Allow-Origin\", \"*\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Methods\", \"POST, GET, PATCH, OPTIONS\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Headers\", \"*\"));\n        response.set_header(Header::new(\"Access-Control-Allow-Credentials\", \"true\"));\n    }\n}\n\n#[rocket::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    let _ = rocket::build();\n\n    info!(\"[tfapi] loading config\");\n\n    let _ = dotenv();\n\n    if std::env::var(\"CONFIG_FILE\").is_err() \u0026\u0026 !Path::new(\"config.toml\").exists() {\n        error!(\"[tfapi] fatal: the environment variable CONFIG_FILE is not set\");\n        error!(\"[tfapi]  help: try creating a .env file that sets it\");\n        error!(\"[tfapi]  help: or, create a file config.toml with your config, as it is loaded automatically\");\n        std::process::exit(1);\n    }\n\n    let config_file = if Path::new(\"config.toml\").exists() {\n        \"config.toml\".to_string()\n    } else {\n        std::env::var(\"CONFIG_FILE\").unwrap()\n    };\n\n    let config_data = match fs::read_to_string(\u0026config_file) {\n        Ok(d) =\u003e d,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to read config from {}\", config_file);\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    let config: TFConfig = match toml::from_str(\u0026config_data) {\n        Ok(c) =\u003e c,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to parse config from {}\", config_file);\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    info!(\"[tfapi] connecting to database pool\");\n\n    let pool = match PgPoolOptions::new().max_connections(5).connect(\u0026config.db_url).await {\n        Ok(p) =\u003e p,\n        Err(e) =\u003e {\n            error!(\"[tfapi] fatal: unable to connect to database pool\");\n            error!(\"[tfapi] fatal: {}\", e);\n            std::process::exit(1);\n        }\n    };\n\n    info!(\"[tfapi] running database migrations\");\n\n    MIGRATOR.run(\u0026pool).await?;\n\n    info!(\"[tfapi] building rocket config\");\n\n    let figment = rocket::Config::figment().merge((\"port\", config.listen_port));\n\n    let _ = rocket::custom(figment)\n        .mount(\"/\", routes![\n            crate::routes::v1::auth::magic_link::magiclink_request,\n            crate::routes::v1::auth::magic_link::options,\n            crate::routes::v1::signup::signup_request,\n            crate::routes::v1::signup::options,\n            crate::routes::v1::auth::verify_magic_link::verify_magic_link,\n            crate::routes::v1::auth::verify_magic_link::options,\n            crate::routes::v1::totp_authenticators::totp_authenticators_request,\n            crate::routes::v1::totp_authenticators::options,\n            crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,\n            crate::routes::v1::verify_totp_authenticator::options,\n            crate::routes::v1::auth::totp::totp_request,\n            crate::routes::v1::auth::totp::options,\n            crate::routes::v1::auth::check_session::check_session,\n            crate::routes::v1::auth::check_session::check_session_auth,\n            crate::routes::v1::auth::check_session::options,\n            crate::routes::v1::auth::check_session::options_auth,\n            crate::routes::v2::whoami::whoami_request,\n            crate::routes::v2::whoami::options,\n\n            crate::routes::v1::organization::options,\n            crate::routes::v1::organization::orgidoptions,\n            crate::routes::v1::organization::orginfo_req,\n            crate::routes::v1::organization::orglist_req,\n            crate::routes::v1::organization::create_org\n        ])\n        .register(\"/\", catchers![\n            crate::routes::handler_400,\n            crate::routes::handler_401,\n            crate::routes::handler_403,\n            crate::routes::handler_404,\n            crate::routes::handler_422,\n\n            crate::routes::handler_500,\n            crate::routes::handler_501,\n            crate::routes::handler_502,\n            crate::routes::handler_503,\n            crate::routes::handler_504,\n            crate::routes::handler_505,\n        ])\n        .attach(CORS)\n        .manage(pool)\n        .manage(config)\n        .launch().await?;\n\n    Ok(())\n}","traces":[{"line":38,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":39,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":40,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":41,"address":[],"length":0,"stats":{"Line":0},"fn_name":null},{"line":42,"address":[],"length":0,"stats":{"Line":0},"fn_name":null}],"covered":0,"coverable":5},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","org.rs"],"content":"use std::error::Error;\nuse rocket::form::validate::Contains;\nuse sqlx::PgPool;\n\npub async fn get_org_by_owner_id(user: i32, db: \u0026PgPool) -\u003e Result\u003cOption\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    Ok(match sqlx::query!(\"SELECT id FROM organizations WHERE owner = $1\", user).fetch_optional(db).await? {\n        Some(r) =\u003e Some(r.id),\n        None =\u003e None\n    })\n}\n\npub async fn get_orgs_by_assoc_id(user: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let res: Vec\u003c_\u003e = sqlx::query!(\"SELECT org_id FROM organization_authorized_users WHERE user_id = $1\", user).fetch_all(db).await?;\n\n    let mut ret = vec![];\n\n    for i in res {\n        ret.push(i.org_id);\n    }\n\n    Ok(ret)\n}\n\npub async fn get_users_by_assoc_org(org: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let res: Vec\u003c_\u003e = sqlx::query!(\"SELECT user_id FROM organization_authorized_users WHERE org_id = $1\", org).fetch_all(db).await?;\n\n    let mut ret = vec![];\n\n    for i in res {\n        ret.push(i.org_id);\n    }\n\n    Ok(ret)\n}\n\npub async fn get_associated_orgs(user: i32, db: \u0026PgPool) -\u003e Result\u003cVec\u003ci32\u003e, Box\u003cdyn Error\u003e\u003e {\n    let mut assoc_orgs = vec![];\n\n    if let Some(owned_org) = get_org_by_owner_id(user, db).await? {\n        assoc_orgs.push(owned_org);\n    }\n\n    assoc_orgs.append(\u0026mut get_orgs_by_assoc_id(user, db).await?);\n\n    Ok(assoc_orgs)\n}\n\npub async fn user_has_org_assoc(user: i32, org: i32, db: \u0026PgPool) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n    Ok(get_associated_orgs(user, db).await?.contains(org))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","mod.rs"],"content":"pub mod v1;\npub mod v2;\n\nuse rocket::catch;\nuse serde::{Serialize};\nuse rocket::http::Status;\n\npub const ERR_MSG_MALFORMED_REQUEST: \u0026str = \"unable to parse the request body - is it valid JSON, using correct types?\";\npub const ERR_MSG_MALFORMED_REQUEST_CODE: \u0026str = \"ERR_MALFORMED_REQUEST\";\n\n/*\nTODO:\n    /v1/auth/magic-link                         [done]\n    /v1/auth/totp                               [done]\n    /v1/auth/verify-magic-link                  [done]\n    /v1/hosts/host-{id}/enrollment-code\n    /v1/hosts/host-{id}/enrollment-code-check\n    /v1/hosts/host-{id}\n    /v1/roles/role-{id}\n    /v1/feature-flags\n    /v1/hosts\n    /v1/networks\n    /v1/roles\n    /v1/signup                                  [done]\n    /v1/totp-authenticators                     [done]\n    /v1/verify-totp-authenticator               [done]\n    /v1/dnclient\n    /v2/enroll\n    /v2/whoami                                  [in-progress]\n */\n\n#[derive(Serialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct APIError {\n    errors: Vec\u003cAPIErrorSingular\u003e\n}\n#[derive(Serialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct APIErrorSingular {\n    code: String,\n    message: String\n}\n\nmacro_rules! error_handler {\n    ($code: expr, $err: expr, $msg: expr) =\u003e {\n        ::paste::paste! {\n            #[catch($code)]\n            pub fn [\u003chandler_ $code\u003e]() -\u003e (Status, String) {\n                (Status::from_code($code).unwrap(), format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", $err, $msg))\n            }\n        }\n    };\n}\nerror_handler!(400, \"ERR_MALFORMED_REQUEST\", \"unable to parse the request body, is it properly formatted?\");\nerror_handler!(401, \"ERR_AUTHENTICATION_REQUIRED\", \"this endpoint requires authentication but it was not provided\");\nerror_handler!(403, \"ERR_UNAUTHORIZED\", \"authorization was provided but it is expired or invalid\");\nerror_handler!(404, \"ERR_NOT_FOUND\", \"resource not found\");\nerror_handler!(405, \"ERR_METHOD_NOT_ALLOWED\", \"method not allowed for this endpoint\");\nerror_handler!(422, \"ERR_MALFORMED_REQUEST\", \"unable to parse the request body, is it properly formatted?\");\n\nerror_handler!(500, \"ERR_QL_QUERY_FAILED\", \"graphql query timed out\");\nerror_handler!(501, \"ERR_NOT_IMPLEMENTED\", \"query not supported by this version of graphql\");\nerror_handler!(502, \"ERR_PROXY_ERR\", \"servers under load, please try again later\");\nerror_handler!(503, \"ERR_SERVER_OVERLOADED\", \"servers under load, please try again later\");\nerror_handler!(504, \"ERR_PROXY_TIMEOUT\", \"servers under load, please try again later\");\nerror_handler!(505, \"ERR_CLIENT_UNSUPPORTED\", \"your version of dnclient is out of date, please update\");\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","check_session.rs"],"content":"use rocket::{post, options};\nuse crate::auth::{PartialUserInfo, TOTPAuthenticatedUserInfo};\n\n#[options(\"/v1/auth/check_session\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/auth/check_session\")]\npub async fn check_session(_user: PartialUserInfo) -\u003e \u0026'static str {\n    \"ok\"\n}\n\n#[options(\"/v1/auth/check_auth\")]\npub async fn options_auth() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/check_auth\")]\npub async fn check_session_auth(_user: TOTPAuthenticatedUserInfo) -\u003e \u0026'static str {\n    \"ok\"\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","magic_link.rs"],"content":"use rocket::{post, State};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse rocket::http::{ContentType, Status};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::send_magic_link;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkRequest {\n    pub email: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct MagicLinkResponse {\n    pub data: Option\u003cString\u003e,\n    pub metadata: MagicLinkResponseMetadata,\n}\n\n\n#[options(\"/v1/auth/magic-link\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/auth/magic-link\", data = \"\u003creq\u003e\")]\npub async fn magiclink_request(req: Json\u003cMagicLinkRequest\u003e, pool: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cMagicLinkResponse\u003e), (Status, String)\u003e {\n    // figure out if the user already exists\n    let mut id = -1;\n    match sqlx::query!(\"SELECT id FROM users WHERE email = $1\", req.email.clone()).fetch_optional(pool.inner()).await {\n        Ok(res) =\u003e if let Some(r) = res { id = r.id as i64 },\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    }\n\n    if id == -1 {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"authorization was provided but it is expired or invalid\")))\n    }\n\n    // send magic link to email\n    match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    };\n\n    // this endpoint doesn't actually ever return an error? it will send you the magic link no matter what\n    // this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)\n    Ok((ContentType::JSON, Json(MagicLinkResponse {\n        data: None,\n        metadata: MagicLinkResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","mod.rs"],"content":"pub mod verify_magic_link;\npub mod magic_link;\npub mod totp;\npub mod check_session;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","totp.rs"],"content":"use rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse crate::auth::PartialUserInfo;\nuse serde::{Serialize, Deserialize};\nuse rocket::{post, State};\nuse sqlx::PgPool;\nuse rocket::options;\nuse crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp};\n\npub const TOTP_GENERIC_UNAUTHORIZED_ERROR: \u0026str = \"{\\\"errors\\\":[{\\\"code\\\":\\\"ERR_INVALID_TOTP_CODE\\\",\\\"message\\\":\\\"invalid TOTP code (maybe it expired?)\\\",\\\"path\\\":\\\"code\\\"}]}\";\npub const TOTP_NO_TOTP_ERROR: \u0026str = \"{\\\"errors\\\":[{\\\"code\\\":\\\"ERR_NO_TOTP\\\",\\\"message\\\":\\\"logged-in user does not have totp enabled\\\",\\\"path\\\":\\\"code\\\"}]}\";\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpRequest {\n    pub code: String\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponseData {\n    #[serde(rename = \"authToken\")]\n    auth_token: String\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponseMetadata {\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpResponse {\n    data: TotpResponseData,\n    metadata: TotpResponseMetadata\n}\n\n#[options(\"/v1/auth/totp\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/totp\", data = \"\u003creq\u003e\")]\npub async fn totp_request(req: Json\u003cTotpRequest\u003e, user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cTotpResponse\u003e), (Status, String)\u003e {\n    if !match user_has_totp(user.user_id, db.inner()).await {\n        Ok(b) =\u003e b,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    } {\n        return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string()))\n    }\n\n    if user.has_totp_auth {\n        return Err((Status::BadRequest, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_TOTP_ALREADY_AUTHED\", \"user already has valid totp authentication\")))\n    }\n\n    let totpmachine = match get_totpmachine(user.user_id, db.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    };\n\n    if !totpmachine.check_current(\u0026req.0.code).unwrap_or(false) {\n        return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string()))\n    }\n\n    Ok((ContentType::JSON, Json(TotpResponse {\n        data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {\n            Ok(t) =\u003e t,\n            Err(e) =\u003e { return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e))) }\n        } },\n        metadata: TotpResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","auth","verify_magic_link.rs"],"content":"use std::time::{SystemTime, UNIX_EPOCH};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse rocket::{post, State};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::generate_session_token;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct VerifyMagicLinkRequest {\n    #[serde(rename = \"magicLinkToken\")]\n    pub magic_link_token: String,\n}\n\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponseData {\n    #[serde(rename = \"sessionToken\")]\n    pub session_token: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyMagicLinkResponse {\n    pub data: VerifyMagicLinkResponseData,\n    pub metadata: VerifyMagicLinkResponseMetadata,\n}\n\n#[options(\"/v1/auth/verify-magic-link\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/auth/verify-magic-link\", data = \"\u003creq\u003e\")]\npub async fn verify_magic_link(req: Json\u003cVerifyMagicLinkRequest\u003e, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cVerifyMagicLinkResponse\u003e), (Status, String)\u003e {\n    // get the current time to check if the token is expired\n    let (user_id, expired_at) = match sqlx::query!(\"SELECT user_id, expires_on FROM magic_links WHERE id = $1\", req.0.magic_link_token).fetch_one(db.inner()).await {\n        Ok(row) =\u003e (row.user_id, row.expires_on),\n        Err(e) =\u003e {\n            return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"this token is invalid\", e)))\n        }\n    };\n    let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;\n    println!(\"expired on {}, currently {}\", expired_at, current_time);\n    if expired_at \u003c current_time {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"valid authorization was provided but it is expired\")))\n    }\n\n    // generate session token\n    let token = match generate_session_token(user_id as i64, db.inner(), config.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    };\n\n    // delete the token\n    match sqlx::query!(\"DELETE FROM magic_links WHERE id = $1\", req.0.magic_link_token).execute(db.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        }\n    }\n\n    Ok((ContentType::JSON, Json(VerifyMagicLinkResponse {\n        data: VerifyMagicLinkResponseData { session_token: token },\n        metadata: VerifyMagicLinkResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","mod.rs"],"content":"pub mod auth;\npub mod signup;\n\npub mod totp_authenticators;\npub mod verify_totp_authenticator;\npub mod organization;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","organization.rs"],"content":"use std::slice::from_raw_parts_mut;\nuse rocket::{get, post, options, State};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse sqlx::PgPool;\nuse crate::auth::TOTPAuthenticatedUserInfo;\nuse crate::config::TFConfig;\nuse crate::org::{get_associated_orgs, get_org_by_owner_id, get_users_by_assoc_org, user_has_org_assoc};\n\n#[options(\"/v1/orgs\")]\npub fn options() -\u003e \u0026'static str {\n    \"\"\n}\n#[options(\"/v1/org/\u003c_id\u003e\")]\npub fn orgidoptions(_id: i32) -\u003e \u0026'static str {\n    \"\"\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct OrglistStruct {\n    pub org_ids: Vec\u003ci32\u003e\n}\n\n\n#[get(\"/v1/orgs\")]\npub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrglistStruct\u003e), (Status, String)\u003e {\n    // this endpoint lists the associated organizations this user has access to\n    let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {\n        Ok(r) =\u003e r,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_DB_QUERY_FAILED\", \"an error occurred while running the database query\")))\n    };\n\n    Ok((ContentType::JSON, Json(OrglistStruct {\n        org_ids: associated_orgs\n    })))\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct OrginfoStruct {\n    pub org_id: i32,\n    pub owner_id: i32,\n    pub ca_crt: String,\n    pub authorized_users: Vec\u003ci32\u003e\n}\n\n#[get(\"/v1/org/\u003corgid\u003e\")]\npub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrginfoStruct\u003e), (Status, String)\u003e {\n    if !user_has_org_assoc(orgid, user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))? {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_MISSING_ORG_AUTHORIZATION\", \"this user does not have permission to access this org\")));\n    }\n\n    let org = sqlx::query!(\"SELECT id, owner, ca_crt FROM organizations WHERE id = $1\", orgid).fetch_one(orgid).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?;\n    let authorized_users = get_users_by_assoc_org(orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?;\n\n    Ok((ContentType::JSON, Json(\n        OrginfoStruct {\n            org_id: orgid,\n            owner_id: org.owner,\n            ca_crt: org.ca_crt,\n            authorized_users,\n        }\n    )))\n}\n\n#[post(\"/v1/org\")]\npub async fn create_org(user: TOTPAuthenticatedUserInfo, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cOrginfoStruct\u003e), (Status, String)\u003e {\n    if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))?.is_some() {\n        return Err((Status::Conflict, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_USER_OWNS_ORG\", \"a user can only own one organization at a time\")))\n    }\n\n    // we need to generate a keypair and CA certificate for this new org\n\n    Ok((ContentType::JSON, Json(\n        OrginfoStruct {\n            org_id: orgid,\n            owner_id: org.owner,\n            ca_crt: org.ca_crt,\n            authorized_users,\n        }\n    )))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","signup.rs"],"content":"use rocket::{post, State};\nuse rocket::serde::json::Json;\nuse serde::{Serialize, Deserialize};\nuse std::time::{SystemTime, UNIX_EPOCH};\nuse rocket::http::{ContentType, Status};\nuse sqlx::PgPool;\nuse crate::config::TFConfig;\nuse crate::tokens::send_magic_link;\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupRequest {\n    pub email: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct SignupResponse {\n    pub data: Option\u003cString\u003e,\n    pub metadata: SignupResponseMetadata,\n}\n/*\ncreated_on TIMESTAMP NOT NULL,\n\n    banned INTEGER NOT NULL,\n    ban_reason VARCHAR(1024) NOT NULL\n */\n#[options(\"/v1/signup\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[post(\"/v1/signup\", data = \"\u003creq\u003e\")]\npub async fn signup_request(req: Json\u003cSignupRequest\u003e, pool: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cSignupResponse\u003e), (Status, String)\u003e {\n    // figure out if the user already exists\n    let mut id = -1;\n    match sqlx::query!(\"SELECT id FROM users WHERE email = $1\", req.email.clone()).fetch_optional(pool.inner()).await {\n        Ok(res) =\u003e if let Some(r) = res { id = r.id as i64 },\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    }\n\n    if id == -1 {\n        let id_res = match sqlx::query!(\"INSERT INTO users (email, created_on, banned, ban_reason, totp_secret, totp_otpurl, totp_verified) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING RETURNING id;\", req.email, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 0, \"\", \"\", \"\", 0).fetch_one(pool.inner()).await {\n            Ok(row) =\u003e row.id,\n            Err(e) =\u003e {\n                return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n            }\n        };\n        id = id_res as i64;\n    }\n\n    // send magic link to email\n    match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e {\n            return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_QL_QUERY_FAILED\", \"an error occurred while running the graphql query\", e)))\n        }\n    };\n\n    // this endpoint doesn't actually ever return an error? it will send you the magic link no matter what\n    // this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)\n    Ok((ContentType::JSON, Json(SignupResponse {\n        data: None,\n        metadata: SignupResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","totp_authenticators.rs"],"content":"use rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse rocket::{State, post};\nuse sqlx::PgPool;\nuse serde::{Serialize, Deserialize};\nuse crate::auth::PartialUserInfo;\nuse crate::config::TFConfig;\nuse crate::tokens::{create_totp_token, user_has_totp};\nuse rocket::options;\n\n#[derive(Deserialize)]\npub struct TotpAuthenticatorsRequest {}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponseMetadata {}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponseData {\n    #[serde(rename = \"totpToken\")]\n    pub totp_token: String,\n    pub secret: String,\n    pub url: String,\n}\n#[derive(Serialize, Deserialize)]\n#[serde(crate = \"rocket::serde\")]\npub struct TotpAuthenticatorsResponse {\n    pub data: TotpAuthenticatorsResponseData,\n    pub metadata: TotpAuthenticatorsResponseMetadata,\n}\n\n#[options(\"/v1/totp-authenticators\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/totp-authenticators\", data = \"\u003c_req\u003e\")]\npub async fn totp_authenticators_request(_req: Json\u003cTotpAuthenticatorsRequest\u003e, user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e, config: \u0026State\u003cTFConfig\u003e) -\u003e Result\u003c(ContentType, Json\u003cTotpAuthenticatorsResponse\u003e), (Status, String)\u003e {\n    if match user_has_totp(user.user_id, db.inner()).await {\n        Ok(b) =\u003e b,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    } {\n        return Err((Status::BadRequest, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\"}}]}}\", \"ERR_TOTP_ALREADY_EXISTS\", \"this user already has a totp authenticator on their account\")))\n    }\n\n    // generate a totp token\n    let (totptoken, totpmachine) = match create_totp_token(user.email, db.inner(), config.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured issuing a totp token, try again later\", e)))\n    };\n\n    Ok((ContentType::JSON, Json(TotpAuthenticatorsResponse {\n        data: TotpAuthenticatorsResponseData {\n            totp_token: totptoken,\n            secret: totpmachine.get_secret_base32(),\n            url: totpmachine.get_url(),\n        },\n        metadata: TotpAuthenticatorsResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v1","verify_totp_authenticator.rs"],"content":"/*\n{\"totpToken\":\"totp-mH9eLzA9Q5WB-sg3Fq8CfkP13eTh3DxF25kVK2VEDOk\",\"code\":\"266242\"}\n{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_TOKEN\",\"message\":\"TOTP token does not exist (maybe it expired?)\",\"path\":\"totpToken\"}]}\n\n\n{\"totpToken\":\"totp-gaUDaxPrrIBc8GEQ6z0vPisT8k0MEP1fgI8FA2ztLMw\",\"code\":\"175543\"}\n{\"data\":{\"authToken\":\"auth-O7mugxdYta-RKtLMqDW4j8XCJ85EfZKKezeZZXBYtFQ\"},\"metadata\":{}}\n*/\n\nuse rocket::http::{ContentType, Status};\nuse crate::auth::PartialUserInfo;\nuse rocket::post;\nuse rocket::serde::json::Json;\nuse rocket::State;\nuse serde::{Serialize, Deserialize};\nuse sqlx::PgPool;\nuse crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token};\nuse rocket::options;\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorRequest {\n    #[serde(rename = \"totpToken\")]\n    pub totp_token: String,\n    pub code: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponseMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponseData {\n    #[serde(rename = \"authToken\")]\n    pub auth_token: String,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct VerifyTotpAuthenticatorResponse {\n    pub data: VerifyTotpAuthenticatorResponseData,\n    pub metadata: VerifyTotpAuthenticatorResponseMetadata,\n}\n\n#[options(\"/v1/verify-totp-authenticator\")]\npub async fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n\n#[post(\"/v1/verify-totp-authenticator\", data = \"\u003creq\u003e\")]\npub async fn verify_totp_authenticator_request(req: Json\u003cVerifyTotpAuthenticatorRequest\u003e, db: \u0026State\u003cPgPool\u003e, user: PartialUserInfo) -\u003e Result\u003c(ContentType, Json\u003cVerifyTotpAuthenticatorResponse\u003e), (Status, String)\u003e {\n    let totpmachine = match verify_totp_token(req.0.totp_token.clone(), user.email.clone(), db.inner()).await {\n        Ok(t) =\u003e t,\n        Err(e) =\u003e return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNAUTHORIZED\", \"this token is invalid\", e)))\n    };\n    if !totpmachine.check_current(\u0026req.0.code).unwrap() {\n        return Err((Status::Unauthorized, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{}\\\",\\\"path\\\":\\\"totpToken\\\"}}]}}\", \"ERR_INVALID_TOTP_CODE\", \"Invalid TOTP code\")))\n    }\n    match use_totp_token(req.0.totp_token, user.email, db.inner()).await {\n        Ok(_) =\u003e (),\n        Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n    }\n    Ok((ContentType::JSON, Json(VerifyTotpAuthenticatorResponse {\n        data: VerifyTotpAuthenticatorResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {\n            Ok(at) =\u003e at,\n            Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_UNABLE_TO_ISSUE\", \"an error occured trying to issue a session token, please try again later\", e)))\n        } },\n        metadata: VerifyTotpAuthenticatorResponseMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v2","mod.rs"],"content":"pub mod whoami;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","routes","v2","whoami.rs"],"content":"use chrono::{NaiveDateTime, Utc};\nuse serde::{Serialize, Deserialize};\nuse rocket::{options, get, State};\nuse rocket::http::{ContentType, Status};\nuse rocket::serde::json::Json;\nuse sqlx::PgPool;\nuse crate::auth::PartialUserInfo;\nuse crate::tokens::user_has_totp;\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiMetadata {}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiActor {\n    pub id: String,\n    #[serde(rename = \"organizationID\")]\n    pub organization_id: String,\n    pub email: String,\n    #[serde(rename = \"createdAt\")]\n    pub created_at: String,\n    #[serde(rename = \"hasTOTPAuthenticator\")]\n    pub has_totpauthenticator: bool,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiData {\n    #[serde(rename = \"actorType\")]\n    pub actor_type: String,\n    pub actor: WhoamiActor,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct WhoamiResponse {\n    pub data: WhoamiData,\n    pub metadata: WhoamiMetadata,\n}\n\n#[options(\"/v2/whoami\")]\npub fn options() -\u003e \u0026'static str {\n    \"\"\n}\n\n#[get(\"/v2/whoami\")]\npub async fn whoami_request(user: PartialUserInfo, db: \u0026State\u003cPgPool\u003e) -\u003e Result\u003c(ContentType, Json\u003cWhoamiResponse\u003e), (Status, String)\u003e {\n    Ok((ContentType::JSON, Json(WhoamiResponse {\n        data: WhoamiData {\n            actor_type: \"user\".to_string(),\n            actor: WhoamiActor {\n                id: user.user_id.to_string(),\n                organization_id: \"TEMP_ORG_BECAUSE_THAT_ISNT_IMPLEMENTED_YET\".to_string(),\n                email: user.email,\n                created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),\n                has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {\n                    Ok(b) =\u003e b,\n                    Err(e) =\u003e return Err((Status::InternalServerError, format!(\"{{\\\"errors\\\":[{{\\\"code\\\":\\\"{}\\\",\\\"message\\\":\\\"{} - {}\\\"}}]}}\", \"ERR_DBERROR\", \"an error occured trying to verify your user\", e)))\n                },\n            }\n        },\n        metadata: WhoamiMetadata {},\n    })))\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","tokens.rs"],"content":"use std::error::Error;\nuse log::info;\nuse sqlx::PgPool;\nuse uuid::Uuid;\nuse crate::config::TFConfig;\nuse std::time::SystemTime;\nuse std::time::UNIX_EPOCH;\nuse totp_rs::{Secret, TOTP};\nuse crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP};\n\n// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com\u0026token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg\npub async fn send_magic_link(id: i64, email: String, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    let otp = format!(\"ml-{}\", Uuid::new_v4());\n    let otp_url = config.web_root.join(\u0026format!(\"/auth/magic-link?email={}\u0026token={}\", urlencoding::encode(\u0026email.clone()), otp.clone())).unwrap();\n    sqlx::query!(\"INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;\n    // TODO: send email\n    info!(\"sent magic link {} to {}, valid for {} seconds\", otp_url, email.clone(), config.magic_links_valid_for);\n    Ok(())\n}\n\npub async fn generate_session_token(user_id: i64, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n    let token = format!(\"st-{}\", Uuid::new_v4());\n    sqlx::query!(\"INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;\n    Ok(token)\n}\npub async fn validate_session_token(token: String, db: \u0026PgPool) -\u003e Result\u003ci64, Box\u003cdyn Error\u003e\u003e {\n    Ok(sqlx::query!(\"SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on \u003e $2\", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)\n}\n\npub async fn generate_auth_token(user_id: i64, session_id: String, db: \u0026PgPool) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n    let token = format!(\"at-{}\", Uuid::new_v4());\n    sqlx::query!(\"INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;\", token, session_id, user_id as i32).execute(db).await?;\n    Ok(token)\n}\npub async fn validate_auth_token(token: String, session_id: String, db: \u0026PgPool) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n    validate_session_token(session_id.clone(), db).await?;\n    sqlx::query!(\"SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2\", token, session_id).fetch_one(db).await?;\n    Ok(())\n}\n\n\n/*\nCREATE TABLE totp_create_tokens (\n    id VARCHAR(39) NOT NULL PRIMARY KEY,\n    expires_on INTEGER NOT NULL,\n    totp_otpurl VARCHAR(3000) NOT NULL,\n    totp_secret VARCHAR(128) NOT NULL\n);\n */\n\npub async fn create_totp_token(email: String, db: \u0026PgPool, config: \u0026TFConfig) -\u003e Result\u003c(String, TOTP), Box\u003cdyn Error\u003e\u003e {\n    // create the TOTP parameters\n\n    let secret = Secret::generate_secret();\n    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();\n    let otpurl = totpmachine.get_url();\n    let otpsecret = totpmachine.get_secret_base32();\n\n    let otpid = format!(\"totp-{}\", Uuid::new_v4());\n\n    sqlx::query!(\"INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);\", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;\n\n    Ok((otpid, totpmachine))\n}\n\npub async fn verify_totp_token(otpid: String, email: String, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let totprow = sqlx::query!(\"SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on \u003e $2\", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?;\n    let secret = Secret::Encoded(totprow.totp_secret);\n    let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();\n\n    if totpmachine.get_url() != totprow.totp_otpurl {\n        return Err(\"OTPURLs do not match (email does not match?)\".into())\n    }\n\n    Ok(totpmachine)\n}\n\npub async fn use_totp_token(otpid: String, email: String, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let totpmachine = verify_totp_token(otpid.clone(), email.clone(), db).await?;\n    sqlx::query!(\"DELETE FROM totp_create_tokens WHERE id = $1\", otpid).execute(db).await?;\n    sqlx::query!(\"UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3\", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;\n    Ok(totpmachine)\n}\n\npub async fn get_totpmachine(user: i32, db: \u0026PgPool) -\u003e Result\u003cTOTP, Box\u003cdyn Error\u003e\u003e {\n    let user = sqlx::query!(\"SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1\", user).fetch_one(db).await?;\n    let secret = Secret::Encoded(user.totp_secret);\n    Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())\n}\n\npub async fn user_has_totp(user: i32, db: \u0026PgPool) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n    Ok(sqlx::query!(\"SELECT totp_verified FROM users WHERE id = $1\", user).fetch_one(db).await?.totp_verified == 1)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-api","src","util.rs"],"content":"use base64::Engine;\nuse totp_rs::Algorithm;\n\npub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1;\npub const TOTP_DIGITS: usize = 6;\npub const TOTP_SKEW: u8 = 1;\npub const TOTP_STEP: u64 = 30;\npub const TOTP_ISSUER: \u0026str = \"trifidapi\";\n\npub fn base64decode(val: \u0026str) -\u003e Result\u003cVec\u003cu8\u003e, base64::DecodeError\u003e {\n    base64::engine::general_purpose::STANDARD.decode(val)\n}\npub fn base64encode(val: Vec\u003cu8\u003e) -\u003e String {\n    base64::engine::general_purpose::STANDARD.encode(val)\n}","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","ca.rs"],"content":"//! Structs to represent a pool of CA's and blacklisted certificates\n\nuse std::collections::HashMap;\nuse std::error::Error;\nuse std::fmt::{Display, Formatter};\nuse std::time::SystemTime;\nuse ed25519_dalek::VerifyingKey;\nuse crate::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate};\n\n/// A pool of trusted CA certificates, and certificates that should be blocked.\n/// This is equivalent to the `pki` section in a typical Nebula config.yml.\n#[derive(Default)]\npub struct NebulaCAPool {\n    /// The list of CA root certificates that should be trusted.\n    pub cas: HashMap\u003cString, NebulaCertificate\u003e,\n    /// The list of blocklisted certificate fingerprints\n    pub cert_blocklist: Vec\u003cString\u003e,\n    /// True if any of the member CAs certificates are expired. Must be handled.\n    pub expired: bool\n}\n\nimpl NebulaCAPool {\n    /// Create a new, blank CA pool\n    pub fn new() -\u003e Self {\n        Self::default()\n    }\n\n    /// Create a new CA pool from a set of PEM encoded CA certificates.\n    /// If any of the certificates are expired, the pool will **still be returned**, with the expired flag set.\n    /// This must be handled properly.\n    /// # Errors\n    /// This function will return an error if PEM data provided was invalid.\n    pub fn new_from_pem(bytes: \u0026[u8]) -\u003e Result\u003cSelf, Box\u003cdyn Error\u003e\u003e {\n        let pems = pem::parse_many(bytes)?;\n\n        let mut pool = Self::new();\n\n        for cert in pems {\n            match pool.add_ca_certificate(pem::encode(\u0026cert).as_bytes()) {\n                Ok(did_expire) =\u003e if did_expire { pool.expired = true },\n                Err(e) =\u003e return Err(e)\n            }\n        }\n\n        Ok(pool)\n    }\n\n    /// Add a given CA certificate to the CA pool. If the certificate is expired, it will **still be added** - the return value will be `true` instead of `false`\n    /// # Errors\n    /// This function will return an error if the certificate is invalid in any way.\n    pub fn add_ca_certificate(\u0026mut self, bytes: \u0026[u8]) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n        let cert = deserialize_nebula_certificate_from_pem(bytes)?;\n\n        if !cert.details.is_ca {\n            return Err(CaPoolError::NotACA.into())\n        }\n\n        if !cert.check_signature(\u0026VerifyingKey::from_bytes(\u0026cert.details.public_key)?)? {\n            return Err(CaPoolError::NotSelfSigned.into())\n        }\n\n        let fingerprint = cert.sha256sum()?;\n        let expired = cert.expired(SystemTime::now());\n\n        if expired { self.expired = true }\n\n        self.cas.insert(fingerprint, cert);\n\n       Ok(expired)\n    }\n\n    /// Blocklist the given certificate in the CA pool\n    pub fn blocklist_fingerprint(\u0026mut self, fingerprint: \u0026str) {\n        self.cert_blocklist.push(fingerprint.to_string());\n    }\n\n    /// Clears the list of blocklisted fingerprints\n    pub fn reset_blocklist(\u0026mut self) {\n        self.cert_blocklist = vec![];\n    }\n\n    /// Checks if the given certificate is blocklisted\n    pub fn is_blocklisted(\u0026self, cert: \u0026NebulaCertificate) -\u003e bool {\n        let Ok(h) = cert.sha256sum() else { return false };\n        self.cert_blocklist.contains(\u0026h)\n    }\n\n    /// Gets the CA certificate used to sign the given certificate\n    /// # Errors\n    /// This function will return an error if the certificate does not have an issuer attached (it is self-signed)\n    pub fn get_ca_for_cert(\u0026self, cert: \u0026NebulaCertificate) -\u003e Result\u003cOption\u003c\u0026NebulaCertificate\u003e, Box\u003cdyn Error\u003e\u003e {\n        if cert.details.issuer == String::new() {\n            return Err(CaPoolError::NoIssuer.into())\n        }\n\n        Ok(self.cas.get(\u0026cert.details.issuer))\n    }\n\n    /// Get a list of trusted CA fingerprints\n    pub fn get_fingerprints(\u0026self) -\u003e Vec\u003c\u0026String\u003e {\n        self.cas.keys().collect()\n    }\n}\n\n#[derive(Debug)]\n/// A list of errors that can happen when working with a CA Pool\npub enum CaPoolError {\n    /// Tried to add a non-CA cert to the CA pool\n    NotACA,\n    /// Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates)\n    NotSelfSigned,\n    /// Tried to look up a certificate that does not have an issuer field\n    NoIssuer\n}\nimpl Error for CaPoolError {}\n#[cfg(not(tarpaulin_include))]\nimpl Display for CaPoolError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::NotACA =\u003e write!(f, \"Tried to add a non-CA cert to the CA pool\"),\n            Self::NotSelfSigned =\u003e write!(f, \"Tried to add a non-self-signed cert to the CA pool (all CAs must be root certificates)\"),\n            Self::NoIssuer =\u003e write!(f, \"Tried to look up a certificate with a null issuer field\")\n        }\n    }\n}","traces":[{"line":24,"address":[453696],"length":1,"stats":{"Line":1},"fn_name":"new"},{"line":25,"address":[453704],"length":1,"stats":{"Line":1},"fn_name":null},{"line":33,"address":[454852,454538,453728],"length":1,"stats":{"Line":1},"fn_name":"new_from_pem"},{"line":34,"address":[453895,453761],"length":1,"stats":{"Line":1},"fn_name":null},{"line":36,"address":[453885],"length":1,"stats":{"Line":1},"fn_name":null},{"line":38,"address":[454023,454788,454117,454667],"length":1,"stats":{"Line":3},"fn_name":null},{"line":39,"address":[454500,454418,454339],"length":1,"stats":{"Line":3},"fn_name":null},{"line":40,"address":[454759,454549],"length":1,"stats":{"Line":2},"fn_name":null},{"line":41,"address":[454586],"length":1,"stats":{"Line":0},"fn_name":null},{"line":45,"address":[454793],"length":1,"stats":{"Line":1},"fn_name":null},{"line":51,"address":[456302,454880],"length":1,"stats":{"Line":1},"fn_name":"add_ca_certificate"},{"line":52,"address":[454952,455110],"length":1,"stats":{"Line":1},"fn_name":null},{"line":54,"address":[455090],"length":1,"stats":{"Line":3},"fn_name":null},{"line":55,"address":[455190,455279],"length":1,"stats":{"Line":2},"fn_name":null},{"line":58,"address":[455304,455183,455546],"length":1,"stats":{"Line":8},"fn_name":null},{"line":59,"address":[455715],"length":1,"stats":{"Line":0},"fn_name":null},{"line":62,"address":[455708,455909,455791],"length":1,"stats":{"Line":5},"fn_name":null},{"line":63,"address":[456066,455881],"length":1,"stats":{"Line":6},"fn_name":null},{"line":65,"address":[456216,456093],"length":1,"stats":{"Line":4},"fn_name":null},{"line":67,"address":[456230,456097],"length":1,"stats":{"Line":6},"fn_name":null},{"line":69,"address":[456246],"length":1,"stats":{"Line":3},"fn_name":null},{"line":73,"address":[456352],"length":1,"stats":{"Line":1},"fn_name":"blocklist_fingerprint"},{"line":74,"address":[456371],"length":1,"stats":{"Line":1},"fn_name":null},{"line":78,"address":[456480,456416],"length":1,"stats":{"Line":1},"fn_name":"reset_blocklist"},{"line":79,"address":[456434,456512],"length":1,"stats":{"Line":2},"fn_name":null},{"line":83,"address":[456832,456804,456544],"length":1,"stats":{"Line":2},"fn_name":"is_blocklisted"},{"line":84,"address":[456686,456743,456566],"length":1,"stats":{"Line":2},"fn_name":null},{"line":85,"address":[456765,456713],"length":1,"stats":{"Line":4},"fn_name":null},{"line":91,"address":[456982,456848],"length":1,"stats":{"Line":1},"fn_name":"get_ca_for_cert"},{"line":92,"address":[456881],"length":1,"stats":{"Line":1},"fn_name":null},{"line":93,"address":[457047],"length":1,"stats":{"Line":0},"fn_name":null},{"line":96,"address":[457014],"length":1,"stats":{"Line":1},"fn_name":null},{"line":100,"address":[457088],"length":1,"stats":{"Line":1},"fn_name":"get_fingerprints"},{"line":101,"address":[457107],"length":1,"stats":{"Line":1},"fn_name":null}],"covered":31,"coverable":34},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","cert.rs"],"content":"//! Manage Nebula PKI Certificates\n//! This is pretty much a direct port of nebula/cert/cert.go\n\nuse std::error::Error;\nuse std::fmt::{Display, Formatter};\nuse std::net::Ipv4Addr;\nuse std::ops::Add;\nuse std::time::{Duration, SystemTime, UNIX_EPOCH};\nuse ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};\nuse ipnet::{Ipv4Net};\nuse pem::Pem;\nuse quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};\nuse sha2::Sha256;\nuse crate::ca::NebulaCAPool;\nuse crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};\nuse sha2::Digest;\n\n/// The length, in bytes, of public keys\npub const PUBLIC_KEY_LENGTH: i32 = 32;\n\n/// The PEM banner for certificates\npub const CERT_BANNER: \u0026str = \"NEBULA CERTIFICATE\";\n/// The PEM banner for X25519 private keys\npub const X25519_PRIVATE_KEY_BANNER: \u0026str = \"NEBULA X25519 PRIVATE KEY\";\n/// The PEM banner for X25519 public keys\npub const X25519_PUBLIC_KEY_BANNER: \u0026str = \"NEBULA X25519 PUBLIC KEY\";\n/// The PEM banner for Ed25519 private keys\npub const ED25519_PRIVATE_KEY_BANNER: \u0026str = \"NEBULA ED25519 PRIVATE KEY\";\n/// The PEM banner for Ed25519 public keys\npub const ED25519_PUBLIC_KEY_BANNER: \u0026str = \"NEBULA ED25519 PUBLIC KEY\";\n\n/// A Nebula PKI certificate\n#[derive(Debug, Clone)]\npub struct NebulaCertificate {\n    /// The signed data of this certificate\n    pub details: NebulaCertificateDetails,\n    /// The Ed25519 signature of this certificate\n    pub signature: Vec\u003cu8\u003e\n}\n\n/// The signed details contained in a Nebula PKI certificate\n#[derive(Debug, Clone)]\npub struct NebulaCertificateDetails {\n    /// The name of the identity this certificate was issued for\n    pub name: String,\n    /// The IPv4 addresses issued to this node\n    pub ips: Vec\u003cIpv4Net\u003e,\n    /// The IPv4 subnets this node is responsible for routing\n    pub subnets: Vec\u003cIpv4Net\u003e,\n    /// The groups this node is a part of\n    pub groups: Vec\u003cString\u003e,\n    /// Certificate start date and time\n    pub not_before: SystemTime,\n    /// Certificate expiry date and time\n    pub not_after: SystemTime,\n    /// Public key\n    pub public_key: [u8; PUBLIC_KEY_LENGTH as usize],\n    /// Is this node a CA?\n    pub is_ca: bool,\n    /// SHA256 of issuer certificate. If blank, this cert is self-signed.\n    pub issuer: String\n}\n\n/// A list of errors that can occur parsing certificates\n#[derive(Debug)]\npub enum CertificateError {\n    /// Attempted to deserialize a certificate from an empty byte array\n    EmptyByteArray,\n    /// The encoded Details field is null\n    NilDetails,\n    /// The encoded Ips field is not formatted correctly\n    IpsNotPairs,\n    /// The encoded Subnets field is not formatted correctly\n    SubnetsNotPairs,\n    /// Signatures are expected to be 64 bytes but the signature on the certificate was a different length\n    WrongSigLength,\n    /// Public keys are expected to be 32 bytes but the public key on this cert is not\n    WrongKeyLength,\n    /// Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not\n    WrongPemTag,\n    /// This certificate either is not yet valid or has already expired\n    Expired,\n    /// The public key does not match the expected value\n    KeyMismatch\n}\n#[cfg(not(tarpaulin_include))]\nimpl Display for CertificateError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::EmptyByteArray =\u003e write!(f, \"Certificate bytearray is empty\"),\n            Self::NilDetails =\u003e write!(f, \"The encoded Details field is null\"),\n            Self::IpsNotPairs =\u003e write!(f, \"encoded IPs should be in pairs, an odd number was found\"),\n            Self::SubnetsNotPairs =\u003e write!(f, \"encoded subnets should be in pairs, an odd number was found\"),\n            Self::WrongSigLength =\u003e write!(f, \"Signature should be 64 bytes but is a different size\"),\n            Self::WrongKeyLength =\u003e write!(f, \"Public keys are expected to be 32 bytes but the public key on this cert is not\"),\n            Self::WrongPemTag =\u003e write!(f, \"Certificates should have the PEM tag `NEBULA CERTIFICATE`, but this block did not\"),\n            Self::Expired =\u003e write!(f, \"This certificate either is not yet valid or has already expired\"),\n            Self::KeyMismatch =\u003e write!(f, \"Key does not match expected value\")\n        }\n    }\n}\nimpl Error for CertificateError {}\n\nfn map_cidr_pairs(pairs: \u0026[u32]) -\u003e Result\u003cVec\u003cIpv4Net\u003e, Box\u003cdyn Error\u003e\u003e {\n    let mut res_vec = vec![];\n    for pair in pairs.chunks(2) {\n        res_vec.push(Ipv4Net::with_netmask(Ipv4Addr::from(pair[0]), Ipv4Addr::from(pair[1]))?);\n    }\n    Ok(res_vec)\n}\n\n#[cfg(not(tarpaulin_include))]\nimpl Display for NebulaCertificate {\n    #[allow(clippy::unwrap_used)]\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        writeln!(f, \"NebulaCertificate {{\")?;\n        writeln!(f, \"   Details {{\")?;\n        writeln!(f, \"       Name: {}\", self.details.name)?;\n        writeln!(f, \"       Ips: {:?}\", self.details.ips)?;\n        writeln!(f, \"       Subnets: {:?}\", self.details.subnets)?;\n        writeln!(f, \"       Groups: {:?}\", self.details.groups)?;\n        writeln!(f, \"       Not before: {:?}\", self.details.not_before)?;\n        writeln!(f, \"       Not after: {:?}\", self.details.not_after)?;\n        writeln!(f, \"       Is CA: {}\", self.details.is_ca)?;\n        writeln!(f, \"       Issuer: {}\", self.details.issuer)?;\n        writeln!(f, \"       Public key: {}\", hex::encode(self.details.public_key))?;\n        writeln!(f, \"   }}\")?;\n        writeln!(f, \"   Fingerprint: {}\", self.sha256sum().unwrap())?;\n        writeln!(f, \"   Signature: {}\", hex::encode(self.signature.clone()))?;\n        writeln!(f, \"}}\")\n    }\n}\n\n/// Given a protobuf-encoded certificate bytearray, deserialize it into a `NebulaCertificate` object.\n/// # Errors\n/// This function will return an error if there is a protobuf parsing error, or if the certificate data is invalid.\n/// # Panics\npub fn deserialize_nebula_certificate(bytes: \u0026[u8]) -\u003e Result\u003cNebulaCertificate, Box\u003cdyn Error\u003e\u003e {\n    if bytes.is_empty() {\n        return Err(CertificateError::EmptyByteArray.into())\n    }\n\n    let mut reader = BytesReader::from_bytes(bytes);\n\n    let raw_cert = RawNebulaCertificate::from_reader(\u0026mut reader, bytes)?;\n\n    let details = raw_cert.Details.ok_or(CertificateError::NilDetails)?;\n\n    if details.Ips.len() % 2 != 0 {\n        return Err(CertificateError::IpsNotPairs.into())\n    }\n\n    if details.Subnets.len() % 2 != 0 {\n        return Err(CertificateError::SubnetsNotPairs.into())\n    }\n\n    let mut nebula_cert;\n    #[allow(clippy::cast_sign_loss)]\n    {\n        nebula_cert = NebulaCertificate {\n            details: NebulaCertificateDetails {\n                name: details.Name.to_string(),\n                ips: map_cidr_pairs(\u0026details.Ips)?,\n                subnets: map_cidr_pairs(\u0026details.Subnets)?,\n                groups: details.Groups.iter().map(std::string::ToString::to_string).collect(),\n                not_before: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotBefore as u64)),\n                not_after: SystemTime::UNIX_EPOCH.add(Duration::from_secs(details.NotAfter as u64)),\n                public_key: [0u8; 32],\n                is_ca: details.IsCA,\n                issuer: hex::encode(details.Issuer),\n            },\n            signature: vec![],\n        };\n    }\n\n    nebula_cert.signature = raw_cert.Signature;\n\n    if details.PublicKey.len() != 32 {\n        return Err(CertificateError::WrongKeyLength.into())\n    }\n\n    #[allow(clippy::unwrap_used)] { nebula_cert.details.public_key = details.PublicKey.try_into().unwrap(); }\n\n    Ok(nebula_cert)\n}\n\n/// A list of errors that can occur parsing keys\n#[derive(Debug)]\npub enum KeyError {\n    /// Keys should have their associated PEM tags but this had the wrong one\n    WrongPemTag,\n    /// Ed25519 private keys are 64 bytes\n    Not64Bytes,\n    /// X25519 private keys are 32 bytes\n    Not32Bytes\n}\n#[cfg(not(tarpaulin_include))]\nimpl Display for KeyError {\n    fn fmt(\u0026self, f: \u0026mut Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n        match self {\n            Self::WrongPemTag =\u003e write!(f, \"Keys should have their associated PEM tags but this had the wrong one\"),\n            Self::Not64Bytes =\u003e write!(f, \"Ed25519 private keys are 64 bytes\"),\n            Self::Not32Bytes =\u003e write!(f, \"X25519 private keys are 32 bytes\")\n        }\n    }\n}\nimpl Error for KeyError {}\n\n\n/// Deserialize the first PEM block in the given byte array into a `NebulaCertificate`\n/// # Errors\n/// This function will return an error if the PEM data is invalid, or if there is an error parsing the certificate (see `deserialize_nebula_certificate`)\npub fn deserialize_nebula_certificate_from_pem(bytes: \u0026[u8]) -\u003e Result\u003cNebulaCertificate, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != CERT_BANNER {\n        return Err(CertificateError::WrongPemTag.into())\n    }\n    deserialize_nebula_certificate(\u0026pem.contents)\n}\n\n/// Simple helper to PEM encode an X25519 private key\npub fn serialize_x25519_private(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: X25519_PRIVATE_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Simple helper to PEM encode an X25519 public key\npub fn serialize_x25519_public(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: X25519_PUBLIC_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Attempt to deserialize a PEM encoded X25519 private key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_x25519_private(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != X25519_PRIVATE_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 32 {\n        return Err(KeyError::Not32Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Attempt to deserialize a PEM encoded X25519 public key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_x25519_public(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != X25519_PUBLIC_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 32 {\n        return Err(KeyError::Not32Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Simple helper to PEM encode an Ed25519 private key\npub fn serialize_ed25519_private(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: ED25519_PRIVATE_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Simple helper to PEM encode an Ed25519 public key\npub fn serialize_ed25519_public(bytes: \u0026[u8]) -\u003e Vec\u003cu8\u003e {\n    pem::encode(\u0026Pem {\n        tag: ED25519_PUBLIC_KEY_BANNER.to_string(),\n        contents: bytes.to_vec(),\n    }).as_bytes().to_vec()\n}\n\n/// Attempt to deserialize a PEM encoded Ed25519 private key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_ed25519_private(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != ED25519_PRIVATE_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 64 {\n        return Err(KeyError::Not64Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\n/// Attempt to deserialize a PEM encoded Ed25519 public key\n/// # Errors\n/// This function will return an error if the PEM data is invalid or has the wrong tag\npub fn deserialize_ed25519_public(bytes: \u0026[u8]) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n    let pem = pem::parse(bytes)?;\n    if pem.tag != ED25519_PUBLIC_KEY_BANNER {\n        return Err(KeyError::WrongPemTag.into())\n    }\n    if pem.contents.len() != 64 {\n        return Err(KeyError::Not64Bytes.into())\n    }\n    Ok(pem.contents)\n}\n\nimpl NebulaCertificate {\n    /// Sign a nebula certificate with the provided private key\n    /// # Errors\n    /// This function will return an error if the certificate could not be serialized or signed.\n    pub fn sign(\u0026mut self, key: \u0026SigningKey) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n        let mut out = Vec::new();\n        let mut writer = Writer::new(\u0026mut out);\n        self.get_raw_details().write_message(\u0026mut writer)?;\n\n        self.signature = key.sign(\u0026out).to_vec();\n        Ok(())\n    }\n\n    /// Verify the signature on a certificate with the provided public key\n    /// # Errors\n    /// This function will return an error if the certificate could not be serialized or the signature could not be checked.\n    pub fn check_signature(\u0026self, key: \u0026VerifyingKey) -\u003e Result\u003cbool, Box\u003cdyn Error\u003e\u003e {\n        let mut out = Vec::new();\n        let mut writer = Writer::new(\u0026mut out);\n        self.get_raw_details().write_message(\u0026mut writer)?;\n\n        let sig = Signature::from_slice(\u0026self.signature)?;\n\n        Ok(key.verify(\u0026out, \u0026sig).is_ok())\n    }\n\n    /// Returns true if the signature is too young or too old compared to the provided time\n    pub fn expired(\u0026self, time: SystemTime) -\u003e bool {\n        self.details.not_before \u003e time || self.details.not_after \u003c time\n    }\n\n    /// Verify will ensure a certificate is good in all respects (expiry, group membership, signature, cert blocklist, etc)\n    /// # Errors\n    /// This function will return an error if there is an error parsing the cert or the CA pool.\n    pub fn verify(\u0026self, time: SystemTime, ca_pool: \u0026NebulaCAPool) -\u003e Result\u003cCertificateValidity, Box\u003cdyn Error\u003e\u003e {\n        if ca_pool.is_blocklisted(self) {\n            return Ok(CertificateValidity::Blocklisted);\n        }\n\n        let Some(signer) = ca_pool.get_ca_for_cert(self)? else { return Ok(CertificateValidity::NotSignedByThisCAPool) };\n\n        if signer.expired(time) {\n            return Ok(CertificateValidity::RootCertExpired)\n        }\n\n        if self.expired(time) {\n            return Ok(CertificateValidity::CertExpired)\n        }\n\n        if !self.check_signature(\u0026VerifyingKey::from_bytes(\u0026signer.details.public_key)?)? {\n            return Ok(CertificateValidity::BadSignature)\n        }\n\n        Ok(self.check_root_constraints(signer))\n    }\n\n    /// Make sure that this certificate does not break any of the constraints set by the signing certificate\n    pub fn check_root_constraints(\u0026self, signer: \u0026Self) -\u003e CertificateValidity {\n        // Make sure this cert doesn't expire after the signer\n        println!(\"{:?} {:?}\", signer.details.not_before, self.details.not_before);\n        if signer.details.not_before \u003c self.details.not_before {\n            return CertificateValidity::CertExpiresAfterSigner;\n        }\n\n        // Make sure this cert doesn't come into validity before the root\n        if signer.details.not_before \u003e self.details.not_before {\n            return CertificateValidity::CertValidBeforeSigner;\n        }\n\n        // If the signer contains a limited set of groups, make sure this cert only has a subset of them\n        if !signer.details.groups.is_empty() {\n            println!(\"root groups: {:?}, child groups: {:?}\", signer.details.groups, self.details.groups);\n            for group in \u0026self.details.groups {\n                if !signer.details.groups.contains(group) {\n                    return CertificateValidity::GroupNotPresentOnSigner;\n                }\n            }\n        }\n\n        // If the signer contains a limited set of IP ranges, make sure the cert only contains a subset\n        if !signer.details.ips.is_empty() {\n            for ip in \u0026self.details.ips {\n                if !net_match(*ip, \u0026signer.details.ips) {\n                    return CertificateValidity::IPNotPresentOnSigner;\n                }\n            }\n        }\n\n        // If the signer contains a limited set of subnets, make sure the cert only contains a subset\n        if !signer.details.subnets.is_empty() {\n            for subnet in \u0026self.details.subnets {\n                if !net_match(*subnet, \u0026signer.details.subnets) {\n                    return CertificateValidity::SubnetNotPresentOnSigner;\n                }\n            }\n        }\n\n        CertificateValidity::Ok\n    }\n\n    #[allow(clippy::unwrap_used)]\n    /// Verify if the given private key corresponds to the public key contained in this certificate\n    /// # Errors\n    /// This function will return an error if either keys are invalid.\n    /// # Panics\n    /// This function, while containing calls to unwrap, has proper bounds checking and will not panic.\n    pub fn verify_private_key(\u0026self, key: \u0026[u8]) -\u003e Result\u003c(), Box\u003cdyn Error\u003e\u003e {\n        if self.details.is_ca {\n            // convert the keys\n            if key.len() != 64 {\n                return Err(\"key not 64-bytes long\".into())\n            }\n\n\n            let secret = SigningKey::from_keypair_bytes(key.try_into().unwrap())?;\n            let pub_key = secret.verifying_key().to_bytes();\n            if pub_key != self.details.public_key {\n                return Err(CertificateError::KeyMismatch.into());\n            }\n\n            return Ok(());\n        }\n\n        if key.len() != 32 {\n            return Err(\"key not 32-bytes long\".into())\n        }\n\n        let pubkey_raw = SigningKey::from_bytes(key.try_into()?).verifying_key();\n        let pubkey = pubkey_raw.as_bytes();\n\n        println!(\"{} {}\", hex::encode(pubkey), hex::encode(self.details.public_key));\n        if *pubkey != self.details.public_key {\n            return Err(CertificateError::KeyMismatch.into());\n        }\n\n        Ok(())\n    }\n\n\n    /// Get a protobuf-ready raw struct, ready for serialization\n    #[allow(clippy::expect_used)]\n    #[allow(clippy::cast_possible_wrap)]\n    /// # Panics\n    /// This function will panic if time went backwards, or if the certificate contains extremely invalid data.\n    pub fn get_raw_details(\u0026self) -\u003e RawNebulaCertificateDetails {\n\n        let mut raw = RawNebulaCertificateDetails {\n            Name: self.details.name.clone(),\n            Ips: vec![],\n            Subnets: vec![],\n            Groups: self.details.groups.iter().map(std::convert::Into::into).collect(),\n            NotBefore: self.details.not_before.duration_since(UNIX_EPOCH).expect(\"Time went backwards\").as_secs() as i64,\n            NotAfter: self.details.not_after.duration_since(UNIX_EPOCH).expect(\"Time went backwards\").as_secs() as i64,\n            PublicKey: self.details.public_key.into(),\n            IsCA: self.details.is_ca,\n            Issuer: hex::decode(\u0026self.details.issuer).expect(\"Issuer was not a hex-encoded value\"),\n        };\n\n        for ip_net in \u0026self.details.ips {\n            raw.Ips.push(ip_net.addr().into());\n            raw.Ips.push(ip_net.netmask().into());\n        }\n\n        for subnet in \u0026self.details.subnets {\n            raw.Subnets.push(subnet.addr().into());\n            raw.Subnets.push(subnet.netmask().into());\n        }\n\n        raw\n    }\n\n    /// Will serialize this cert into a protobuf byte array.\n    /// # Errors\n    /// This function will return an error if protobuf was unable to serialize the data.\n    pub fn serialize(\u0026self) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n        let raw_cert = RawNebulaCertificate {\n            Details: Some(self.get_raw_details()),\n            Signature: self.signature.clone(),\n        };\n\n        let mut out = vec![];\n        let mut writer = Writer::new(\u0026mut out);\n        raw_cert.write_message(\u0026mut writer)?;\n\n        Ok(out)\n    }\n\n    /// Will serialize this cert into a PEM byte array.\n    /// # Errors\n    /// This function will return an error if protobuf was unable to serialize the data.\n    pub fn serialize_to_pem(\u0026self) -\u003e Result\u003cVec\u003cu8\u003e, Box\u003cdyn Error\u003e\u003e {\n        let pbuf_bytes = self.serialize()?;\n\n        Ok(pem::encode(\u0026Pem {\n            tag: CERT_BANNER.to_string(),\n            contents: pbuf_bytes,\n        }).as_bytes().to_vec())\n    }\n\n    /// Get the fingerprint of this certificate\n    /// # Errors\n    /// This functiom will return an error if protobuf was unable to serialize the cert.\n    pub fn sha256sum(\u0026self) -\u003e Result\u003cString, Box\u003cdyn Error\u003e\u003e {\n        let pbuf_bytes = self.serialize()?;\n\n        let mut hasher = Sha256::new();\n        hasher.update(pbuf_bytes);\n\n        Ok(hex::encode(hasher.finalize()))\n    }\n}\n\n/// A list of possible errors that can happen validating a certificate\n#[derive(Eq, PartialEq, Debug)]\npub enum CertificateValidity {\n    /// There are no issues with this certificate\n    Ok,\n    /// This cert has been blocklisted in the given CA pool\n    Blocklisted,\n    /// The certificate that signed this cert is expired\n    RootCertExpired,\n    /// This cert is expired\n    CertExpired,\n    /// This cert's signature is invalid\n    BadSignature,\n    /// This cert was not signed by any CAs in the CA pool\n    NotSignedByThisCAPool,\n    /// This cert expires after the signer's cert expires\n    CertExpiresAfterSigner,\n    /// This cert enters validity before the signer's cert does\n    CertValidBeforeSigner,\n    /// A group present on this certificate is not present on the signer's certificate\n    GroupNotPresentOnSigner,\n    /// An IP present on this certificate is not present on the signer's certificate\n    IPNotPresentOnSigner,\n    /// A subnet on this certificate is not present on the signer's certificate\n    SubnetNotPresentOnSigner\n}\n\nfn net_match(cert_ip: Ipv4Net, root_ips: \u0026Vec\u003cIpv4Net\u003e) -\u003e bool {\n    for net in root_ips {\n        if net.contains(\u0026cert_ip) {\n            return true;\n        }\n    }\n    false\n}","traces":[{"line":104,"address":[549155,548224],"length":1,"stats":{"Line":1},"fn_name":"map_cidr_pairs"},{"line":105,"address":[548267],"length":1,"stats":{"Line":1},"fn_name":null},{"line":106,"address":[548589,548304,548365],"length":1,"stats":{"Line":3},"fn_name":null},{"line":107,"address":[549150,548631],"length":1,"stats":{"Line":2},"fn_name":null},{"line":109,"address":[548502],"length":1,"stats":{"Line":1},"fn_name":null},{"line":138,"address":[553592,557101,552064],"length":1,"stats":{"Line":4},"fn_name":"deserialize_nebula_certificate"},{"line":139,"address":[552141],"length":1,"stats":{"Line":4},"fn_name":null},{"line":140,"address":[552335],"length":1,"stats":{"Line":1},"fn_name":null},{"line":143,"address":[552219],"length":1,"stats":{"Line":3},"fn_name":null},{"line":145,"address":[552263,552598,552449],"length":1,"stats":{"Line":6},"fn_name":null},{"line":147,"address":[552509,552824,553002],"length":1,"stats":{"Line":6},"fn_name":null},{"line":149,"address":[553149,552964],"length":1,"stats":{"Line":6},"fn_name":null},{"line":150,"address":[553186],"length":1,"stats":{"Line":1},"fn_name":null},{"line":153,"address":[553293,553159],"length":1,"stats":{"Line":4},"fn_name":null},{"line":154,"address":[553326],"length":1,"stats":{"Line":1},"fn_name":null},{"line":160,"address":[555110],"length":1,"stats":{"Line":3},"fn_name":null},{"line":161,"address":[554782],"length":1,"stats":{"Line":3},"fn_name":null},{"line":162,"address":[553303],"length":1,"stats":{"Line":1},"fn_name":null},{"line":163,"address":[553603,553449,553713,553549],"length":1,"stats":{"Line":3},"fn_name":null},{"line":164,"address":[554067,553916,553667],"length":1,"stats":{"Line":2},"fn_name":null},{"line":165,"address":[554344,554021],"length":1,"stats":{"Line":2},"fn_name":null},{"line":166,"address":[554526,554437],"length":1,"stats":{"Line":4},"fn_name":null},{"line":167,"address":[554583],"length":1,"stats":{"Line":2},"fn_name":null},{"line":168,"address":[554667],"length":1,"stats":{"Line":2},"fn_name":null},{"line":169,"address":[554686],"length":1,"stats":{"Line":2},"fn_name":null},{"line":170,"address":[554697],"length":1,"stats":{"Line":2},"fn_name":null},{"line":172,"address":[555013],"length":1,"stats":{"Line":3},"fn_name":null},{"line":176,"address":[555185],"length":1,"stats":{"Line":4},"fn_name":null},{"line":178,"address":[555367],"length":1,"stats":{"Line":4},"fn_name":null},{"line":179,"address":[555468],"length":1,"stats":{"Line":1},"fn_name":null},{"line":184,"address":[556429],"length":1,"stats":{"Line":3},"fn_name":null},{"line":213,"address":[557392,557869],"length":1,"stats":{"Line":1},"fn_name":"deserialize_nebula_certificate_from_pem"},{"line":214,"address":[557425,557589],"length":1,"stats":{"Line":2},"fn_name":null},{"line":215,"address":[557721,557563],"length":1,"stats":{"Line":2},"fn_name":null},{"line":216,"address":[557753],"length":1,"stats":{"Line":1},"fn_name":null},{"line":218,"address":[557846,557727],"length":1,"stats":{"Line":2},"fn_name":null},{"line":222,"address":[557904,558115],"length":1,"stats":{"Line":1},"fn_name":"serialize_x25519_private"},{"line":223,"address":[558184,558254,558042],"length":1,"stats":{"Line":3},"fn_name":null},{"line":224,"address":[557947],"length":1,"stats":{"Line":1},"fn_name":null},{"line":225,"address":[557982],"length":1,"stats":{"Line":1},"fn_name":null},{"line":226,"address":[558001],"length":1,"stats":{"Line":0},"fn_name":null},{"line":230,"address":[558531,558320],"length":1,"stats":{"Line":1},"fn_name":"serialize_x25519_public"},{"line":231,"address":[558600,558458,558670],"length":1,"stats":{"Line":3},"fn_name":null},{"line":232,"address":[558363],"length":1,"stats":{"Line":1},"fn_name":null},{"line":233,"address":[558398],"length":1,"stats":{"Line":1},"fn_name":null},{"line":234,"address":[558417],"length":1,"stats":{"Line":0},"fn_name":null},{"line":240,"address":[559351,558736],"length":1,"stats":{"Line":1},"fn_name":"deserialize_x25519_private"},{"line":241,"address":[558769,558933],"length":1,"stats":{"Line":2},"fn_name":null},{"line":242,"address":[558907,559065],"length":1,"stats":{"Line":2},"fn_name":null},{"line":243,"address":[559092],"length":1,"stats":{"Line":1},"fn_name":null},{"line":245,"address":[559071,559176],"length":1,"stats":{"Line":2},"fn_name":null},{"line":246,"address":[559285],"length":1,"stats":{"Line":1},"fn_name":null},{"line":248,"address":[559187],"length":1,"stats":{"Line":2},"fn_name":null},{"line":254,"address":[559392,560007],"length":1,"stats":{"Line":1},"fn_name":"deserialize_x25519_public"},{"line":255,"address":[559425,559589],"length":1,"stats":{"Line":2},"fn_name":null},{"line":256,"address":[559721,559563],"length":1,"stats":{"Line":2},"fn_name":null},{"line":257,"address":[559748],"length":1,"stats":{"Line":0},"fn_name":null},{"line":259,"address":[559727,559832],"length":1,"stats":{"Line":2},"fn_name":null},{"line":260,"address":[559941],"length":1,"stats":{"Line":1},"fn_name":null},{"line":262,"address":[559843],"length":1,"stats":{"Line":1},"fn_name":null},{"line":266,"address":[560048,560259],"length":1,"stats":{"Line":1},"fn_name":"serialize_ed25519_private"},{"line":267,"address":[560186,560398,560328],"length":1,"stats":{"Line":3},"fn_name":null},{"line":268,"address":[560091],"length":1,"stats":{"Line":1},"fn_name":null},{"line":269,"address":[560126],"length":1,"stats":{"Line":1},"fn_name":null},{"line":270,"address":[560145],"length":1,"stats":{"Line":0},"fn_name":null},{"line":274,"address":[560464,560675],"length":1,"stats":{"Line":1},"fn_name":"serialize_ed25519_public"},{"line":275,"address":[560602,560744,560814],"length":1,"stats":{"Line":3},"fn_name":null},{"line":276,"address":[560507],"length":1,"stats":{"Line":1},"fn_name":null},{"line":277,"address":[560542],"length":1,"stats":{"Line":1},"fn_name":null},{"line":278,"address":[560561],"length":1,"stats":{"Line":0},"fn_name":null},{"line":284,"address":[560880,561495],"length":1,"stats":{"Line":1},"fn_name":"deserialize_ed25519_private"},{"line":285,"address":[561077,560913],"length":1,"stats":{"Line":2},"fn_name":null},{"line":286,"address":[561051,561209],"length":1,"stats":{"Line":2},"fn_name":null},{"line":287,"address":[561236],"length":1,"stats":{"Line":1},"fn_name":null},{"line":289,"address":[561215,561320],"length":1,"stats":{"Line":2},"fn_name":null},{"line":290,"address":[561429],"length":1,"stats":{"Line":1},"fn_name":null},{"line":292,"address":[561331],"length":1,"stats":{"Line":1},"fn_name":null},{"line":298,"address":[561536,562151],"length":1,"stats":{"Line":1},"fn_name":"deserialize_ed25519_public"},{"line":299,"address":[561733,561569],"length":1,"stats":{"Line":2},"fn_name":null},{"line":300,"address":[561865,561707],"length":1,"stats":{"Line":2},"fn_name":null},{"line":301,"address":[561892],"length":1,"stats":{"Line":1},"fn_name":null},{"line":303,"address":[561871,561976],"length":1,"stats":{"Line":2},"fn_name":null},{"line":304,"address":[562085],"length":1,"stats":{"Line":1},"fn_name":null},{"line":306,"address":[561987],"length":1,"stats":{"Line":1},"fn_name":null},{"line":313,"address":[562192,562914,562612],"length":1,"stats":{"Line":3},"fn_name":"sign"},{"line":314,"address":[562225],"length":1,"stats":{"Line":3},"fn_name":null},{"line":315,"address":[562312,562249],"length":1,"stats":{"Line":6},"fn_name":null},{"line":316,"address":[562325],"length":1,"stats":{"Line":3},"fn_name":null},{"line":318,"address":[562652],"length":1,"stats":{"Line":2},"fn_name":null},{"line":319,"address":[562889],"length":1,"stats":{"Line":4},"fn_name":null},{"line":325,"address":[563794,562944,563349],"length":1,"stats":{"Line":1},"fn_name":"check_signature"},{"line":326,"address":[562987],"length":1,"stats":{"Line":1},"fn_name":null},{"line":327,"address":[563011,563074],"length":1,"stats":{"Line":2},"fn_name":null},{"line":328,"address":[563087],"length":1,"stats":{"Line":1},"fn_name":null},{"line":330,"address":[563381,563642],"length":1,"stats":{"Line":2},"fn_name":null},{"line":332,"address":[563611,563748,563856],"length":1,"stats":{"Line":3},"fn_name":null},{"line":336,"address":[563952],"length":1,"stats":{"Line":1},"fn_name":"expired"},{"line":337,"address":[563974],"length":1,"stats":{"Line":1},"fn_name":null},{"line":343,"address":[564048],"length":1,"stats":{"Line":1},"fn_name":"verify"},{"line":344,"address":[564123],"length":1,"stats":{"Line":1},"fn_name":null},{"line":345,"address":[564200],"length":1,"stats":{"Line":1},"fn_name":null},{"line":348,"address":[564224,564142,564357,564324],"length":1,"stats":{"Line":5},"fn_name":null},{"line":350,"address":[564341],"length":1,"stats":{"Line":2},"fn_name":null},{"line":351,"address":[564403],"length":1,"stats":{"Line":1},"fn_name":null},{"line":354,"address":[564387],"length":1,"stats":{"Line":1},"fn_name":null},{"line":355,"address":[564479],"length":1,"stats":{"Line":1},"fn_name":null},{"line":358,"address":[564495,564671,564423],"length":1,"stats":{"Line":5},"fn_name":null},{"line":359,"address":[564781],"length":1,"stats":{"Line":0},"fn_name":null},{"line":362,"address":[564749],"length":1,"stats":{"Line":2},"fn_name":null},{"line":366,"address":[564800],"length":1,"stats":{"Line":2},"fn_name":"check_root_constraints"},{"line":368,"address":[564846],"length":1,"stats":{"Line":2},"fn_name":null},{"line":369,"address":[564972],"length":1,"stats":{"Line":2},"fn_name":null},{"line":370,"address":[565018],"length":1,"stats":{"Line":0},"fn_name":null},{"line":374,"address":[564999],"length":1,"stats":{"Line":2},"fn_name":null},{"line":375,"address":[565060],"length":1,"stats":{"Line":0},"fn_name":null},{"line":379,"address":[565040],"length":1,"stats":{"Line":2},"fn_name":null},{"line":380,"address":[565101],"length":1,"stats":{"Line":1},"fn_name":null},{"line":381,"address":[565240],"length":1,"stats":{"Line":1},"fn_name":null},{"line":382,"address":[565348],"length":1,"stats":{"Line":1},"fn_name":null},{"line":383,"address":[565384],"length":1,"stats":{"Line":1},"fn_name":null},{"line":389,"address":[565072],"length":1,"stats":{"Line":1},"fn_name":null},{"line":390,"address":[565428],"length":1,"stats":{"Line":1},"fn_name":null},{"line":391,"address":[565525],"length":1,"stats":{"Line":1},"fn_name":null},{"line":392,"address":[565588],"length":1,"stats":{"Line":1},"fn_name":null},{"line":398,"address":[565399],"length":1,"stats":{"Line":1},"fn_name":null},{"line":399,"address":[565613],"length":1,"stats":{"Line":1},"fn_name":null},{"line":400,"address":[565710],"length":1,"stats":{"Line":1},"fn_name":null},{"line":401,"address":[565773],"length":1,"stats":{"Line":1},"fn_name":null},{"line":406,"address":[565598],"length":1,"stats":{"Line":1},"fn_name":null},{"line":415,"address":[565792,566389],"length":1,"stats":{"Line":1},"fn_name":"verify_private_key"},{"line":416,"address":[565847],"length":1,"stats":{"Line":1},"fn_name":null},{"line":418,"address":[565887],"length":1,"stats":{"Line":1},"fn_name":null},{"line":419,"address":[565983],"length":1,"stats":{"Line":0},"fn_name":null},{"line":423,"address":[566145,565909,566040],"length":1,"stats":{"Line":2},"fn_name":null},{"line":424,"address":[566126,566270],"length":1,"stats":{"Line":2},"fn_name":null},{"line":425,"address":[566293],"length":1,"stats":{"Line":1},"fn_name":null},{"line":426,"address":[566328],"length":1,"stats":{"Line":1},"fn_name":null},{"line":429,"address":[566314],"length":1,"stats":{"Line":1},"fn_name":null},{"line":432,"address":[565864],"length":1,"stats":{"Line":1},"fn_name":null},{"line":433,"address":[566503],"length":1,"stats":{"Line":0},"fn_name":null},{"line":436,"address":[566541,566450],"length":1,"stats":{"Line":2},"fn_name":null},{"line":437,"address":[566701],"length":1,"stats":{"Line":1},"fn_name":null},{"line":439,"address":[566738],"length":1,"stats":{"Line":1},"fn_name":null},{"line":440,"address":[567118],"length":1,"stats":{"Line":1},"fn_name":null},{"line":441,"address":[567144],"length":1,"stats":{"Line":1},"fn_name":null},{"line":444,"address":[567127],"length":1,"stats":{"Line":1},"fn_name":null},{"line":453,"address":[567200,568321],"length":1,"stats":{"Line":5},"fn_name":"get_raw_details"},{"line":456,"address":[567239],"length":1,"stats":{"Line":5},"fn_name":null},{"line":457,"address":[567268],"length":1,"stats":{"Line":6},"fn_name":null},{"line":458,"address":[567327],"length":1,"stats":{"Line":5},"fn_name":null},{"line":459,"address":[567383,567474],"length":1,"stats":{"Line":11},"fn_name":null},{"line":460,"address":[567575,567655],"length":1,"stats":{"Line":11},"fn_name":null},{"line":461,"address":[567765],"length":1,"stats":{"Line":5},"fn_name":null},{"line":462,"address":[567911],"length":1,"stats":{"Line":5},"fn_name":null},{"line":463,"address":[567965],"length":1,"stats":{"Line":6},"fn_name":null},{"line":464,"address":[568044,567978],"length":1,"stats":{"Line":11},"fn_name":null},{"line":467,"address":[568528,568400,568294],"length":1,"stats":{"Line":13},"fn_name":null},{"line":468,"address":[568549],"length":1,"stats":{"Line":2},"fn_name":null},{"line":469,"address":[568649],"length":1,"stats":{"Line":2},"fn_name":null},{"line":472,"address":[568751,568496,568857],"length":1,"stats":{"Line":8},"fn_name":null},{"line":473,"address":[568878],"length":1,"stats":{"Line":1},"fn_name":null},{"line":474,"address":[568978],"length":1,"stats":{"Line":1},"fn_name":null},{"line":483,"address":[569287,569072],"length":1,"stats":{"Line":1},"fn_name":"serialize"},{"line":485,"address":[569115],"length":1,"stats":{"Line":1},"fn_name":null},{"line":486,"address":[569152],"length":1,"stats":{"Line":1},"fn_name":null},{"line":489,"address":[569268],"length":1,"stats":{"Line":1},"fn_name":null},{"line":490,"address":[569355,569413],"length":1,"stats":{"Line":5},"fn_name":null},{"line":491,"address":[569613,569442],"length":1,"stats":{"Line":3},"fn_name":null},{"line":493,"address":[569502],"length":1,"stats":{"Line":4},"fn_name":null},{"line":499,"address":[570127,569776],"length":1,"stats":{"Line":1},"fn_name":"serialize_to_pem"},{"line":500,"address":[569801,569918],"length":1,"stats":{"Line":1},"fn_name":null},{"line":502,"address":[570036,570202,570268],"length":1,"stats":{"Line":6},"fn_name":null},{"line":503,"address":[569886],"length":1,"stats":{"Line":1},"fn_name":null},{"line":504,"address":[570010],"length":1,"stats":{"Line":1},"fn_name":null},{"line":511,"address":[570368,570912,570881],"length":1,"stats":{"Line":1},"fn_name":"sha256sum"},{"line":512,"address":[570506,570392],"length":1,"stats":{"Line":1},"fn_name":null},{"line":514,"address":[570495],"length":1,"stats":{"Line":2},"fn_name":null},{"line":515,"address":[570606],"length":1,"stats":{"Line":3},"fn_name":null},{"line":517,"address":[570663],"length":1,"stats":{"Line":2},"fn_name":null},{"line":548,"address":[570928],"length":1,"stats":{"Line":1},"fn_name":"net_match"},{"line":549,"address":[570978,571042],"length":1,"stats":{"Line":2},"fn_name":null},{"line":550,"address":[571052],"length":1,"stats":{"Line":1},"fn_name":null},{"line":551,"address":[571069],"length":1,"stats":{"Line":1},"fn_name":null},{"line":554,"address":[571035],"length":1,"stats":{"Line":1},"fn_name":null}],"covered":174,"coverable":184},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","cert_codec.rs"],"content":"// Automatically generated rust module for 'cert_codec.proto' file\n\n#![allow(non_snake_case)]\n#![allow(non_upper_case_globals)]\n#![allow(non_camel_case_types)]\n#![allow(unused_imports)]\n#![allow(unknown_lints)]\n#![allow(clippy::all)]\n#![cfg_attr(rustfmt, rustfmt_skip)]\n#![allow(clippy::pedantic)]\n\nuse quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result};\nuse quick_protobuf::sizeofs::*;\nuse super::*;\n\n#[allow(clippy::derive_partial_eq_without_eq)]\n#[derive(Debug, Default, PartialEq, Clone)]\npub struct RawNebulaCertificate {\n    pub Details: Option\u003ccert_codec::RawNebulaCertificateDetails\u003e,\n    pub Signature: Vec\u003cu8\u003e,\n}\n\nimpl\u003c'a\u003e MessageRead\u003c'a\u003e for RawNebulaCertificate {\n    fn from_reader(r: \u0026mut BytesReader, bytes: \u0026'a [u8]) -\u003e Result\u003cSelf\u003e {\n        let mut msg = Self::default();\n        while !r.is_eof() {\n            match r.next_tag(bytes) {\n                Ok(10) =\u003e msg.Details = Some(r.read_message::\u003ccert_codec::RawNebulaCertificateDetails\u003e(bytes)?),\n                Ok(18) =\u003e msg.Signature = r.read_bytes(bytes)?.to_owned(),\n                Ok(t) =\u003e { r.read_unknown(bytes, t)?; }\n                Err(e) =\u003e return Err(e),\n            }\n        }\n        Ok(msg)\n    }\n}\n\nimpl MessageWrite for RawNebulaCertificate {\n    fn get_size(\u0026self) -\u003e usize {\n        0\n        + self.Details.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size()))\n        + if self.Signature.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.Signature).len()) }\n    }\n\n    fn write_message\u003cW: WriterBackend\u003e(\u0026self, w: \u0026mut Writer\u003cW\u003e) -\u003e Result\u003c()\u003e {\n        if let Some(ref s) = self.Details { w.write_with_tag(10, |w| w.write_message(s))?; }\n        if !self.Signature.is_empty() { w.write_with_tag(18, |w| w.write_bytes(\u0026**\u0026self.Signature))?; }\n        Ok(())\n    }\n}\n\n#[allow(clippy::derive_partial_eq_without_eq)]\n#[derive(Debug, Default, PartialEq, Clone)]\npub struct RawNebulaCertificateDetails {\n    pub Name: String,\n    pub Ips: Vec\u003cu32\u003e,\n    pub Subnets: Vec\u003cu32\u003e,\n    pub Groups: Vec\u003cString\u003e,\n    pub NotBefore: i64,\n    pub NotAfter: i64,\n    pub PublicKey: Vec\u003cu8\u003e,\n    pub IsCA: bool,\n    pub Issuer: Vec\u003cu8\u003e,\n}\n\nimpl\u003c'a\u003e MessageRead\u003c'a\u003e for RawNebulaCertificateDetails {\n    fn from_reader(r: \u0026mut BytesReader, bytes: \u0026'a [u8]) -\u003e Result\u003cSelf\u003e {\n        let mut msg = Self::default();\n        while !r.is_eof() {\n            match r.next_tag(bytes) {\n                Ok(10) =\u003e msg.Name = r.read_string(bytes)?.to_owned(),\n                Ok(18) =\u003e msg.Ips = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?,\n                Ok(26) =\u003e msg.Subnets = r.read_packed(bytes, |r, bytes| Ok(r.read_uint32(bytes)?))?,\n                Ok(34) =\u003e msg.Groups.push(r.read_string(bytes)?.to_owned()),\n                Ok(40) =\u003e msg.NotBefore = r.read_int64(bytes)?,\n                Ok(48) =\u003e msg.NotAfter = r.read_int64(bytes)?,\n                Ok(58) =\u003e msg.PublicKey = r.read_bytes(bytes)?.to_owned(),\n                Ok(64) =\u003e msg.IsCA = r.read_bool(bytes)?,\n                Ok(74) =\u003e msg.Issuer = r.read_bytes(bytes)?.to_owned(),\n                Ok(t) =\u003e { r.read_unknown(bytes, t)?; }\n                Err(e) =\u003e return Err(e),\n            }\n        }\n        Ok(msg)\n    }\n}\n\nimpl MessageWrite for RawNebulaCertificateDetails {\n    fn get_size(\u0026self) -\u003e usize {\n        0\n        + if self.Name == String::default() { 0 } else { 1 + sizeof_len((\u0026self.Name).len()) }\n        + if self.Ips.is_empty() { 0 } else { 1 + sizeof_len(self.Ips.iter().map(|s| sizeof_varint(*(s) as u64)).sum::\u003cusize\u003e()) }\n        + if self.Subnets.is_empty() { 0 } else { 1 + sizeof_len(self.Subnets.iter().map(|s| sizeof_varint(*(s) as u64)).sum::\u003cusize\u003e()) }\n        + self.Groups.iter().map(|s| 1 + sizeof_len((s).len())).sum::\u003cusize\u003e()\n        + if self.NotBefore == 0i64 { 0 } else { 1 + sizeof_varint(*(\u0026self.NotBefore) as u64) }\n        + if self.NotAfter == 0i64 { 0 } else { 1 + sizeof_varint(*(\u0026self.NotAfter) as u64) }\n        + if self.PublicKey.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.PublicKey).len()) }\n        + if self.IsCA == false { 0 } else { 1 + sizeof_varint(*(\u0026self.IsCA) as u64) }\n        + if self.Issuer.is_empty() { 0 } else { 1 + sizeof_len((\u0026self.Issuer).len()) }\n    }\n\n    fn write_message\u003cW: WriterBackend\u003e(\u0026self, w: \u0026mut Writer\u003cW\u003e) -\u003e Result\u003c()\u003e {\n        if self.Name != String::default() { w.write_with_tag(10, |w| w.write_string(\u0026**\u0026self.Name))?; }\n        w.write_packed_with_tag(18, \u0026self.Ips, |w, m| w.write_uint32(*m), \u0026|m| sizeof_varint(*(m) as u64))?;\n        w.write_packed_with_tag(26, \u0026self.Subnets, |w, m| w.write_uint32(*m), \u0026|m| sizeof_varint(*(m) as u64))?;\n        for s in \u0026self.Groups { w.write_with_tag(34, |w| w.write_string(\u0026**s))?; }\n        if self.NotBefore != 0i64 { w.write_with_tag(40, |w| w.write_int64(*\u0026self.NotBefore))?; }\n        if self.NotAfter != 0i64 { w.write_with_tag(48, |w| w.write_int64(*\u0026self.NotAfter))?; }\n        if !self.PublicKey.is_empty() { w.write_with_tag(58, |w| w.write_bytes(\u0026**\u0026self.PublicKey))?; }\n        if self.IsCA != false { w.write_with_tag(64, |w| w.write_bool(*\u0026self.IsCA))?; }\n        if !self.Issuer.is_empty() { w.write_with_tag(74, |w| w.write_bytes(\u0026**\u0026self.Issuer))?; }\n        Ok(())\n    }\n}\n\n","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","lib.rs"],"content":"//! # trifid-pki\n//! trifid-pki is a crate for interacting with the Nebula PKI system. It was created to prevent the need to make constant CLI calls for signing operations in Nebula.\n//! It is designed to be interoperable with the original Go implementation and as such has some oddities with key management to ensure compatability.\n//!\n//! This crate has not received any format security audits, however the underlying crates used for actual cryptographic operations (ed25519-dalek and curve25519-dalek) have been audited with no major issues.\n\n#![warn(clippy::pedantic)]\n#![warn(clippy::nursery)]\n#![deny(clippy::unwrap_used)]\n#![deny(clippy::expect_used)]\n#![deny(missing_docs)]\n#![deny(clippy::missing_errors_doc)]\n#![deny(clippy::missing_panics_doc)]\n#![deny(clippy::missing_safety_doc)]\n#![allow(clippy::must_use_candidate)]\n#![allow(clippy::too_many_lines)]\n#![allow(clippy::module_name_repetitions)]\n\n\nextern crate core;\n\npub mod ca;\npub mod cert;\n#[cfg(not(tarpaulin_include))]\npub(crate) mod cert_codec;\n#[cfg(test)]\n#[macro_use]\npub mod test;","traces":[],"covered":0,"coverable":0},{"path":["/","home","core","prj","e3t","trifid","trifid-pki","src","test.rs"],"content":"#![allow(clippy::unwrap_used)]\n#![allow(clippy::expect_used)]\n\nuse crate::netmask;\nuse std::net::Ipv4Addr;\nuse std::ops::{Add, Sub};\nuse std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};\nuse ipnet::Ipv4Net;\nuse crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public};\nuse std::str::FromStr;\nuse ed25519_dalek::{SigningKey, VerifyingKey};\nuse quick_protobuf::{MessageWrite, Writer};\nuse rand::rngs::OsRng;\nuse crate::ca::{NebulaCAPool};\nuse crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};\n\n/// This is a cert that we (e3team) actually use in production, and it's a known-good certificate.\npub const KNOWN_GOOD_CERT: \u0026[u8; 258] = b\"-----BEGIN NEBULA CERTIFICATE-----\\nCkkKF2UzdGVhbSBJbnRlcm5hbCBOZXR3b3JrKJWev5wGMJWFxKsGOiCvpwoHyKY5\\n8Q5+2XxDjtoCf/zlNY/EUdB8bwXQSwEo50ABEkB0Dx76lkMqc3IyH5+ml2dKjTyv\\nB4Jiw6x3abf5YZcf8rDuVEgQpvFdJmo3xJyIb3C9vKZ6kXsUxjw6s1JdWgkA\\n-----END NEBULA CERTIFICATE-----\";\n\n#[test]\nfn certificate_serialization() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let bytes = cert.serialize().unwrap();\n\n    let deserialized = deserialize_nebula_certificate(\u0026bytes).unwrap();\n\n    assert_eq!(cert.signature, deserialized.signature);\n    assert_eq!(cert.details.name, deserialized.details.name);\n    assert_eq!(cert.details.not_before, deserialized.details.not_before);\n    assert_eq!(cert.details.not_after, deserialized.details.not_after);\n    assert_eq!(cert.details.public_key, deserialized.details.public_key);\n    assert_eq!(cert.details.is_ca, deserialized.details.is_ca);\n\n    assert_eq!(cert.details.ips.len(), deserialized.details.ips.len());\n    for item in \u0026cert.details.ips {\n        assert!(deserialized.details.ips.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.subnets.len(), deserialized.details.subnets.len());\n    for item in \u0026cert.details.subnets {\n        assert!(deserialized.details.subnets.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.groups.len(), deserialized.details.groups.len());\n    for item in \u0026cert.details.groups {\n        assert!(deserialized.details.groups.contains(item), \"deserialized does not contain from source\");\n    }\n}\n\n#[test]\nfn certificate_serialization_pem() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let bytes = cert.serialize_to_pem().unwrap();\n\n    let deserialized = deserialize_nebula_certificate_from_pem(\u0026bytes).unwrap();\n\n    assert_eq!(cert.signature, deserialized.signature);\n    assert_eq!(cert.details.name, deserialized.details.name);\n    assert_eq!(cert.details.not_before, deserialized.details.not_before);\n    assert_eq!(cert.details.not_after, deserialized.details.not_after);\n    assert_eq!(cert.details.public_key, deserialized.details.public_key);\n    assert_eq!(cert.details.is_ca, deserialized.details.is_ca);\n\n    assert_eq!(cert.details.ips.len(), deserialized.details.ips.len());\n    for item in \u0026cert.details.ips {\n        assert!(deserialized.details.ips.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.subnets.len(), deserialized.details.subnets.len());\n    for item in \u0026cert.details.subnets {\n        assert!(deserialized.details.subnets.contains(item), \"deserialized does not contain from source\");\n    }\n\n    assert_eq!(cert.details.groups.len(), deserialized.details.groups.len());\n    for item in \u0026cert.details.groups {\n        assert!(deserialized.details.groups.contains(item), \"deserialized does not contain from source\");\n    }\n}\n\n#[test]\nfn cert_signing() {\n    let before = round_systime_to_secs(SystemTime::now() - Duration::from_secs(60)).unwrap();\n    let after = round_systime_to_secs(SystemTime::now() + Duration::from_secs(60)).unwrap();\n    let pub_key = b\"1234567890abcedfghij1234567890ab\";\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"testing\".to_string(),\n            ips: vec![\n                netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n                netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n                netmask!(\"10.1.1.3\", \"255.0.0.0\")\n            ],\n            subnets: vec![\n                netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n                netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n                netmask!(\"9.1.1.3\", \"255.255.0.0\")\n            ],\n            groups: vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()],\n            not_before: before,\n            not_after: after,\n            public_key: *pub_key,\n            is_ca: false,\n            issuer: \"1234567890abcedfabcd1234567890ab\".to_string(),\n        },\n        signature: b\"1234567890abcedfghij1234567890ab\".to_vec(),\n    };\n\n    let mut csprng = OsRng;\n    let key = SigningKey::generate(\u0026mut csprng);\n\n    assert!(cert.check_signature(\u0026key.verifying_key()).is_err());\n    cert.sign(\u0026key).unwrap();\n    assert!(cert.check_signature(\u0026key.verifying_key()).unwrap());\n}\n\n#[test]\nfn cert_expiry() {\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: String::new(),\n            ips: vec![],\n            subnets: vec![],\n            groups: vec![],\n            not_before: SystemTime::now().sub(Duration::from_secs(60)),\n            not_after: SystemTime::now().add(Duration::from_secs(60)),\n            public_key: [0u8; 32],\n            is_ca: false,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n\n    assert!(cert.expired(SystemTime::now().add(Duration::from_secs(60 * 60 * 60))));\n    assert!(cert.expired(SystemTime::now().sub(Duration::from_secs(60 * 60 * 60))));\n    assert!(!cert.expired(SystemTime::now()));\n}\n\n#[test]\nfn cert_display() {\n    let cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: String::new(),\n            ips: vec![],\n            subnets: vec![],\n            groups: vec![],\n            not_before: SystemTime::now().sub(Duration::from_secs(60)),\n            not_after: SystemTime::now().add(Duration::from_secs(60)),\n            public_key: [0u8; 32],\n            is_ca: false,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n    println!(\"{cert}\");\n}\n\n#[test]\n#[should_panic]\nfn cert_deserialize_empty_bytes() {\n    deserialize_nebula_certificate(\u0026[]).unwrap();\n}\n\n#[test]\nfn cert_deserialize_unpaired_ips() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![0],\n            Subnets: vec![],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n}\n\n#[test]\nfn cert_deserialize_unpaired_subnets() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![],\n            Subnets: vec![0],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n}\n\n#[test]\nfn cert_deserialize_wrong_pubkey_len() {\n    let broken_cert = RawNebulaCertificate {\n        Details: Some(RawNebulaCertificateDetails {\n            Name: String::new(),\n            Ips: vec![],\n            Subnets: vec![],\n            Groups: vec![],\n            NotBefore: 0,\n            NotAfter: 0,\n            PublicKey: vec![0u8; 31],\n            IsCA: false,\n            Issuer: vec![],\n        }),\n        Signature: vec![],\n    };\n    let mut bytes = vec![];\n    let mut writer = Writer::new(\u0026mut bytes);\n    broken_cert.write_message(\u0026mut writer).unwrap();\n\n    deserialize_nebula_certificate(\u0026bytes).unwrap_err();\n\n    assert!(deserialize_nebula_certificate_from_pem(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn x25519_serialization() {\n    let bytes = [0u8; 32];\n    assert_eq!(deserialize_x25519_private(\u0026serialize_x25519_private(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_x25519_private(\u0026[0u8; 32]).is_err());\n    assert_eq!(deserialize_x25519_public(\u0026serialize_x25519_public(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_x25519_public(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn ed25519_serialization() {\n    let bytes = [0u8; 64];\n    assert_eq!(deserialize_ed25519_private(\u0026serialize_ed25519_private(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_ed25519_private(\u0026[0u8; 32]).is_err());\n    assert_eq!(deserialize_ed25519_public(\u0026serialize_ed25519_public(\u0026bytes)).unwrap(), bytes);\n    assert!(deserialize_ed25519_public(\u0026[0u8; 32]).is_err());\n}\n\n#[test]\nfn cert_verify() {\n    let (ca_cert, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![], vec![], vec![\"groupa\".to_string()]);\n\n    let (cert, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, SystemTime::now(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![], vec![]);\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_cert.serialize_to_pem().unwrap()).unwrap();\n\n    let fingerprint = cert.sha256sum().unwrap();\n    ca_pool.blocklist_fingerprint(\u0026fingerprint);\n\n    assert!(matches!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Blocklisted));\n\n    ca_pool.reset_blocklist();\n\n    assert!(matches!(cert.verify(SystemTime::now() + Duration::from_secs(60 * 60 * 60), \u0026ca_pool).unwrap(), CertificateValidity::RootCertExpired));\n    assert!(matches!(cert.verify(SystemTime::now() + Duration::from_secs(60 * 60 * 6), \u0026ca_pool).unwrap(), CertificateValidity::CertExpired));\n\n    let (cert_with_bad_group, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), in_a_minute(), vec![], vec![], vec![\"group-not-present on parent\".to_string()]);\n    assert_eq!(cert_with_bad_group.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::GroupNotPresentOnSigner);\n\n    let (cert_with_good_group, _, _) = test_cert(\u0026ca_cert, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), in_a_minute(), vec![], vec![], vec![\"groupa\".to_string()]);\n    assert_eq!(cert_with_good_group.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n}\n\n#[test]\nfn cert_verify_ip() {\n    let ca_ip_1 = Ipv4Net::from_str(\"10.0.0.0/16\").unwrap();\n    let ca_ip_2 = Ipv4Net::from_str(\"192.168.0.0/24\").unwrap();\n\n    let (ca, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![ca_ip_1, ca_ip_2], vec![], vec![]);\n\n    let ca_pem = ca.serialize_to_pem().unwrap();\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_pem).unwrap();\n\n    // ip is outside the network\n    let cip1 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let cip2 = netmask!(\"192.198.0.1\", \"255.255.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is outside the network - reversed order from above\n    let cip1 = netmask!(\"192.198.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is within the network but mask is outside\n    let cip1 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip is within the network but mask is outside - reversed order from above\n    let cip1 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::IPNotPresentOnSigner);\n\n    // ip and mask are within the network\n    let cip1 = netmask!(\"10.0.1.0\", \"255.255.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.128\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_1, ca_ip_2], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_2, ca_ip_1], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed with just one\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![ca_ip_2], vec![], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n}\n\n#[test]\nfn cert_verify_subnet() {\n    let ca_ip_1 = Ipv4Net::from_str(\"10.0.0.0/16\").unwrap();\n    let ca_ip_2 = Ipv4Net::from_str(\"192.168.0.0/24\").unwrap();\n\n    let (ca, ca_key, _ca_pub) = test_ca_cert(round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 10)).unwrap(), vec![],vec![ca_ip_1, ca_ip_2], vec![]);\n\n    let ca_pem = ca.serialize_to_pem().unwrap();\n\n    let mut ca_pool = NebulaCAPool::new();\n    ca_pool.add_ca_certificate(\u0026ca_pem).unwrap();\n\n    // ip is outside the network\n    let cip1 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let cip2 = netmask!(\"192.198.0.1\", \"255.255.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is outside the network - reversed order from above\n    let cip1 = netmask!(\"192.198.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.1.0.0\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is within the network but mask is outside\n    let cip1 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip is within the network but mask is outside - reversed order from above\n    let cip1 = netmask!(\"192.168.0.1\", \"255.255.255.0\");\n    let cip2 = netmask!(\"10.0.1.0\", \"255.254.0.0\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![cip1, cip2], vec![], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::SubnetNotPresentOnSigner);\n\n    // ip and mask are within the network\n    let cip1 = netmask!(\"10.0.1.0\", \"255.255.0.0\");\n    let cip2 = netmask!(\"192.168.0.1\", \"255.255.255.128\");\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![cip1, cip2], vec![]);\n\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![],vec![ca_ip_1, ca_ip_2], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![ca_ip_2, ca_ip_1], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n\n    // Exact matches reversed with just one\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, round_systime_to_secs(SystemTime::now()).unwrap(), round_systime_to_secs(SystemTime::now() + Duration::from_secs(60 * 60 * 5)).unwrap(), vec![], vec![ca_ip_2], vec![]);\n    assert_eq!(cert.verify(SystemTime::now(), \u0026ca_pool).unwrap(), CertificateValidity::Ok);\n}\n\n#[test]\nfn cert_private_key() {\n    let (ca, ca_key, _) = test_ca_cert(SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    ca.verify_private_key(\u0026ca_key.to_keypair_bytes()).unwrap();\n\n    let (_, ca_key2, _) = test_ca_cert(SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    ca.verify_private_key(\u0026ca_key2.to_keypair_bytes()).unwrap_err();\n\n    let (cert, priv_key, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    cert.verify_private_key(\u0026priv_key.to_bytes()).unwrap();\n\n    let (cert2, _, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n    cert2.verify_private_key(\u0026priv_key.to_bytes()).unwrap_err();\n}\n\n#[test]\nfn capool_from_pem() {\n    let no_newlines = b\"# Current provisional, Remove once everything moves over to the real root.\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\n# root-ca01\n-----BEGIN NEBULA CERTIFICATE-----\nCkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG\nBVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf\n8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF\n-----END NEBULA CERTIFICATE-----\";\n    let with_newlines = b\"# Current provisional, Remove once everything moves over to the real root.\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\n# root-ca01\n-----BEGIN NEBULA CERTIFICATE-----\nCkMKEW5lYnVsYSByb290IGNhIDAxKJL2u9EFMJL86+cGOiDPXMH4oU6HZTk/CqTG\nBVG+oJpAoqokUBbI4U0N8CSfpUABEkB/Pm5A2xyH/nc8mg/wvGUWG3pZ7nHzaDMf\n8/phAUt+FLzqTECzQKisYswKvE3pl9mbEYKbOdIHrxdIp95mo4sF\n-----END NEBULA CERTIFICATE-----\n\n\";\n    let expired = b\"# expired certificate\n-----BEGIN NEBULA CERTIFICATE-----\nCjkKB2V4cGlyZWQouPmWjQYwufmWjQY6ILCRaoCkJlqHgv5jfDN4lzLHBvDzaQm4\nvZxfu144hmgjQAESQG4qlnZi8DncvD/LDZnLgJHOaX1DWCHHEh59epVsC+BNgTie\nWH1M9n4O7cFtGlM6sJJOS+rCVVEJ3ABS7+MPdQs=\n-----END NEBULA CERTIFICATE-----\";\n\n    let pool_a = NebulaCAPool::new_from_pem(no_newlines).unwrap();\n    assert_eq!(pool_a.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_a.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert!(!pool_a.expired);\n\n    let pool_b = NebulaCAPool::new_from_pem(with_newlines).unwrap();\n    assert_eq!(pool_b.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_b.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert!(!pool_b.expired);\n\n    let pool_c = NebulaCAPool::new_from_pem(expired).unwrap();\n    assert!(pool_c.expired);\n    assert_eq!(pool_c.cas[\"152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0\"].details.name, \"expired\");\n\n    let mut pool_d = NebulaCAPool::new_from_pem(with_newlines).unwrap();\n    pool_d.add_ca_certificate(expired).unwrap();\n    assert_eq!(pool_d.cas[\"c9bfaf7ce8e84b2eeda2e27b469f4b9617bde192efd214b68891ecda6ed49522\"].details.name, \"nebula root ca\".to_string());\n    assert_eq!(pool_d.cas[\"5c9c3f23e7ee7fe97637cbd3a0a5b854154d1d9aaaf7b566a51f4a88f76b64cd\"].details.name, \"nebula root ca 01\".to_string());\n    assert_eq!(pool_d.cas[\"152070be6bb19bc9e3bde4c2f0e7d8f4ff5448b4c9856b8eccb314fade0229b0\"].details.name, \"expired\");\n    assert!(pool_d.expired);\n    assert_eq!(pool_d.get_fingerprints().len(), 3);\n}\n\n#[macro_export]\nmacro_rules! netmask {\n    ($ip:expr,$mask:expr) =\u003e {\n        Ipv4Net::with_netmask(Ipv4Addr::from_str($ip).unwrap(), Ipv4Addr::from_str($mask).unwrap()).unwrap()\n    };\n}\n\nfn round_systime_to_secs(time: SystemTime) -\u003e Result\u003cSystemTime, SystemTimeError\u003e {\n    let secs = time.duration_since(UNIX_EPOCH)?.as_secs();\n    Ok(SystemTime::UNIX_EPOCH.add(Duration::from_secs(secs)))\n}\n\nfn test_ca_cert(before: SystemTime, after: SystemTime, ips: Vec\u003cIpv4Net\u003e, subnets: Vec\u003cIpv4Net\u003e, groups: Vec\u003cString\u003e) -\u003e (NebulaCertificate, SigningKey, VerifyingKey) {\n    let mut csprng = OsRng;\n    let key = SigningKey::generate(\u0026mut csprng);\n    let pub_key = key.verifying_key();\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"TEST_CA\".to_string(),\n            ips,\n            subnets,\n            groups,\n            not_before: before,\n            not_after: after,\n            public_key: pub_key.to_bytes(),\n            is_ca: true,\n            issuer: String::new(),\n        },\n        signature: vec![],\n    };\n    cert.sign(\u0026key).unwrap();\n    (cert, key, pub_key)\n}\n\n#[test]\nfn test_deserialize_ed25519_private() {\n    let priv_key = b\"-----BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA ED25519 PRIVATE KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END NEBULA ED25519 PRIVATE KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NOT A NEBULA PRIVATE KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA ED25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-END NEBULA ED25519 PRIVATE KEY-----\";\n\n    deserialize_ed25519_private(priv_key).unwrap();\n\n    deserialize_ed25519_private(short_key).unwrap_err();\n\n    deserialize_ed25519_private(invalid_banner).unwrap_err();\n\n    deserialize_ed25519_private(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_deserialize_x25519_private() {\n    let priv_key = b\"-----BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NEBULA X25519 PRIVATE KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA X25519 PRIVATE KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NOT A NEBULA PRIVATE KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA X25519 PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-END NEBULA X25519 PRIVATE KEY-----\";\n\n    deserialize_x25519_private(priv_key).unwrap();\n\n    deserialize_x25519_private(short_key).unwrap_err();\n\n    deserialize_x25519_private(invalid_banner).unwrap_err();\n\n    deserialize_x25519_private(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_pem_deserialization() {\n    let good_cert = b\"# A good cert\n-----BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NEBULA CERTIFICATE-----\";\n    let bad_banner = b\"-----BEGIN NOT A NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-----END NOT A NEBULA CERTIFICATE-----\";\n    let invalid_pem = b\"# Not a valid PEM format\n-BEGIN NEBULA CERTIFICATE-----\nCkAKDm5lYnVsYSByb290IGNhKJfap9AFMJfg1+YGOiCUQGByMuNRhIlQBOyzXWbL\nvcKBwDhov900phEfJ5DN3kABEkDCq5R8qBiu8sl54yVfgRcQXEDt3cHr8UTSLszv\nbzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB\n-END NEBULA CERTIFICATE----\";\n\n    // success\n    deserialize_nebula_certificate_from_pem(good_cert).unwrap();\n\n    // fail because invalid banner\n    deserialize_nebula_certificate_from_pem(bad_banner).unwrap_err();\n\n    // fail because nonsense pem\n    deserialize_nebula_certificate_from_pem(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn test_deserialize_ed25519_public() {\n    let priv_key = b\"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA ED25519 PUBLIC KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END NEBULA ED25519 PUBLIC KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NOT A NEBULA PUBLIC KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA ED25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-END NEBULA ED25519 PUBLIC KEY-----\";\n\n    deserialize_ed25519_public(priv_key).unwrap();\n\n    deserialize_ed25519_public(short_key).unwrap_err();\n\n    deserialize_ed25519_public(invalid_banner).unwrap_err();\n\n    deserialize_ed25519_public(invalid_pem).unwrap_err();\n}\n\n#[test]\n// Ensure that trifid-pki produces the *exact same* bit-for-bit representation as nebula does\n// to ensure signature interoperability\n// We use an e3team production certificate for this as it is a known-good certificate.\nfn test_serialization_interoperability() {\n    let known_good_cert = hex::decode(\"0a650a08636f72652d74777212098184c4508080f8ff0f28ae9fbf9c06309485c4ab063a20304e6279d8722f0fd8d966faa70adaeec08c4649d486457bb038be243753a7474a2056860ded3d14b7f3b77ca2a062ee5d683dd06b35f87446d8d6a7e923b6a7783d1240eb5d6b688eda0d36e925219c098ff2799e42207a093f7d9b7d875823ca05b2e0d3a749dd2fc9cec811ed9a2865d71c4d53cfdfd1bf7e6d5058bd9ecd388ddf0d\").unwrap();\n\n    let cert = deserialize_nebula_certificate(\u0026known_good_cert).unwrap();\n    let reserialized_cert_pem = cert.serialize().unwrap();\n    assert_eq!(known_good_cert, reserialized_cert_pem);\n}\n\n#[test]\nfn test_deserialize_x25519_public() {\n    let priv_key = b\"-----BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NEBULA X25519 PUBLIC KEY-----\";\n    let short_key = b\"-----BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\n-----END NEBULA X25519 PUBLIC KEY-----\";\n    let invalid_banner = b\"-----BEGIN NOT A NEBULA PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-----END NOT A NEBULA PUBLIC KEY-----\";\n    let invalid_pem = b\"-BEGIN NEBULA X25519 PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n-END NEBULA X25519 PUBLIC KEY-----\";\n\n    deserialize_x25519_public(priv_key).unwrap();\n\n    deserialize_x25519_public(short_key).unwrap_err();\n\n    deserialize_x25519_public(invalid_banner).unwrap_err();\n\n    deserialize_x25519_public(invalid_pem).unwrap_err();\n}\n\n#[test]\nfn ca_pool_add_non_ca() {\n    let mut ca_pool = NebulaCAPool::new();\n\n    let (ca, ca_key, _) = test_ca_cert(SystemTime::now(), SystemTime::now() + Duration::from_secs(3600), vec![], vec![], vec![]);\n    let (cert, _, _) = test_cert(\u0026ca, \u0026ca_key, SystemTime::now(), SystemTime::now(), vec![], vec![], vec![]);\n\n    ca_pool.add_ca_certificate(\u0026cert.serialize_to_pem().unwrap()).unwrap_err();\n}\n\nfn test_cert(ca: \u0026NebulaCertificate, key: \u0026SigningKey, before: SystemTime, after: SystemTime, ips: Vec\u003cIpv4Net\u003e, subnets: Vec\u003cIpv4Net\u003e, groups: Vec\u003cString\u003e) -\u003e (NebulaCertificate, SigningKey, VerifyingKey) {\n    let issuer = ca.sha256sum().unwrap();\n\n    let real_groups = if groups.is_empty() {\n        vec![\"test-group1\".to_string(), \"test-group2\".to_string(), \"test-group3\".to_string()]\n    } else {\n        groups\n    };\n\n    let real_ips = if ips.is_empty() {\n        vec![\n            netmask!(\"10.1.1.1\", \"255.255.255.0\"),\n            netmask!(\"10.1.1.2\", \"255.255.0.0\"),\n            netmask!(\"10.1.1.3\", \"255.0.0.0\")\n        ]\n    } else {\n        ips\n    };\n\n    let real_subnets = if subnets.is_empty() {\n        vec![\n            netmask!(\"9.1.1.1\", \"255.255.255.128\"),\n            netmask!(\"9.1.1.2\", \"255.255.255.0\"),\n            netmask!(\"9.1.1.3\", \"255.255.0.0\")\n        ]\n    } else {\n        subnets\n    };\n\n    let mut csprng = OsRng;\n    let cert_key = SigningKey::generate(\u0026mut csprng);\n    let pub_key = cert_key.verifying_key();\n\n    let mut cert = NebulaCertificate {\n        details: NebulaCertificateDetails {\n            name: \"TEST_CA\".to_string(),\n            ips: real_ips,\n            subnets: real_subnets,\n            groups: real_groups,\n            not_before: before,\n            not_after: after,\n            public_key: pub_key.to_bytes(),\n            is_ca: false,\n            issuer,\n        },\n        signature: vec![],\n    };\n    cert.sign(key).unwrap();\n    (cert, cert_key, pub_key)\n}\n\n#[allow(dead_code)]\nfn in_a_minute() -\u003e SystemTime {\n    round_systime_to_secs(SystemTime::now().add(Duration::from_secs(60))).unwrap()\n}\n#[allow(dead_code)]\nfn a_minute_ago() -\u003e SystemTime {\n    round_systime_to_secs(SystemTime::now().sub(Duration::from_secs(60))).unwrap()\n}","traces":[],"covered":0,"coverable":0}]};
    </script>
    <script crossorigin>/** @license React v16.13.1
 * react.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
'use strict';(function(d,r){"object"===typeof exports&&"undefined"!==typeof module?r(exports):"function"===typeof define&&define.amd?define(["exports"],r):(d=d||self,r(d.React={}))})(this,function(d){function r(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function w(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function da(){}function L(a,b,c){this.props=a;this.context=b;this.refs=ba;this.updater=c||ca}function ea(a,b,c){var g,e={},fa=null,d=null;if(null!=b)for(g in void 0!==b.ref&&(d=b.ref),void 0!==b.key&&(fa=""+b.key),b)ha.call(b,g)&&!ia.hasOwnProperty(g)&&(e[g]=b[g]);var h=arguments.length-2;if(1===h)e.children=c;else if(1<h){for(var k=Array(h),f=0;f<h;f++)k[f]=arguments[f+2];e.children=k}if(a&&a.defaultProps)for(g in h=a.defaultProps,
h)void 0===e[g]&&(e[g]=h[g]);return{$$typeof:x,type:a,key:fa,ref:d,props:e,_owner:M.current}}function va(a,b){return{$$typeof:x,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function N(a){return"object"===typeof a&&null!==a&&a.$$typeof===x}function wa(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ja(a,b,c,g){if(C.length){var e=C.pop();e.result=a;e.keyPrefix=b;e.func=c;e.context=g;e.count=0;return e}return{result:a,keyPrefix:b,func:c,
context:g,count:0}}function ka(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>C.length&&C.push(a)}function O(a,b,c,g){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var d=!1;if(null===a)d=!0;else switch(e){case "string":case "number":d=!0;break;case "object":switch(a.$$typeof){case x:case xa:d=!0}}if(d)return c(g,a,""===b?"."+P(a,0):b),1;d=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){e=a[f];var h=b+P(e,f);d+=O(e,h,c,g)}else if(null===a||
"object"!==typeof a?h=null:(h=la&&a[la]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(e=a.next()).done;)e=e.value,h=b+P(e,f++),d+=O(e,h,c,g);else if("object"===e)throw c=""+a,Error(r(31,"[object Object]"===c?"object with keys {"+Object.keys(a).join(", ")+"}":c,""));return d}function Q(a,b,c){return null==a?0:O(a,"",b,c)}function P(a,b){return"object"===typeof a&&null!==a&&null!=a.key?wa(a.key):b.toString(36)}function ya(a,b,c){a.func.call(a.context,b,
a.count++)}function za(a,b,c){var g=a.result,e=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?R(a,g,c,function(a){return a}):null!=a&&(N(a)&&(a=va(a,e+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(ma,"$&/")+"/")+c)),g.push(a))}function R(a,b,c,g,e){var d="";null!=c&&(d=(""+c).replace(ma,"$&/")+"/");b=ja(b,d,g,e);Q(a,za,b);ka(b)}function t(){var a=na.current;if(null===a)throw Error(r(321));return a}function S(a,b){var c=a.length;a.push(b);a:for(;;){var g=c-1>>>1,e=a[g];if(void 0!==
e&&0<D(e,b))a[g]=b,a[c]=e,c=g;else break a}}function n(a){a=a[0];return void 0===a?null:a}function E(a){var b=a[0];if(void 0!==b){var c=a.pop();if(c!==b){a[0]=c;a:for(var g=0,e=a.length;g<e;){var d=2*(g+1)-1,f=a[d],h=d+1,k=a[h];if(void 0!==f&&0>D(f,c))void 0!==k&&0>D(k,f)?(a[g]=k,a[h]=c,g=h):(a[g]=f,a[d]=c,g=d);else if(void 0!==k&&0>D(k,c))a[g]=k,a[h]=c,g=h;else break a}}return b}return null}function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function F(a){for(var b=n(u);null!==
b;){if(null===b.callback)E(u);else if(b.startTime<=a)E(u),b.sortIndex=b.expirationTime,S(p,b);else break;b=n(u)}}function T(a){y=!1;F(a);if(!v)if(null!==n(p))v=!0,z(U);else{var b=n(u);null!==b&&G(T,b.startTime-a)}}function U(a,b){v=!1;y&&(y=!1,V());H=!0;var c=m;try{F(b);for(l=n(p);null!==l&&(!(l.expirationTime>b)||a&&!W());){var g=l.callback;if(null!==g){l.callback=null;m=l.priorityLevel;var e=g(l.expirationTime<=b);b=q();"function"===typeof e?l.callback=e:l===n(p)&&E(p);F(b)}else E(p);l=n(p)}if(null!==
l)var d=!0;else{var f=n(u);null!==f&&G(T,f.startTime-b);d=!1}return d}finally{l=null,m=c,H=!1}}function oa(a){switch(a){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var f="function"===typeof Symbol&&Symbol.for,x=f?Symbol.for("react.element"):60103,xa=f?Symbol.for("react.portal"):60106,Aa=f?Symbol.for("react.fragment"):60107,Ba=f?Symbol.for("react.strict_mode"):60108,Ca=f?Symbol.for("react.profiler"):60114,Da=f?Symbol.for("react.provider"):60109,
Ea=f?Symbol.for("react.context"):60110,Fa=f?Symbol.for("react.forward_ref"):60112,Ga=f?Symbol.for("react.suspense"):60113,Ha=f?Symbol.for("react.memo"):60115,Ia=f?Symbol.for("react.lazy"):60116,la="function"===typeof Symbol&&Symbol.iterator,pa=Object.getOwnPropertySymbols,Ja=Object.prototype.hasOwnProperty,Ka=Object.prototype.propertyIsEnumerable,I=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+
String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,e=1;e<arguments.length;e++){var d=Object(arguments[e]);
for(var f in d)Ja.call(d,f)&&(c[f]=d[f]);if(pa){g=pa(d);for(var h=0;h<g.length;h++)Ka.call(d,g[h])&&(c[g[h]]=d[g[h]])}}return c},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,c){},enqueueReplaceState:function(a,b,c,d){},enqueueSetState:function(a,b,c,d){}},ba={};w.prototype.isReactComponent={};w.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error(r(85));this.updater.enqueueSetState(this,a,b,"setState")};w.prototype.forceUpdate=
function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};da.prototype=w.prototype;f=L.prototype=new da;f.constructor=L;I(f,w.prototype);f.isPureReactComponent=!0;var M={current:null},ha=Object.prototype.hasOwnProperty,ia={key:!0,ref:!0,__self:!0,__source:!0},ma=/\/+/g,C=[],na={current:null},X;if("undefined"===typeof window||"function"!==typeof MessageChannel){var A=null,qa=null,ra=function(){if(null!==A)try{var a=q();A(!0,a);A=null}catch(b){throw setTimeout(ra,0),b;}},La=Date.now();var q=
function(){return Date.now()-La};var z=function(a){null!==A?setTimeout(z,0,a):(A=a,setTimeout(ra,0))};var G=function(a,b){qa=setTimeout(a,b)};var V=function(){clearTimeout(qa)};var W=function(){return!1};f=X=function(){}}else{var Y=window.performance,sa=window.Date,Ma=window.setTimeout,Na=window.clearTimeout;"undefined"!==typeof console&&(f=window.cancelAnimationFrame,"function"!==typeof window.requestAnimationFrame&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),
"function"!==typeof f&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));if("object"===typeof Y&&"function"===typeof Y.now)q=function(){return Y.now()};else{var Oa=sa.now();q=function(){return sa.now()-Oa}}var J=!1,K=null,Z=-1,ta=5,ua=0;W=function(){return q()>=ua};f=function(){};X=function(a){0>a||125<a?console.error("forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported"):
ta=0<a?Math.floor(1E3/a):5};var B=new MessageChannel,aa=B.port2;B.port1.onmessage=function(){if(null!==K){var a=q();ua=a+ta;try{K(!0,a)?aa.postMessage(null):(J=!1,K=null)}catch(b){throw aa.postMessage(null),b;}}else J=!1};z=function(a){K=a;J||(J=!0,aa.postMessage(null))};G=function(a,b){Z=Ma(function(){a(q())},b)};V=function(){Na(Z);Z=-1}}var p=[],u=[],Pa=1,l=null,m=3,H=!1,v=!1,y=!1,Qa=0;B={ReactCurrentDispatcher:na,ReactCurrentOwner:M,IsSomeRendererActing:{current:!1},assign:I};I(B,{Scheduler:{__proto__:null,
unstable_ImmediatePriority:1,unstable_UserBlockingPriority:2,unstable_NormalPriority:3,unstable_IdlePriority:5,unstable_LowPriority:4,unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3}var c=m;m=a;try{return b()}finally{m=c}},unstable_next:function(a){switch(m){case 1:case 2:case 3:var b=3;break;default:b=m}var c=m;m=b;try{return a()}finally{m=c}},unstable_scheduleCallback:function(a,b,c){var d=q();if("object"===typeof c&&null!==c){var e=c.delay;
e="number"===typeof e&&0<e?d+e:d;c="number"===typeof c.timeout?c.timeout:oa(a)}else c=oa(a),e=d;c=e+c;a={id:Pa++,callback:b,priorityLevel:a,startTime:e,expirationTime:c,sortIndex:-1};e>d?(a.sortIndex=e,S(u,a),null===n(p)&&a===n(u)&&(y?V():y=!0,G(T,e-d))):(a.sortIndex=c,S(p,a),v||H||(v=!0,z(U)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=m;return function(){var c=m;m=b;try{return a.apply(this,arguments)}finally{m=c}}},unstable_getCurrentPriorityLevel:function(){return m},
unstable_shouldYield:function(){var a=q();F(a);var b=n(p);return b!==l&&null!==l&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTime<l.expirationTime||W()},unstable_requestPaint:f,unstable_continueExecution:function(){v||H||(v=!0,z(U))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return n(p)},get unstable_now(){return q},get unstable_forceFrameRate(){return X},unstable_Profiling:null},SchedulerTracing:{__proto__:null,__interactionsRef:null,__subscriberRef:null,
unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Qa},unstable_trace:function(a,b,c){return c()},unstable_wrap:function(a){return a},unstable_subscribe:function(a){},unstable_unsubscribe:function(a){}}});d.Children={map:function(a,b,c){if(null==a)return a;var d=[];R(a,d,null,b,c);return d},forEach:function(a,b,c){if(null==a)return a;b=ja(null,null,b,c);Q(a,ya,b);ka(b)},count:function(a){return Q(a,function(){return null},null)},
toArray:function(a){var b=[];R(a,b,null,function(a){return a});return b},only:function(a){if(!N(a))throw Error(r(143));return a}};d.Component=w;d.Fragment=Aa;d.Profiler=Ca;d.PureComponent=L;d.StrictMode=Ba;d.Suspense=Ga;d.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=B;d.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error(r(267,a));var d=I({},a.props),e=a.key,f=a.ref,m=a._owner;if(null!=b){void 0!==b.ref&&(f=b.ref,m=M.current);void 0!==b.key&&(e=""+b.key);if(a.type&&a.type.defaultProps)var h=
a.type.defaultProps;for(k in b)ha.call(b,k)&&!ia.hasOwnProperty(k)&&(d[k]=void 0===b[k]&&void 0!==h?h[k]:b[k])}var k=arguments.length-2;if(1===k)d.children=c;else if(1<k){h=Array(k);for(var l=0;l<k;l++)h[l]=arguments[l+2];d.children=h}return{$$typeof:x,type:a.type,key:e,ref:f,props:d,_owner:m}};d.createContext=function(a,b){void 0===b&&(b=null);a={$$typeof:Ea,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Da,_context:a};return a.Consumer=
a};d.createElement=ea;d.createFactory=function(a){var b=ea.bind(null,a);b.type=a;return b};d.createRef=function(){return{current:null}};d.forwardRef=function(a){return{$$typeof:Fa,render:a}};d.isValidElement=N;d.lazy=function(a){return{$$typeof:Ia,_ctor:a,_status:-1,_result:null}};d.memo=function(a,b){return{$$typeof:Ha,type:a,compare:void 0===b?null:b}};d.useCallback=function(a,b){return t().useCallback(a,b)};d.useContext=function(a,b){return t().useContext(a,b)};d.useDebugValue=function(a,b){};
d.useEffect=function(a,b){return t().useEffect(a,b)};d.useImperativeHandle=function(a,b,c){return t().useImperativeHandle(a,b,c)};d.useLayoutEffect=function(a,b){return t().useLayoutEffect(a,b)};d.useMemo=function(a,b){return t().useMemo(a,b)};d.useReducer=function(a,b,c){return t().useReducer(a,b,c)};d.useRef=function(a){return t().useRef(a)};d.useState=function(a){return t().useState(a)};d.version="16.13.1"});
</script>
    <script crossorigin>/** @license React v16.13.1
 * react-dom.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
/*
 Modernizr 3.0.0pre (Custom Build) | MIT
*/
'use strict';(function(I,ea){"object"===typeof exports&&"undefined"!==typeof module?ea(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},I.React))})(this,function(I,ea){function k(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c<arguments.length;c++)b+="&args[]="+encodeURIComponent(arguments[c]);return"Minified React error #"+a+"; visit "+b+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}
function ji(a,b,c,d,e,f,g,h,m){yb=!1;gc=null;ki.apply(li,arguments)}function mi(a,b,c,d,e,f,g,h,m){ji.apply(this,arguments);if(yb){if(yb){var n=gc;yb=!1;gc=null}else throw Error(k(198));hc||(hc=!0,pd=n)}}function lf(a,b,c){var d=a.type||"unknown-event";a.currentTarget=mf(c);mi(d,b,void 0,a);a.currentTarget=null}function nf(){if(ic)for(var a in cb){var b=cb[a],c=ic.indexOf(a);if(!(-1<c))throw Error(k(96,a));if(!jc[c]){if(!b.extractEvents)throw Error(k(97,a));jc[c]=b;c=b.eventTypes;for(var d in c){var e=
void 0;var f=c[d],g=b,h=d;if(qd.hasOwnProperty(h))throw Error(k(99,h));qd[h]=f;var m=f.phasedRegistrationNames;if(m){for(e in m)m.hasOwnProperty(e)&&of(m[e],g,h);e=!0}else f.registrationName?(of(f.registrationName,g,h),e=!0):e=!1;if(!e)throw Error(k(98,d,a));}}}}function of(a,b,c){if(db[a])throw Error(k(100,a));db[a]=b;rd[a]=b.eventTypes[c].dependencies}function pf(a){var b=!1,c;for(c in a)if(a.hasOwnProperty(c)){var d=a[c];if(!cb.hasOwnProperty(c)||cb[c]!==d){if(cb[c])throw Error(k(102,c));cb[c]=
d;b=!0}}b&&nf()}function qf(a){if(a=rf(a)){if("function"!==typeof sd)throw Error(k(280));var b=a.stateNode;b&&(b=td(b),sd(a.stateNode,a.type,b))}}function sf(a){eb?fb?fb.push(a):fb=[a]:eb=a}function tf(){if(eb){var a=eb,b=fb;fb=eb=null;qf(a);if(b)for(a=0;a<b.length;a++)qf(b[a])}}function ud(){if(null!==eb||null!==fb)vd(),tf()}function uf(a,b,c){if(wd)return a(b,c);wd=!0;try{return vf(a,b,c)}finally{wd=!1,ud()}}function ni(a){if(wf.call(xf,a))return!0;if(wf.call(yf,a))return!1;if(oi.test(a))return xf[a]=
!0;yf[a]=!0;return!1}function pi(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function qi(a,b,c,d){if(null===b||"undefined"===typeof b||pi(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function L(a,
b,c,d,e,f){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f}function xd(a,b,c,d){var e=E.hasOwnProperty(b)?E[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(qi(b,c,e,d)&&(c=null),d||null===e?ni(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,
d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function zb(a){if(null===a||"object"!==typeof a)return null;a=zf&&a[zf]||a["@@iterator"];return"function"===typeof a?a:null}function ri(a){if(-1===a._status){a._status=0;var b=a._ctor;b=b();a._result=b;b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)})}}function na(a){if(null==a)return null;
if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case Ma:return"Fragment";case gb:return"Portal";case kc:return"Profiler";case Af:return"StrictMode";case lc:return"Suspense";case yd:return"SuspenseList"}if("object"===typeof a)switch(a.$$typeof){case Bf:return"Context.Consumer";case Cf:return"Context.Provider";case zd:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case Ad:return na(a.type);
case Df:return na(a.render);case Ef:if(a=1===a._status?a._result:null)return na(a)}return null}function Bd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=a._debugSource,f=na(a.type);c=null;d&&(c=na(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(si,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n    in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function va(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
default:return""}}function Ff(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function ti(a){var b=Ff(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,
b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function mc(a){a._valueTracker||(a._valueTracker=ti(a))}function Gf(a){if(!a)return!1;var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=Ff(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function Cd(a,b){var c=b.checked;return M({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=
c?c:a._wrapperState.initialChecked})}function Hf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=va(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function If(a,b){b=b.checked;null!=b&&xd(a,"checked",b,!1)}function Dd(a,b){If(a,b);var c=va(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==
""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?Ed(a,b.type,c):b.hasOwnProperty("defaultValue")&&Ed(a,b.type,va(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function Jf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=
b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function Ed(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function ui(a){var b="";ea.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Fd(a,b){a=M({children:void 0},b);if(b=ui(b.children))a.children=b;return a}function hb(a,b,c,d){a=a.options;if(b){b={};
for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+va(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Gd(a,b){if(null!=b.dangerouslySetInnerHTML)throw Error(k(91));return M({},b,{value:void 0,defaultValue:void 0,children:""+a._wrapperState.initialValue})}
function Kf(a,b){var c=b.value;if(null==c){c=b.children;b=b.defaultValue;if(null!=c){if(null!=b)throw Error(k(92));if(Array.isArray(c)){if(!(1>=c.length))throw Error(k(93));c=c[0]}b=c}null==b&&(b="");c=b}a._wrapperState={initialValue:va(c)}}function Lf(a,b){var c=va(b.value),d=va(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Mf(a,b){b=a.textContent;b===a._wrapperState.initialValue&&""!==
b&&null!==b&&(a.value=b)}function Nf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Hd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Nf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function nc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function oc(a){if(Id[a])return Id[a];
if(!ib[a])return a;var b=ib[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Of)return Id[a]=b[c];return a}function Jd(a){var b=Pf.get(a);void 0===b&&(b=new Map,Pf.set(a,b));return b}function Na(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else{a=b;do b=a,0!==(b.effectTag&1026)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function Qf(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function Rf(a){if(Na(a)!==
a)throw Error(k(188));}function vi(a){var b=a.alternate;if(!b){b=Na(a);if(null===b)throw Error(k(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return Rf(e),a;if(f===d)return Rf(e),b;f=f.sibling}throw Error(k(188));}if(c.return!==d.return)c=e,d=f;else{for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=
f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}if(!g)throw Error(k(189));}}if(c.alternate!==d)throw Error(k(190));}if(3!==c.tag)throw Error(k(188));return c.stateNode.current===c?a:b}function Sf(a){a=vi(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function jb(a,b){if(null==
b)throw Error(k(30));if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function Kd(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function pc(a){null!==a&&(Ab=jb(Ab,a));a=Ab;Ab=null;if(a){Kd(a,wi);if(Ab)throw Error(k(95));if(hc)throw a=pd,hc=!1,pd=null,a;}}function Ld(a){a=a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:
a}function Tf(a){if(!wa)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function Uf(a){a.topLevelType=null;a.nativeEvent=null;a.targetInst=null;a.ancestors.length=0;10>qc.length&&qc.push(a)}function Vf(a,b,c,d){if(qc.length){var e=qc.pop();e.topLevelType=a;e.eventSystemFlags=d;e.nativeEvent=b;e.targetInst=c;return e}return{topLevelType:a,eventSystemFlags:d,nativeEvent:b,targetInst:c,ancestors:[]}}function Wf(a){var b=
a.targetInst,c=b;do{if(!c){a.ancestors.push(c);break}var d=c;if(3===d.tag)d=d.stateNode.containerInfo;else{for(;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo}if(!d)break;b=c.tag;5!==b&&6!==b||a.ancestors.push(c);c=Bb(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=Ld(a.nativeEvent);d=a.topLevelType;var f=a.nativeEvent,g=a.eventSystemFlags;0===c&&(g|=64);for(var h=null,m=0;m<jc.length;m++){var n=jc[m];n&&(n=n.extractEvents(d,b,f,e,g))&&(h=jb(h,n))}pc(h)}}function Md(a,
b,c){if(!c.has(a)){switch(a){case "scroll":Cb(b,"scroll",!0);break;case "focus":case "blur":Cb(b,"focus",!0);Cb(b,"blur",!0);c.set("blur",null);c.set("focus",null);break;case "cancel":case "close":Tf(a)&&Cb(b,a,!0);break;case "invalid":case "submit":case "reset":break;default:-1===Db.indexOf(a)&&w(a,b)}c.set(a,null)}}function xi(a,b){var c=Jd(b);Nd.forEach(function(a){Md(a,b,c)});yi.forEach(function(a){Md(a,b,c)})}function Od(a,b,c,d,e){return{blockedOn:a,topLevelType:b,eventSystemFlags:c|32,nativeEvent:e,
container:d}}function Xf(a,b){switch(a){case "focus":case "blur":xa=null;break;case "dragenter":case "dragleave":ya=null;break;case "mouseover":case "mouseout":za=null;break;case "pointerover":case "pointerout":Eb.delete(b.pointerId);break;case "gotpointercapture":case "lostpointercapture":Fb.delete(b.pointerId)}}function Gb(a,b,c,d,e,f){if(null===a||a.nativeEvent!==f)return a=Od(b,c,d,e,f),null!==b&&(b=Hb(b),null!==b&&Yf(b)),a;a.eventSystemFlags|=d;return a}function zi(a,b,c,d,e){switch(b){case "focus":return xa=
Gb(xa,a,b,c,d,e),!0;case "dragenter":return ya=Gb(ya,a,b,c,d,e),!0;case "mouseover":return za=Gb(za,a,b,c,d,e),!0;case "pointerover":var f=e.pointerId;Eb.set(f,Gb(Eb.get(f)||null,a,b,c,d,e));return!0;case "gotpointercapture":return f=e.pointerId,Fb.set(f,Gb(Fb.get(f)||null,a,b,c,d,e)),!0}return!1}function Ai(a){var b=Bb(a.target);if(null!==b){var c=Na(b);if(null!==c)if(b=c.tag,13===b){if(b=Qf(c),null!==b){a.blockedOn=b;Pd(a.priority,function(){Bi(c)});return}}else if(3===b&&c.stateNode.hydrate){a.blockedOn=
3===c.tag?c.stateNode.containerInfo:null;return}}a.blockedOn=null}function rc(a){if(null!==a.blockedOn)return!1;var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);if(null!==b){var c=Hb(b);null!==c&&Yf(c);a.blockedOn=b;return!1}return!0}function Zf(a,b,c){rc(a)&&c.delete(b)}function Ci(){for(Rd=!1;0<fa.length;){var a=fa[0];if(null!==a.blockedOn){a=Hb(a.blockedOn);null!==a&&Di(a);break}var b=Qd(a.topLevelType,a.eventSystemFlags,a.container,a.nativeEvent);null!==b?a.blockedOn=b:fa.shift()}null!==
xa&&rc(xa)&&(xa=null);null!==ya&&rc(ya)&&(ya=null);null!==za&&rc(za)&&(za=null);Eb.forEach(Zf);Fb.forEach(Zf)}function Ib(a,b){a.blockedOn===b&&(a.blockedOn=null,Rd||(Rd=!0,$f(ag,Ci)))}function bg(a){if(0<fa.length){Ib(fa[0],a);for(var b=1;b<fa.length;b++){var c=fa[b];c.blockedOn===a&&(c.blockedOn=null)}}null!==xa&&Ib(xa,a);null!==ya&&Ib(ya,a);null!==za&&Ib(za,a);b=function(b){return Ib(b,a)};Eb.forEach(b);Fb.forEach(b);for(b=0;b<Jb.length;b++)c=Jb[b],c.blockedOn===a&&(c.blockedOn=null);for(;0<Jb.length&&
(b=Jb[0],null===b.blockedOn);)Ai(b),null===b.blockedOn&&Jb.shift()}function Sd(a,b){for(var c=0;c<a.length;c+=2){var d=a[c],e=a[c+1],f="on"+(e[0].toUpperCase()+e.slice(1));f={phasedRegistrationNames:{bubbled:f,captured:f+"Capture"},dependencies:[d],eventPriority:b};Td.set(d,b);cg.set(d,f);dg[e]=f}}function w(a,b){Cb(b,a,!1)}function Cb(a,b,c){var d=Td.get(b);switch(void 0===d?2:d){case 0:d=Ei.bind(null,b,1,a);break;case 1:d=Fi.bind(null,b,1,a);break;default:d=sc.bind(null,b,1,a)}c?a.addEventListener(b,
d,!0):a.addEventListener(b,d,!1)}function Ei(a,b,c,d){Oa||vd();var e=sc,f=Oa;Oa=!0;try{eg(e,a,b,c,d)}finally{(Oa=f)||ud()}}function Fi(a,b,c,d){Gi(Hi,sc.bind(null,a,b,c,d))}function sc(a,b,c,d){if(tc)if(0<fa.length&&-1<Nd.indexOf(a))a=Od(null,a,b,c,d),fa.push(a);else{var e=Qd(a,b,c,d);if(null===e)Xf(a,d);else if(-1<Nd.indexOf(a))a=Od(e,a,b,c,d),fa.push(a);else if(!zi(e,a,b,c,d)){Xf(a,d);a=Vf(a,d,null,b);try{uf(Wf,a)}finally{Uf(a)}}}}function Qd(a,b,c,d){c=Ld(d);c=Bb(c);if(null!==c){var e=Na(c);if(null===
e)c=null;else{var f=e.tag;if(13===f){c=Qf(e);if(null!==c)return c;c=null}else if(3===f){if(e.stateNode.hydrate)return 3===e.tag?e.stateNode.containerInfo:null;c=null}else e!==c&&(c=null)}}a=Vf(a,d,c,b);try{uf(Wf,a)}finally{Uf(a)}return null}function fg(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||Kb.hasOwnProperty(a)&&Kb[a]?(""+b).trim():b+"px"}function gg(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),e=fg(c,b[c],d);"float"===
c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ud(a,b){if(b){if(Ii[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML))throw Error(k(137,a,""));if(null!=b.dangerouslySetInnerHTML){if(null!=b.children)throw Error(k(60));if(!("object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML))throw Error(k(61));}if(null!=b.style&&"object"!==typeof b.style)throw Error(k(62,""));}}function Vd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
default:return!0}}function oa(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Jd(a);b=rd[b];for(var d=0;d<b.length;d++)Md(b[d],a,c)}function uc(){}function Wd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function hg(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function ig(a,b){var c=hg(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,
offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=hg(c)}}function jg(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?jg(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function kg(){for(var a=window,b=Wd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=Wd(a.document)}return b}
function Xd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function lg(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}function Yd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&
null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function kb(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break}return a}function mg(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if(c===ng||c===Zd||c===$d){if(0===b)return a;b--}else c===og&&b++}a=a.previousSibling}return null}function Bb(a){var b=a[Aa];if(b)return b;for(var c=a.parentNode;c;){if(b=c[Lb]||c[Aa]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=mg(a);null!==
a;){if(c=a[Aa])return c;a=mg(a)}return b}a=c;c=a.parentNode}return null}function Hb(a){a=a[Aa]||a[Lb];return!a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Pa(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(k(33));}function ae(a){return a[vc]||null}function pa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function pg(a,b){var c=a.stateNode;if(!c)return null;var d=td(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=
!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;if(c&&"function"!==typeof c)throw Error(k(231,b,typeof c));return c}function qg(a,b,c){if(b=pg(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a)}function Ji(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=pa(b);for(b=c.length;0<b--;)qg(c[b],
"captured",a);for(b=0;b<c.length;b++)qg(c[b],"bubbled",a)}}function be(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=pg(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=jb(c._dispatchListeners,b),c._dispatchInstances=jb(c._dispatchInstances,a))}function Ki(a){a&&a.dispatchConfig.registrationName&&be(a._targetInst,null,a)}function lb(a){Kd(a,Ji)}function rg(){if(wc)return wc;var a,b=ce,c=b.length,d,e="value"in Ba?Ba.value:Ba.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=
c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return wc=e.slice(a,1<d?1-d:void 0)}function xc(){return!0}function yc(){return!1}function R(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?xc:yc;this.isPropagationStopped=yc;return this}function Li(a,b,c,d){if(this.eventPool.length){var e=
this.eventPool.pop();this.call(e,a,b,c,d);return e}return new this(a,b,c,d)}function Mi(a){if(!(a instanceof this))throw Error(k(279));a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function sg(a){a.eventPool=[];a.getPooled=Li;a.release=Mi}function tg(a,b){switch(a){case "keyup":return-1!==Ni.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function ug(a){a=a.detail;return"object"===typeof a&&"data"in
a?a.data:null}function Oi(a,b){switch(a){case "compositionend":return ug(b);case "keypress":if(32!==b.which)return null;vg=!0;return wg;case "textInput":return a=b.data,a===wg&&vg?null:a;default:return null}}function Pi(a,b){if(mb)return"compositionend"===a||!de&&tg(a,b)?(a=rg(),wc=ce=Ba=null,mb=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;
case "compositionend":return xg&&"ko"!==b.locale?null:b.data;default:return null}}function yg(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Qi[a.type]:"textarea"===b?!0:!1}function zg(a,b,c){a=R.getPooled(Ag.change,a,b,c);a.type="change";sf(c);lb(a);return a}function Ri(a){pc(a)}function zc(a){var b=Pa(a);if(Gf(b))return a}function Si(a,b){if("change"===a)return b}function Bg(){Mb&&(Mb.detachEvent("onpropertychange",Cg),Nb=Mb=null)}function Cg(a){if("value"===a.propertyName&&
zc(Nb))if(a=zg(Nb,a,Ld(a)),Oa)pc(a);else{Oa=!0;try{ee(Ri,a)}finally{Oa=!1,ud()}}}function Ti(a,b,c){"focus"===a?(Bg(),Mb=b,Nb=c,Mb.attachEvent("onpropertychange",Cg)):"blur"===a&&Bg()}function Ui(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return zc(Nb)}function Vi(a,b){if("click"===a)return zc(b)}function Wi(a,b){if("input"===a||"change"===a)return zc(b)}function Xi(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Yi[a])?!!b[a]:!1}function fe(a){return Xi}
function Zi(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function Ob(a,b){if(Qa(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!$i.call(b,c[d])||!Qa(a[c[d]],b[c[d]]))return!1;return!0}function Dg(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(ge||null==nb||nb!==Wd(c))return null;c=nb;"selectionStart"in c&&Xd(c)?c={start:c.selectionStart,
end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return Pb&&Ob(Pb,c)?null:(Pb=c,a=R.getPooled(Eg.select,he,a,b),a.type="select",a.target=nb,lb(a),a)}function Ac(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function q(a,b){0>ob||(a.current=ie[ob],ie[ob]=null,ob--)}function y(a,b,c){ob++;
ie[ob]=a.current;a.current=b}function pb(a,b){var c=a.type.contextTypes;if(!c)return Ca;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function N(a){a=a.childContextTypes;return null!==a&&void 0!==a}function Fg(a,b,c){if(B.current!==Ca)throw Error(k(168));y(B,b);y(G,c)}
function Gg(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(k(108,na(b)||"Unknown",e));return M({},c,{},d)}function Bc(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Ca;Ra=B.current;y(B,a);y(G,G.current);return!0}function Hg(a,b,c){var d=a.stateNode;if(!d)throw Error(k(169));c?(a=Gg(a,b,Ra),d.__reactInternalMemoizedMergedChildContext=a,q(G),q(B),y(B,a)):q(G);y(G,c)}function Cc(){switch(aj()){case Dc:return 99;
case Ig:return 98;case Jg:return 97;case Kg:return 96;case Lg:return 95;default:throw Error(k(332));}}function Mg(a){switch(a){case 99:return Dc;case 98:return Ig;case 97:return Jg;case 96:return Kg;case 95:return Lg;default:throw Error(k(332));}}function Da(a,b){a=Mg(a);return bj(a,b)}function Ng(a,b,c){a=Mg(a);return je(a,b,c)}function Og(a){null===qa?(qa=[a],Ec=je(Dc,Pg)):qa.push(a);return Qg}function ha(){if(null!==Ec){var a=Ec;Ec=null;Rg(a)}Pg()}function Pg(){if(!ke&&null!==qa){ke=!0;var a=0;
try{var b=qa;Da(99,function(){for(;a<b.length;a++){var c=b[a];do c=c(!0);while(null!==c)}});qa=null}catch(c){throw null!==qa&&(qa=qa.slice(a+1)),je(Dc,ha),c;}finally{ke=!1}}}function Fc(a,b,c){c/=10;return 1073741821-(((1073741821-a+b/10)/c|0)+1)*c}function aa(a,b){if(a&&a.defaultProps){b=M({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function le(){Gc=qb=Hc=null}function me(a){var b=Ic.current;q(Ic);a.type._context._currentValue=b}function Sg(a,b){for(;null!==a;){var c=
a.alternate;if(a.childExpirationTime<b)a.childExpirationTime=b,null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);else if(null!==c&&c.childExpirationTime<b)c.childExpirationTime=b;else break;a=a.return}}function rb(a,b){Hc=a;Gc=qb=null;a=a.dependencies;null!==a&&null!==a.firstContext&&(a.expirationTime>=b&&(ia=!0),a.firstContext=null)}function W(a,b){if(Gc!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Gc=a,b=1073741823;b={context:a,observedBits:b,next:null};if(null===qb){if(null===
Hc)throw Error(k(308));qb=b;Hc.dependencies={expirationTime:0,firstContext:b,responders:null}}else qb=qb.next=b}return a._currentValue}function ne(a){a.updateQueue={baseState:a.memoizedState,baseQueue:null,shared:{pending:null},effects:null}}function oe(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,baseQueue:a.baseQueue,shared:a.shared,effects:a.effects})}function Ea(a,b){a={expirationTime:a,suspenseConfig:b,tag:Tg,payload:null,callback:null,next:null};return a.next=
a}function Fa(a,b){a=a.updateQueue;if(null!==a){a=a.shared;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b}}function Ug(a,b){var c=a.alternate;null!==c&&oe(c,a);a=a.updateQueue;c=a.baseQueue;null===c?(a.baseQueue=b.next=b,b.next=b):(b.next=c.next,c.next=b)}function Qb(a,b,c,d){var e=a.updateQueue;Ga=!1;var f=e.baseQueue,g=e.shared.pending;if(null!==g){if(null!==f){var h=f.next;f.next=g.next;g.next=h}f=g;e.shared.pending=null;h=a.alternate;null!==h&&(h=h.updateQueue,null!==h&&
(h.baseQueue=g))}if(null!==f){h=f.next;var m=e.baseState,n=0,k=null,ba=null,l=null;if(null!==h){var p=h;do{g=p.expirationTime;if(g<d){var t={expirationTime:p.expirationTime,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null};null===l?(ba=l=t,k=m):l=l.next=t;g>n&&(n=g)}else{null!==l&&(l=l.next={expirationTime:1073741823,suspenseConfig:p.suspenseConfig,tag:p.tag,payload:p.payload,callback:p.callback,next:null});Vg(g,p.suspenseConfig);a:{var q=a,r=p;g=b;t=c;switch(r.tag){case 1:q=
r.payload;if("function"===typeof q){m=q.call(t,m,g);break a}m=q;break a;case 3:q.effectTag=q.effectTag&-4097|64;case Tg:q=r.payload;g="function"===typeof q?q.call(t,m,g):q;if(null===g||void 0===g)break a;m=M({},m,g);break a;case Jc:Ga=!0}}null!==p.callback&&(a.effectTag|=32,g=e.effects,null===g?e.effects=[p]:g.push(p))}p=p.next;if(null===p||p===h)if(g=e.shared.pending,null===g)break;else p=f.next=g.next,g.next=h,e.baseQueue=f=g,e.shared.pending=null}while(1)}null===l?k=m:l.next=ba;e.baseState=k;e.baseQueue=
l;Kc(n);a.expirationTime=n;a.memoizedState=m}}function Wg(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;b<a.length;b++){var d=a[b],e=d.callback;if(null!==e){d.callback=null;d=e;e=c;if("function"!==typeof d)throw Error(k(191,d));d.call(e)}}}function Lc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:M({},b,c);a.memoizedState=c;0===a.expirationTime&&(a.updateQueue.baseState=c)}function Xg(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,
f,g):b.prototype&&b.prototype.isPureReactComponent?!Ob(c,d)||!Ob(e,f):!0}function Yg(a,b,c){var d=!1,e=Ca;var f=b.contextType;"object"===typeof f&&null!==f?f=W(f):(e=N(b)?Ra:B.current,d=b.contextTypes,f=(d=null!==d&&void 0!==d)?pb(a,e):Ca);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=Mc;a.stateNode=b;b._reactInternalFiber=a;d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=e,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zg(a,
b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==a&&Mc.enqueueReplaceState(b,b.state,null)}function pe(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$g;ne(a);var f=b.contextType;"object"===typeof f&&null!==f?e.context=W(f):(f=N(b)?Ra:B.current,e.context=pb(a,f));Qb(a,c,e,d);e.state=a.memoizedState;f=b.getDerivedStateFromProps;
"function"===typeof f&&(Lc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&Mc.enqueueReplaceState(e,e.state,null),Qb(a,c,e,d),e.state=a.memoizedState);"function"===
typeof e.componentDidMount&&(a.effectTag|=4)}function Rb(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;if(c){if(1!==c.tag)throw Error(k(309));var d=c.stateNode}if(!d)throw Error(k(147,a));var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$g&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}if("string"!==typeof a)throw Error(k(284));if(!c._owner)throw Error(k(290,
a));}return a}function Nc(a,b){if("textarea"!==a.type)throw Error(k(31,"[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,""));}function ah(a){function b(b,c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,
b),b=b.sibling;return a}function e(a,b){a=Sa(a,b);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=qe(c,a.mode,d),b.return=a,b;b=e(b,c);b.return=a;return b}function m(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props),d.ref=Rb(a,b,c),d.return=a,d;d=Oc(c.type,
c.key,c.props,null,a.mode,d);d.ref=Rb(a,b,c);d.return=a;return d}function n(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=re(c,a.mode,d),b.return=a,b;b=e(b,c.children||[]);b.return=a;return b}function l(a,b,c,d,f){if(null===b||7!==b.tag)return b=Ha(c,a.mode,d,f),b.return=a,b;b=e(b,c);b.return=a;return b}function ba(a,b,c){if("string"===typeof b||"number"===typeof b)return b=qe(""+b,a.mode,c),b.return=a,b;if("object"===
typeof b&&null!==b){switch(b.$$typeof){case Pc:return c=Oc(b.type,b.key,b.props,null,a.mode,c),c.ref=Rb(a,null,b),c.return=a,c;case gb:return b=re(b,a.mode,c),b.return=a,b}if(Qc(b)||zb(b))return b=Ha(b,a.mode,c,null),b.return=a,b;Nc(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Pc:return c.key===e?c.type===Ma?l(a,b,c.props.children,d,e):m(a,b,c,
d):null;case gb:return c.key===e?n(a,b,c,d):null}if(Qc(c)||zb(c))return null!==e?null:l(a,b,c,d,null);Nc(a,c)}return null}function t(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Pc:return a=a.get(null===d.key?c:d.key)||null,d.type===Ma?l(b,a,d.props.children,e,d.key):m(b,a,d,e);case gb:return a=a.get(null===d.key?c:d.key)||null,n(b,a,d,e)}if(Qc(d)||zb(d))return a=a.get(c)||null,l(b,a,d,e,null);
Nc(b,d)}return null}function q(e,g,h,m){for(var n=null,k=null,l=g,r=g=0,C=null;null!==l&&r<h.length;r++){l.index>r?(C=l,l=null):C=l.sibling;var O=p(e,l,h[r],m);if(null===O){null===l&&(l=C);break}a&&l&&null===O.alternate&&b(e,l);g=f(O,g,r);null===k?n=O:k.sibling=O;k=O;l=C}if(r===h.length)return c(e,l),n;if(null===l){for(;r<h.length;r++)l=ba(e,h[r],m),null!==l&&(g=f(l,g,r),null===k?n=l:k.sibling=l,k=l);return n}for(l=d(e,l);r<h.length;r++)C=t(l,e,r,h[r],m),null!==C&&(a&&null!==C.alternate&&l.delete(null===
C.key?r:C.key),g=f(C,g,r),null===k?n=C:k.sibling=C,k=C);a&&l.forEach(function(a){return b(e,a)});return n}function w(e,g,h,n){var m=zb(h);if("function"!==typeof m)throw Error(k(150));h=m.call(h);if(null==h)throw Error(k(151));for(var l=m=null,r=g,C=g=0,O=null,v=h.next();null!==r&&!v.done;C++,v=h.next()){r.index>C?(O=r,r=null):O=r.sibling;var q=p(e,r,v.value,n);if(null===q){null===r&&(r=O);break}a&&r&&null===q.alternate&&b(e,r);g=f(q,g,C);null===l?m=q:l.sibling=q;l=q;r=O}if(v.done)return c(e,r),m;
if(null===r){for(;!v.done;C++,v=h.next())v=ba(e,v.value,n),null!==v&&(g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);return m}for(r=d(e,r);!v.done;C++,v=h.next())v=t(r,e,C,v.value,n),null!==v&&(a&&null!==v.alternate&&r.delete(null===v.key?C:v.key),g=f(v,g,C),null===l?m=v:l.sibling=v,l=v);a&&r.forEach(function(a){return b(e,a)});return m}return function(a,d,f,h){var m="object"===typeof f&&null!==f&&f.type===Ma&&null===f.key;m&&(f=f.props.children);var n="object"===typeof f&&null!==f;if(n)switch(f.$$typeof){case Pc:a:{n=
f.key;for(m=d;null!==m;){if(m.key===n){switch(m.tag){case 7:if(f.type===Ma){c(a,m.sibling);d=e(m,f.props.children);d.return=a;a=d;break a}break;default:if(m.elementType===f.type){c(a,m.sibling);d=e(m,f.props);d.ref=Rb(a,m,f);d.return=a;a=d;break a}}c(a,m);break}else b(a,m);m=m.sibling}f.type===Ma?(d=Ha(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Oc(f.type,f.key,f.props,null,a.mode,h),h.ref=Rb(a,d,f),h.return=a,a=h)}return g(a);case gb:a:{for(m=f.key;null!==d;){if(d.key===m)if(4===d.tag&&d.stateNode.containerInfo===
f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=re(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=qe(f,a.mode,h),d.return=a,a=d),g(a);if(Qc(f))return q(a,d,f,h);if(zb(f))return w(a,d,f,h);n&&Nc(a,f);if("undefined"===typeof f&&!m)switch(a.tag){case 1:case 0:throw a=
a.type,Error(k(152,a.displayName||a.name||"Component"));}return c(a,d)}}function Ta(a){if(a===Sb)throw Error(k(174));return a}function se(a,b){y(Tb,b);y(Ub,a);y(ja,Sb);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Hd(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=Hd(b,a)}q(ja);y(ja,b)}function tb(a){q(ja);q(Ub);q(Tb)}function bh(a){Ta(Tb.current);var b=Ta(ja.current);var c=Hd(b,a.type);b!==c&&(y(Ub,a),y(ja,c))}function te(a){Ub.current===
a&&(q(ja),q(Ub))}function Rc(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||c.data===$d||c.data===Zd))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.effectTag&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}return null}function ue(a,b){return{responder:a,props:b}}
function S(){throw Error(k(321));}function ve(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Qa(a[c],b[c]))return!1;return!0}function we(a,b,c,d,e,f){Ia=f;z=b;b.memoizedState=null;b.updateQueue=null;b.expirationTime=0;Sc.current=null===a||null===a.memoizedState?dj:ej;a=c(d,e);if(b.expirationTime===Ia){f=0;do{b.expirationTime=0;if(!(25>f))throw Error(k(301));f+=1;J=K=null;b.updateQueue=null;Sc.current=fj;a=c(d,e)}while(b.expirationTime===Ia)}Sc.current=Tc;b=null!==K&&null!==K.next;
Ia=0;J=K=z=null;Uc=!1;if(b)throw Error(k(300));return a}function ub(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===J?z.memoizedState=J=a:J=J.next=a;return J}function vb(){if(null===K){var a=z.alternate;a=null!==a?a.memoizedState:null}else a=K.next;var b=null===J?z.memoizedState:J.next;if(null!==b)J=b,K=a;else{if(null===a)throw Error(k(310));K=a;a={memoizedState:K.memoizedState,baseState:K.baseState,baseQueue:K.baseQueue,queue:K.queue,next:null};null===J?z.memoizedState=
J=a:J=J.next=a}return J}function Ua(a,b){return"function"===typeof b?b(a):b}function Vc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=K,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g}d.baseQueue=e=f;c.pending=null}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,m=e;do{var n=m.expirationTime;if(n<Ia){var l={expirationTime:m.expirationTime,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,
next:null};null===h?(g=h=l,f=d):h=h.next=l;n>z.expirationTime&&(z.expirationTime=n,Kc(n))}else null!==h&&(h=h.next={expirationTime:1073741823,suspenseConfig:m.suspenseConfig,action:m.action,eagerReducer:m.eagerReducer,eagerState:m.eagerState,next:null}),Vg(n,m.suspenseConfig),d=m.eagerReducer===a?m.eagerState:a(d,m.action);m=m.next}while(null!==m&&m!==e);null===h?f=d:h.next=g;Qa(d,b.memoizedState)||(ia=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d}return[b.memoizedState,
c.dispatch]}function Wc(a,b,c){b=vb();c=b.queue;if(null===c)throw Error(k(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);Qa(f,b.memoizedState)||(ia=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f}return[f,d]}function xe(a){var b=ub();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={pending:null,dispatch:null,lastRenderedReducer:Ua,
lastRenderedState:a};a=a.dispatch=ch.bind(null,z,a);return[b.memoizedState,a]}function ye(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=z.updateQueue;null===b?(b={lastEffect:null},z.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a}function dh(a){return vb().memoizedState}function ze(a,b,c,d){var e=ub();z.effectTag|=a;e.memoizedState=ye(1|b,c,void 0,void 0===d?null:d)}function Ae(a,b,c,d){var e=vb();
d=void 0===d?null:d;var f=void 0;if(null!==K){var g=K.memoizedState;f=g.destroy;if(null!==d&&ve(d,g.deps)){ye(b,c,f,d);return}}z.effectTag|=a;e.memoizedState=ye(1|b,c,f,d)}function eh(a,b){return ze(516,4,a,b)}function Xc(a,b){return Ae(516,4,a,b)}function fh(a,b){return Ae(4,2,a,b)}function gh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function hh(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;
return Ae(4,2,gh.bind(null,b,a),c)}function Be(a,b){}function ih(a,b){ub().memoizedState=[a,void 0===b?null:b];return a}function Yc(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];c.memoizedState=[a,b];return a}function jh(a,b){var c=vb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&ve(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Ce(a,b,c){var d=Cc();Da(98>d?98:d,function(){a(!0)});Da(97<d?97:d,function(){var d=
X.suspense;X.suspense=void 0===b?null:b;try{a(!1),c()}finally{X.suspense=d}})}function ch(a,b,c){var d=ka(),e=Vb.suspense;d=Va(d,a,e);e={expirationTime:d,suspenseConfig:e,action:c,eagerReducer:null,eagerState:null,next:null};var f=b.pending;null===f?e.next=e:(e.next=f.next,f.next=e);b.pending=e;f=a.alternate;if(a===z||null!==f&&f===z)Uc=!0,e.expirationTime=Ia,z.expirationTime=Ia;else{if(0===a.expirationTime&&(null===f||0===f.expirationTime)&&(f=b.lastRenderedReducer,null!==f))try{var g=b.lastRenderedState,
h=f(g,c);e.eagerReducer=f;e.eagerState=h;if(Qa(h,g))return}catch(m){}finally{}Ja(a,d)}}function kh(a,b){var c=la(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function lh(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?
null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function De(a){if(Wa){var b=Ka;if(b){var c=b;if(!lh(a,b)){b=kb(c.nextSibling);if(!b||!lh(a,b)){a.effectTag=a.effectTag&-1025|2;Wa=!1;ra=a;return}kh(ra,c)}ra=a;Ka=kb(b.firstChild)}else a.effectTag=a.effectTag&-1025|2,Wa=!1,ra=a}}function mh(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&13!==a.tag;)a=a.return;ra=a}function Zc(a){if(a!==ra)return!1;if(!Wa)return mh(a),Wa=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==
b&&!Yd(b,a.memoizedProps))for(b=Ka;b;)kh(a,b),b=kb(b.nextSibling);mh(a);if(13===a.tag){a=a.memoizedState;a=null!==a?a.dehydrated:null;if(!a)throw Error(k(317));a:{a=a.nextSibling;for(b=0;a;){if(8===a.nodeType){var c=a.data;if(c===og){if(0===b){Ka=kb(a.nextSibling);break a}b--}else c!==ng&&c!==Zd&&c!==$d||b++}a=a.nextSibling}Ka=null}}else Ka=ra?kb(a.stateNode.nextSibling):null;return!0}function Ee(){Ka=ra=null;Wa=!1}function T(a,b,c,d){b.child=null===a?Fe(b,null,c,d):wb(b,a.child,c,d)}function nh(a,
b,c,d,e){c=c.render;var f=b.ref;rb(b,e);d=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,d,e);return b.child}function oh(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Ge(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,ph(a,b,g,d,e,f);a=Oc(c.type,null,d,null,b.mode,f);a.ref=b.ref;a.return=b;return b.child=a}g=a.child;if(e<
f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:Ob,c(e,d)&&a.ref===b.ref))return sa(a,b,f);b.effectTag|=1;a=Sa(g,d);a.ref=b.ref;a.return=b;return b.child=a}function ph(a,b,c,d,e,f){return null!==a&&Ob(a.memoizedProps,d)&&a.ref===b.ref&&(ia=!1,e<f)?(b.expirationTime=a.expirationTime,sa(a,b,f)):He(a,b,c,d,f)}function qh(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function He(a,b,c,d,e){var f=N(c)?Ra:B.current;f=pb(b,f);rb(b,e);c=we(a,b,c,d,f,e);if(null!==a&&!ia)return b.updateQueue=
a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),sa(a,b,e);b.effectTag|=1;T(a,b,c,e);return b.child}function rh(a,b,c,d,e){if(N(c)){var f=!0;Bc(b)}else f=!1;rb(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yg(b,c,d),pe(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var m=g.context,n=c.contextType;"object"===typeof n&&null!==n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n));var l=c.getDerivedStateFromProps,k="function"===
typeof l||"function"===typeof g.getSnapshotBeforeUpdate;k||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n);Ga=!1;var p=b.memoizedState;g.state=p;Qb(b,d,g,e);m=b.memoizedState;h!==d||p!==m||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),m=b.memoizedState),(h=Ga||Xg(b,c,h,d,p,m,n))?(k||"function"!==typeof g.UNSAFE_componentWillMount&&"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&
g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=m),g.props=d,g.state=m,g.context=n,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,oe(a,b),h=b.memoizedProps,g.props=b.type===b.elementType?h:aa(b.type,h),m=g.context,n=c.contextType,"object"===typeof n&&null!==
n?n=W(n):(n=N(c)?Ra:B.current,n=pb(b,n)),l=c.getDerivedStateFromProps,(k="function"===typeof l||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||m!==n)&&Zg(b,g,d,n),Ga=!1,m=b.memoizedState,g.state=m,Qb(b,d,g,e),p=b.memoizedState,h!==d||m!==p||G.current||Ga?("function"===typeof l&&(Lc(b,c,l,d),p=b.memoizedState),(l=Ga||Xg(b,c,h,d,m,p,n))?(k||"function"!==typeof g.UNSAFE_componentWillUpdate&&
"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,n),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,n)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===
a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=n,d=l):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&m===a.memoizedState||(b.effectTag|=256),d=!1);return Ie(a,b,c,d,f,e)}function Ie(a,b,c,d,e,f){qh(a,b);var g=0!==(b.effectTag&64);if(!d&&!g)return e&&Hg(b,c,!1),sa(a,b,f);d=b.stateNode;gj.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?
null:d.render();b.effectTag|=1;null!==a&&g?(b.child=wb(b,a.child,null,f),b.child=wb(b,null,h,f)):T(a,b,h,f);b.memoizedState=d.state;e&&Hg(b,c,!0);return b.child}function sh(a){var b=a.stateNode;b.pendingContext?Fg(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Fg(a,b.context,!1);se(a,b.containerInfo)}function th(a,b,c){var d=b.mode,e=b.pendingProps,f=D.current,g=!1,h;(h=0!==(b.effectTag&64))||(h=0!==(f&2)&&(null===a||null!==a.memoizedState));h?(g=!0,b.effectTag&=-65):null!==a&&null===
a.memoizedState||void 0===e.fallback||!0===e.unstable_avoidThisFallback||(f|=1);y(D,f&1);if(null===a){void 0!==e.fallback&&De(b);if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;b.memoizedState=Je;b.child=e;return c}d=e.children;b.memoizedState=null;return b.child=Fe(b,null,d,c)}if(null!==a.memoizedState){a=a.child;d=a.sibling;if(g){e=e.fallback;
c=Sa(a,a.pendingProps);c.return=b;if(0===(b.mode&2)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==a.child))for(c.child=g;null!==g;)g.return=c,g=g.sibling;d=Sa(d,e);d.return=b;c.sibling=d;c.childExpirationTime=0;b.memoizedState=Je;b.child=c;return d}c=wb(b,a.child,e.children,c);b.memoizedState=null;return b.child=c}a=a.child;if(g){g=e.fallback;e=Ha(null,d,0,null);e.return=b;e.child=a;null!==a&&(a.return=e);if(0===(b.mode&2))for(a=null!==b.memoizedState?b.child.child:b.child,e.child=a;null!==
a;)a.return=e,a=a.sibling;c=Ha(g,d,c,null);c.return=b;e.sibling=c;c.effectTag|=2;e.childExpirationTime=0;b.memoizedState=Je;b.child=e;return c}b.memoizedState=null;return b.child=wb(b,a,e.children,c)}function uh(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);Sg(a.return,b)}function Ke(a,b,c,d,e,f){var g=a.memoizedState;null===g?a.memoizedState={isBackwards:b,rendering:null,renderingStartTime:0,last:d,tail:c,tailExpiration:0,tailMode:e,
lastEffect:f}:(g.isBackwards=b,g.rendering=null,g.renderingStartTime=0,g.last=d,g.tail=c,g.tailExpiration=0,g.tailMode=e,g.lastEffect=f)}function vh(a,b,c){var d=b.pendingProps,e=d.revealOrder,f=d.tail;T(a,b,d.children,c);d=D.current;if(0!==(d&2))d=d&1|2,b.effectTag|=64;else{if(null!==a&&0!==(a.effectTag&64))a:for(a=b.child;null!==a;){if(13===a.tag)null!==a.memoizedState&&uh(a,c);else if(19===a.tag)uh(a,c);else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===b)break a;for(;null===a.sibling;){if(null===
a.return||a.return===b)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}d&=1}y(D,d);if(0===(b.mode&2))b.memoizedState=null;else switch(e){case "forwards":c=b.child;for(e=null;null!==c;)a=c.alternate,null!==a&&null===Rc(a)&&(e=c),c=c.sibling;c=e;null===c?(e=b.child,b.child=null):(e=c.sibling,c.sibling=null);Ke(b,!1,e,c,f,b.lastEffect);break;case "backwards":c=null;e=b.child;for(b.child=null;null!==e;){a=e.alternate;if(null!==a&&null===Rc(a)){b.child=e;break}a=e.sibling;e.sibling=c;c=e;e=a}Ke(b,
!0,c,null,f,b.lastEffect);break;case "together":Ke(b,!1,null,null,void 0,b.lastEffect);break;default:b.memoizedState=null}return b.child}function sa(a,b,c){null!==a&&(b.dependencies=a.dependencies);var d=b.expirationTime;0!==d&&Kc(d);if(b.childExpirationTime<c)return null;if(null!==a&&b.child!==a.child)throw Error(k(153));if(null!==b.child){a=b.child;c=Sa(a,a.pendingProps);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Sa(a,a.pendingProps),c.return=b;c.sibling=null}return b.child}
function $c(a,b){switch(a.tailMode){case "hidden":b=a.tail;for(var c=null;null!==b;)null!==b.alternate&&(c=b),b=b.sibling;null===c?a.tail=null:c.sibling=null;break;case "collapsed":c=a.tail;for(var d=null;null!==c;)null!==c.alternate&&(d=c),c=c.sibling;null===d?b||null===a.tail?a.tail=null:a.tail.sibling=null:d.sibling=null}}function hj(a,b,c){var d=b.pendingProps;switch(b.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return null;case 1:return N(b.type)&&(q(G),q(B)),
null;case 3:return tb(),q(G),q(B),c=b.stateNode,c.pendingContext&&(c.context=c.pendingContext,c.pendingContext=null),null!==a&&null!==a.child||!Zc(b)||(b.effectTag|=4),wh(b),null;case 5:te(b);c=Ta(Tb.current);var e=b.type;if(null!==a&&null!=b.stateNode)ij(a,b,e,d,c),a.ref!==b.ref&&(b.effectTag|=128);else{if(!d){if(null===b.stateNode)throw Error(k(166));return null}a=Ta(ja.current);if(Zc(b)){d=b.stateNode;e=b.type;var f=b.memoizedProps;d[Aa]=b;d[vc]=f;switch(e){case "iframe":case "object":case "embed":w("load",
d);break;case "video":case "audio":for(a=0;a<Db.length;a++)w(Db[a],d);break;case "source":w("error",d);break;case "img":case "image":case "link":w("error",d);w("load",d);break;case "form":w("reset",d);w("submit",d);break;case "details":w("toggle",d);break;case "input":Hf(d,f);w("invalid",d);oa(c,"onChange");break;case "select":d._wrapperState={wasMultiple:!!f.multiple};w("invalid",d);oa(c,"onChange");break;case "textarea":Kf(d,f),w("invalid",d),oa(c,"onChange")}Ud(e,f);a=null;for(var g in f)if(f.hasOwnProperty(g)){var h=
f[g];"children"===g?"string"===typeof h?d.textContent!==h&&(a=["children",h]):"number"===typeof h&&d.textContent!==""+h&&(a=["children",""+h]):db.hasOwnProperty(g)&&null!=h&&oa(c,g)}switch(e){case "input":mc(d);Jf(d,f,!0);break;case "textarea":mc(d);Mf(d);break;case "select":case "option":break;default:"function"===typeof f.onClick&&(d.onclick=uc)}c=a;b.updateQueue=c;null!==c&&(b.effectTag|=4)}else{g=9===c.nodeType?c:c.ownerDocument;"http://www.w3.org/1999/xhtml"===a&&(a=Nf(e));"http://www.w3.org/1999/xhtml"===
a?"script"===e?(a=g.createElement("div"),a.innerHTML="<script>\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(e,{is:d.is}):(a=g.createElement(e),"select"===e&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,e);a[Aa]=b;a[vc]=d;jj(a,b,!1,!1);b.stateNode=a;g=Vd(e,d);switch(e){case "iframe":case "object":case "embed":w("load",a);h=d;break;case "video":case "audio":for(h=0;h<Db.length;h++)w(Db[h],a);h=d;break;case "source":w("error",a);
h=d;break;case "img":case "image":case "link":w("error",a);w("load",a);h=d;break;case "form":w("reset",a);w("submit",a);h=d;break;case "details":w("toggle",a);h=d;break;case "input":Hf(a,d);h=Cd(a,d);w("invalid",a);oa(c,"onChange");break;case "option":h=Fd(a,d);break;case "select":a._wrapperState={wasMultiple:!!d.multiple};h=M({},d,{value:void 0});w("invalid",a);oa(c,"onChange");break;case "textarea":Kf(a,d);h=Gd(a,d);w("invalid",a);oa(c,"onChange");break;default:h=d}Ud(e,h);var m=h;for(f in m)if(m.hasOwnProperty(f)){var n=
m[f];"style"===f?gg(a,n):"dangerouslySetInnerHTML"===f?(n=n?n.__html:void 0,null!=n&&xh(a,n)):"children"===f?"string"===typeof n?("textarea"!==e||""!==n)&&Wb(a,n):"number"===typeof n&&Wb(a,""+n):"suppressContentEditableWarning"!==f&&"suppressHydrationWarning"!==f&&"autoFocus"!==f&&(db.hasOwnProperty(f)?null!=n&&oa(c,f):null!=n&&xd(a,f,n,g))}switch(e){case "input":mc(a);Jf(a,d,!1);break;case "textarea":mc(a);Mf(a);break;case "option":null!=d.value&&a.setAttribute("value",""+va(d.value));break;case "select":a.multiple=
!!d.multiple;c=d.value;null!=c?hb(a,!!d.multiple,c,!1):null!=d.defaultValue&&hb(a,!!d.multiple,d.defaultValue,!0);break;default:"function"===typeof h.onClick&&(a.onclick=uc)}lg(e,d)&&(b.effectTag|=4)}null!==b.ref&&(b.effectTag|=128)}return null;case 6:if(a&&null!=b.stateNode)kj(a,b,a.memoizedProps,d);else{if("string"!==typeof d&&null===b.stateNode)throw Error(k(166));c=Ta(Tb.current);Ta(ja.current);Zc(b)?(c=b.stateNode,d=b.memoizedProps,c[Aa]=b,c.nodeValue!==d&&(b.effectTag|=4)):(c=(9===c.nodeType?
c:c.ownerDocument).createTextNode(d),c[Aa]=b,b.stateNode=c)}return null;case 13:q(D);d=b.memoizedState;if(0!==(b.effectTag&64))return b.expirationTime=c,b;c=null!==d;d=!1;null===a?void 0!==b.memoizedProps.fallback&&Zc(b):(e=a.memoizedState,d=null!==e,c||null===e||(e=a.child.sibling,null!==e&&(f=b.firstEffect,null!==f?(b.firstEffect=e,e.nextEffect=f):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8)));if(c&&!d&&0!==(b.mode&2))if(null===a&&!0!==b.memoizedProps.unstable_avoidThisFallback||
0!==(D.current&1))F===Xa&&(F=ad);else{if(F===Xa||F===ad)F=bd;0!==Xb&&null!==U&&(Ya(U,P),yh(U,Xb))}if(c||d)b.effectTag|=4;return null;case 4:return tb(),wh(b),null;case 10:return me(b),null;case 17:return N(b.type)&&(q(G),q(B)),null;case 19:q(D);d=b.memoizedState;if(null===d)return null;e=0!==(b.effectTag&64);f=d.rendering;if(null===f)if(e)$c(d,!1);else{if(F!==Xa||null!==a&&0!==(a.effectTag&64))for(f=b.child;null!==f;){a=Rc(f);if(null!==a){b.effectTag|=64;$c(d,!1);e=a.updateQueue;null!==e&&(b.updateQueue=
e,b.effectTag|=4);null===d.lastEffect&&(b.firstEffect=null);b.lastEffect=d.lastEffect;for(d=b.child;null!==d;)e=d,f=c,e.effectTag&=2,e.nextEffect=null,e.firstEffect=null,e.lastEffect=null,a=e.alternate,null===a?(e.childExpirationTime=0,e.expirationTime=f,e.child=null,e.memoizedProps=null,e.memoizedState=null,e.updateQueue=null,e.dependencies=null):(e.childExpirationTime=a.childExpirationTime,e.expirationTime=a.expirationTime,e.child=a.child,e.memoizedProps=a.memoizedProps,e.memoizedState=a.memoizedState,
e.updateQueue=a.updateQueue,f=a.dependencies,e.dependencies=null===f?null:{expirationTime:f.expirationTime,firstContext:f.firstContext,responders:f.responders}),d=d.sibling;y(D,D.current&1|2);return b.child}f=f.sibling}}else{if(!e)if(a=Rc(f),null!==a){if(b.effectTag|=64,e=!0,c=a.updateQueue,null!==c&&(b.updateQueue=c,b.effectTag|=4),$c(d,!0),null===d.tail&&"hidden"===d.tailMode&&!f.alternate)return b=b.lastEffect=d.lastEffect,null!==b&&(b.nextEffect=null),null}else 2*Y()-d.renderingStartTime>d.tailExpiration&&
1<c&&(b.effectTag|=64,e=!0,$c(d,!1),b.expirationTime=b.childExpirationTime=c-1);d.isBackwards?(f.sibling=b.child,b.child=f):(c=d.last,null!==c?c.sibling=f:b.child=f,d.last=f)}return null!==d.tail?(0===d.tailExpiration&&(d.tailExpiration=Y()+500),c=d.tail,d.rendering=c,d.tail=c.sibling,d.lastEffect=b.lastEffect,d.renderingStartTime=Y(),c.sibling=null,b=D.current,y(D,e?b&1|2:b&1),c):null}throw Error(k(156,b.tag));}function lj(a,b){switch(a.tag){case 1:return N(a.type)&&(q(G),q(B)),b=a.effectTag,b&4096?
(a.effectTag=b&-4097|64,a):null;case 3:tb();q(G);q(B);b=a.effectTag;if(0!==(b&64))throw Error(k(285));a.effectTag=b&-4097|64;return a;case 5:return te(a),null;case 13:return q(D),b=a.effectTag,b&4096?(a.effectTag=b&-4097|64,a):null;case 19:return q(D),null;case 4:return tb(),null;case 10:return me(a),null;default:return null}}function Le(a,b){return{value:a,source:b,stack:Bd(b)}}function Me(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=Bd(c));null!==c&&na(c.type);b=b.value;null!==a&&1===a.tag&&
na(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function mj(a,b){try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(c){Za(a,c)}}function zh(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Za(a,c)}else b.current=null}function nj(a,b){switch(b.tag){case 0:case 11:case 15:case 22:return;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===
b.type?c:aa(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}return;case 3:case 5:case 6:case 4:case 17:return}throw Error(k(163));}function Ah(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.destroy;c.destroy=void 0;void 0!==d&&d()}c=c.next}while(c!==b)}}function Bh(a,b){b=b.updateQueue;b=null!==b?b.lastEffect:null;if(null!==b){var c=b=b.next;do{if((c.tag&a)===a){var d=c.create;c.destroy=d()}c=c.next}while(c!==b)}}function oj(a,b,c,d){switch(c.tag){case 0:case 11:case 15:case 22:Bh(3,
c);return;case 1:a=c.stateNode;c.effectTag&4&&(null===b?a.componentDidMount():(d=c.elementType===c.type?b.memoizedProps:aa(c.type,b.memoizedProps),a.componentDidUpdate(d,b.memoizedState,a.__reactInternalSnapshotBeforeUpdate)));b=c.updateQueue;null!==b&&Wg(c,b,a);return;case 3:b=c.updateQueue;if(null!==b){a=null;if(null!==c.child)switch(c.child.tag){case 5:a=c.child.stateNode;break;case 1:a=c.child.stateNode}Wg(c,b,a)}return;case 5:a=c.stateNode;null===b&&c.effectTag&4&&lg(c.type,c.memoizedProps)&&
a.focus();return;case 6:return;case 4:return;case 12:return;case 13:null===c.memoizedState&&(c=c.alternate,null!==c&&(c=c.memoizedState,null!==c&&(c=c.dehydrated,null!==c&&bg(c))));return;case 19:case 17:case 20:case 21:return}throw Error(k(163));}function Ch(a,b,c){"function"===typeof Ne&&Ne(b);switch(b.tag){case 0:case 11:case 14:case 15:case 22:a=b.updateQueue;if(null!==a&&(a=a.lastEffect,null!==a)){var d=a.next;Da(97<c?97:c,function(){var a=d;do{var c=a.destroy;if(void 0!==c){var g=b;try{c()}catch(h){Za(g,
h)}}a=a.next}while(a!==d)})}break;case 1:zh(b);c=b.stateNode;"function"===typeof c.componentWillUnmount&&mj(b,c);break;case 5:zh(b);break;case 4:Dh(a,b,c)}}function Eh(a){var b=a.alternate;a.return=null;a.child=null;a.memoizedState=null;a.updateQueue=null;a.dependencies=null;a.alternate=null;a.firstEffect=null;a.lastEffect=null;a.pendingProps=null;a.memoizedProps=null;a.stateNode=null;null!==b&&Eh(b)}function Fh(a){return 5===a.tag||3===a.tag||4===a.tag}function Gh(a){a:{for(var b=a.return;null!==
b;){if(Fh(b)){var c=b;break a}b=b.return}throw Error(k(160));}b=c.stateNode;switch(c.tag){case 5:var d=!1;break;case 3:b=b.containerInfo;d=!0;break;case 4:b=b.containerInfo;d=!0;break;default:throw Error(k(161));}c.effectTag&16&&(Wb(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fh(c.return)){c=null;break a}c=c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;
else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}d?Oe(a,c,b):Pe(a,c,b)}function Oe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?8===c.nodeType?c.parentNode.insertBefore(a,b):c.insertBefore(a,b):(8===c.nodeType?(b=c.parentNode,b.insertBefore(a,c)):(b=c,b.appendChild(a)),c=c._reactRootContainer,null!==c&&void 0!==c||null!==b.onclick||(b.onclick=uc));else if(4!==d&&(a=a.child,null!==a))for(Oe(a,b,c),a=a.sibling;null!==a;)Oe(a,b,c),a=a.sibling}
function Pe(a,b,c){var d=a.tag,e=5===d||6===d;if(e)a=e?a.stateNode:a.stateNode.instance,b?c.insertBefore(a,b):c.appendChild(a);else if(4!==d&&(a=a.child,null!==a))for(Pe(a,b,c),a=a.sibling;null!==a;)Pe(a,b,c),a=a.sibling}function Dh(a,b,c){for(var d=b,e=!1,f,g;;){if(!e){e=d.return;a:for(;;){if(null===e)throw Error(k(160));f=e.stateNode;switch(e.tag){case 5:g=!1;break a;case 3:f=f.containerInfo;g=!0;break a;case 4:f=f.containerInfo;g=!0;break a}e=e.return}e=!0}if(5===d.tag||6===d.tag){a:for(var h=
a,m=d,n=c,l=m;;)if(Ch(h,l,n),null!==l.child&&4!==l.tag)l.child.return=l,l=l.child;else{if(l===m)break a;for(;null===l.sibling;){if(null===l.return||l.return===m)break a;l=l.return}l.sibling.return=l.return;l=l.sibling}g?(h=f,m=d.stateNode,8===h.nodeType?h.parentNode.removeChild(m):h.removeChild(m)):f.removeChild(d.stateNode)}else if(4===d.tag){if(null!==d.child){f=d.stateNode.containerInfo;g=!0;d.child.return=d;d=d.child;continue}}else if(Ch(a,d,c),null!==d.child){d.child.return=d;d=d.child;continue}if(d===
b)break;for(;null===d.sibling;){if(null===d.return||d.return===b)return;d=d.return;4===d.tag&&(e=!1)}d.sibling.return=d.return;d=d.sibling}}function Qe(a,b){switch(b.tag){case 0:case 11:case 14:case 15:case 22:Ah(3,b);return;case 1:return;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps,e=null!==a?a.memoizedProps:d;a=b.type;var f=b.updateQueue;b.updateQueue=null;if(null!==f){c[vc]=d;"input"===a&&"radio"===d.type&&null!=d.name&&If(c,d);Vd(a,e);b=Vd(a,d);for(e=0;e<f.length;e+=2){var g=f[e],
h=f[e+1];"style"===g?gg(c,h):"dangerouslySetInnerHTML"===g?xh(c,h):"children"===g?Wb(c,h):xd(c,g,h,b)}switch(a){case "input":Dd(c,d);break;case "textarea":Lf(c,d);break;case "select":b=c._wrapperState.wasMultiple,c._wrapperState.wasMultiple=!!d.multiple,a=d.value,null!=a?hb(c,!!d.multiple,a,!1):b!==!!d.multiple&&(null!=d.defaultValue?hb(c,!!d.multiple,d.defaultValue,!0):hb(c,!!d.multiple,d.multiple?[]:"",!1))}}}return;case 6:if(null===b.stateNode)throw Error(k(162));b.stateNode.nodeValue=b.memoizedProps;
return;case 3:b=b.stateNode;b.hydrate&&(b.hydrate=!1,bg(b.containerInfo));return;case 12:return;case 13:c=b;null===b.memoizedState?d=!1:(d=!0,c=b.child,Re=Y());if(null!==c)a:for(a=c;;){if(5===a.tag)f=a.stateNode,d?(f=f.style,"function"===typeof f.setProperty?f.setProperty("display","none","important"):f.display="none"):(f=a.stateNode,e=a.memoizedProps.style,e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null,f.style.display=fg("display",e));else if(6===a.tag)a.stateNode.nodeValue=d?
"":a.memoizedProps;else if(13===a.tag&&null!==a.memoizedState&&null===a.memoizedState.dehydrated){f=a.child.sibling;f.return=a;a=f;continue}else if(null!==a.child){a.child.return=a;a=a.child;continue}if(a===c)break;for(;null===a.sibling;){if(null===a.return||a.return===c)break a;a=a.return}a.sibling.return=a.return;a=a.sibling}Hh(b);return;case 19:Hh(b);return;case 17:return}throw Error(k(163));}function Hh(a){var b=a.updateQueue;if(null!==b){a.updateQueue=null;var c=a.stateNode;null===c&&(c=a.stateNode=
new pj);b.forEach(function(b){var d=qj.bind(null,a,b);c.has(b)||(c.add(b),b.then(d,d))})}}function Ih(a,b,c){c=Ea(c,null);c.tag=3;c.payload={element:null};var d=b.value;c.callback=function(){cd||(cd=!0,Se=d);Me(a,b)};return c}function Jh(a,b,c){c=Ea(c,null);c.tag=3;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){Me(a,b);return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&
(null===La?La=new Set([this]):La.add(this),Me(a,b));var c=b.stack;this.componentDidCatch(b.value,{componentStack:null!==c?c:""})});return c}function ka(){return(p&(ca|ma))!==H?1073741821-(Y()/10|0):0!==dd?dd:dd=1073741821-(Y()/10|0)}function Va(a,b,c){b=b.mode;if(0===(b&2))return 1073741823;var d=Cc();if(0===(b&4))return 99===d?1073741823:1073741822;if((p&ca)!==H)return P;if(null!==c)a=Fc(a,c.timeoutMs|0||5E3,250);else switch(d){case 99:a=1073741823;break;case 98:a=Fc(a,150,100);break;case 97:case 96:a=
Fc(a,5E3,250);break;case 95:a=2;break;default:throw Error(k(326));}null!==U&&a===P&&--a;return a}function ed(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}null!==e&&
(U===e&&(Kc(b),F===bd&&Ya(e,P)),yh(e,b));return e}function fd(a){var b=a.lastExpiredTime;if(0!==b)return b;b=a.firstPendingTime;if(!Kh(a,b))return b;var c=a.lastPingedTime;a=a.nextKnownPendingLevel;a=c>a?c:a;return 2>=a&&b!==a?0:a}function V(a){if(0!==a.lastExpiredTime)a.callbackExpirationTime=1073741823,a.callbackPriority=99,a.callbackNode=Og(Te.bind(null,a));else{var b=fd(a),c=a.callbackNode;if(0===b)null!==c&&(a.callbackNode=null,a.callbackExpirationTime=0,a.callbackPriority=90);else{var d=ka();
1073741823===b?d=99:1===b||2===b?d=95:(d=10*(1073741821-b)-10*(1073741821-d),d=0>=d?99:250>=d?98:5250>=d?97:95);if(null!==c){var e=a.callbackPriority;if(a.callbackExpirationTime===b&&e>=d)return;c!==Qg&&Rg(c)}a.callbackExpirationTime=b;a.callbackPriority=d;b=1073741823===b?Og(Te.bind(null,a)):Ng(d,Lh.bind(null,a),{timeout:10*(1073741821-b)-Y()});a.callbackNode=b}}}function Lh(a,b){dd=0;if(b)return b=ka(),Ue(a,b),V(a),null;var c=fd(a);if(0!==c){b=a.callbackNode;if((p&(ca|ma))!==H)throw Error(k(327));
xb();a===U&&c===P||$a(a,c);if(null!==t){var d=p;p|=ca;var e=Mh();do try{rj();break}catch(h){Nh(a,h)}while(1);le();p=d;gd.current=e;if(F===hd)throw b=id,$a(a,c),Ya(a,c),V(a),b;if(null===t)switch(e=a.finishedWork=a.current.alternate,a.finishedExpirationTime=c,d=F,U=null,d){case Xa:case hd:throw Error(k(345));case Oh:Ue(a,2<c?2:c);break;case ad:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(1073741823===ta&&(e=Re+Ph-Y(),10<e)){if(jd){var f=a.lastPingedTime;if(0===f||f>=c){a.lastPingedTime=
c;$a(a,c);break}}f=fd(a);if(0!==f&&f!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}a.timeoutHandle=We(ab.bind(null,a),e);break}ab(a);break;case bd:Ya(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ve(e));if(jd&&(e=a.lastPingedTime,0===e||e>=c)){a.lastPingedTime=c;$a(a,c);break}e=fd(a);if(0!==e&&e!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}1073741823!==Yb?d=10*(1073741821-Yb)-Y():1073741823===ta?d=0:(d=10*(1073741821-ta)-5E3,e=Y(),c=10*(1073741821-c)-e,d=e-d,0>d&&(d=0),d=
(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*sj(d/1960))-d,c<d&&(d=c));if(10<d){a.timeoutHandle=We(ab.bind(null,a),d);break}ab(a);break;case Xe:if(1073741823!==ta&&null!==kd){f=ta;var g=kd;d=g.busyMinDurationMs|0;0>=d?d=0:(e=g.busyDelayMs|0,f=Y()-(10*(1073741821-f)-(g.timeoutMs|0||5E3)),d=f<=e?0:e+d-f);if(10<d){Ya(a,c);a.timeoutHandle=We(ab.bind(null,a),d);break}}ab(a);break;default:throw Error(k(329));}V(a);if(a.callbackNode===b)return Lh.bind(null,a)}}return null}function Te(a){var b=
a.lastExpiredTime;b=0!==b?b:1073741823;if((p&(ca|ma))!==H)throw Error(k(327));xb();a===U&&b===P||$a(a,b);if(null!==t){var c=p;p|=ca;var d=Mh();do try{tj();break}catch(e){Nh(a,e)}while(1);le();p=c;gd.current=d;if(F===hd)throw c=id,$a(a,b),Ya(a,b),V(a),c;if(null!==t)throw Error(k(261));a.finishedWork=a.current.alternate;a.finishedExpirationTime=b;U=null;ab(a);V(a)}return null}function uj(){if(null!==bb){var a=bb;bb=null;a.forEach(function(a,c){Ue(c,a);V(c)});ha()}}function Qh(a,b){var c=p;p|=1;try{return a(b)}finally{p=
c,p===H&&ha()}}function Rh(a,b){var c=p;p&=-2;p|=Ye;try{return a(b)}finally{p=c,p===H&&ha()}}function $a(a,b){a.finishedWork=null;a.finishedExpirationTime=0;var c=a.timeoutHandle;-1!==c&&(a.timeoutHandle=-1,vj(c));if(null!==t)for(c=t.return;null!==c;){var d=c;switch(d.tag){case 1:d=d.type.childContextTypes;null!==d&&void 0!==d&&(q(G),q(B));break;case 3:tb();q(G);q(B);break;case 5:te(d);break;case 4:tb();break;case 13:q(D);break;case 19:q(D);break;case 10:me(d)}c=c.return}U=a;t=Sa(a.current,null);
P=b;F=Xa;id=null;Yb=ta=1073741823;kd=null;Xb=0;jd=!1}function Nh(a,b){do{try{le();Sc.current=Tc;if(Uc)for(var c=z.memoizedState;null!==c;){var d=c.queue;null!==d&&(d.pending=null);c=c.next}Ia=0;J=K=z=null;Uc=!1;if(null===t||null===t.return)return F=hd,id=b,t=null;a:{var e=a,f=t.return,g=t,h=b;b=P;g.effectTag|=2048;g.firstEffect=g.lastEffect=null;if(null!==h&&"object"===typeof h&&"function"===typeof h.then){var m=h;if(0===(g.mode&2)){var n=g.alternate;n?(g.updateQueue=n.updateQueue,g.memoizedState=
n.memoizedState,g.expirationTime=n.expirationTime):(g.updateQueue=null,g.memoizedState=null)}var l=0!==(D.current&1),k=f;do{var p;if(p=13===k.tag){var q=k.memoizedState;if(null!==q)p=null!==q.dehydrated?!0:!1;else{var w=k.memoizedProps;p=void 0===w.fallback?!1:!0!==w.unstable_avoidThisFallback?!0:l?!1:!0}}if(p){var y=k.updateQueue;if(null===y){var r=new Set;r.add(m);k.updateQueue=r}else y.add(m);if(0===(k.mode&2)){k.effectTag|=64;g.effectTag&=-2981;if(1===g.tag)if(null===g.alternate)g.tag=17;else{var O=
Ea(1073741823,null);O.tag=Jc;Fa(g,O)}g.expirationTime=1073741823;break a}h=void 0;g=b;var v=e.pingCache;null===v?(v=e.pingCache=new wj,h=new Set,v.set(m,h)):(h=v.get(m),void 0===h&&(h=new Set,v.set(m,h)));if(!h.has(g)){h.add(g);var x=xj.bind(null,e,m,g);m.then(x,x)}k.effectTag|=4096;k.expirationTime=b;break a}k=k.return}while(null!==k);h=Error((na(g.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
Bd(g))}F!==Xe&&(F=Oh);h=Le(h,g);k=f;do{switch(k.tag){case 3:m=h;k.effectTag|=4096;k.expirationTime=b;var A=Ih(k,m,b);Ug(k,A);break a;case 1:m=h;var u=k.type,B=k.stateNode;if(0===(k.effectTag&64)&&("function"===typeof u.getDerivedStateFromError||null!==B&&"function"===typeof B.componentDidCatch&&(null===La||!La.has(B)))){k.effectTag|=4096;k.expirationTime=b;var H=Jh(k,m,b);Ug(k,H);break a}}k=k.return}while(null!==k)}t=Sh(t)}catch(cj){b=cj;continue}break}while(1)}function Mh(a){a=gd.current;gd.current=
Tc;return null===a?Tc:a}function Vg(a,b){a<ta&&2<a&&(ta=a);null!==b&&a<Yb&&2<a&&(Yb=a,kd=b)}function Kc(a){a>Xb&&(Xb=a)}function tj(){for(;null!==t;)t=Th(t)}function rj(){for(;null!==t&&!yj();)t=Th(t)}function Th(a){var b=zj(a.alternate,a,P);a.memoizedProps=a.pendingProps;null===b&&(b=Sh(a));Uh.current=null;return b}function Sh(a){t=a;do{var b=t.alternate;a=t.return;if(0===(t.effectTag&2048)){b=hj(b,t,P);if(1===P||1!==t.childExpirationTime){for(var c=0,d=t.child;null!==d;){var e=d.expirationTime,
f=d.childExpirationTime;e>c&&(c=e);f>c&&(c=f);d=d.sibling}t.childExpirationTime=c}if(null!==b)return b;null!==a&&0===(a.effectTag&2048)&&(null===a.firstEffect&&(a.firstEffect=t.firstEffect),null!==t.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=t.firstEffect),a.lastEffect=t.lastEffect),1<t.effectTag&&(null!==a.lastEffect?a.lastEffect.nextEffect=t:a.firstEffect=t,a.lastEffect=t))}else{b=lj(t);if(null!==b)return b.effectTag&=2047,b;null!==a&&(a.firstEffect=a.lastEffect=null,a.effectTag|=
2048)}b=t.sibling;if(null!==b)return b;t=a}while(null!==t);F===Xa&&(F=Xe);return null}function Ve(a){var b=a.expirationTime;a=a.childExpirationTime;return b>a?b:a}function ab(a){var b=Cc();Da(99,Aj.bind(null,a,b));return null}function Aj(a,b){do xb();while(null!==Zb);if((p&(ca|ma))!==H)throw Error(k(327));var c=a.finishedWork,d=a.finishedExpirationTime;if(null===c)return null;a.finishedWork=null;a.finishedExpirationTime=0;if(c===a.current)throw Error(k(177));a.callbackNode=null;a.callbackExpirationTime=
0;a.callbackPriority=90;a.nextKnownPendingLevel=0;var e=Ve(c);a.firstPendingTime=e;d<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:d<=a.firstSuspendedTime&&(a.firstSuspendedTime=d-1);d<=a.lastPingedTime&&(a.lastPingedTime=0);d<=a.lastExpiredTime&&(a.lastExpiredTime=0);a===U&&(t=U=null,P=0);1<c.effectTag?null!==c.lastEffect?(c.lastEffect.nextEffect=c,e=c.firstEffect):e=c:e=c.firstEffect;if(null!==e){var f=p;p|=ma;Uh.current=null;Ze=tc;var g=kg();if(Xd(g)){if("selectionStart"in
g)var h={start:g.selectionStart,end:g.selectionEnd};else a:{h=(h=g.ownerDocument)&&h.defaultView||window;var m=h.getSelection&&h.getSelection();if(m&&0!==m.rangeCount){h=m.anchorNode;var n=m.anchorOffset,q=m.focusNode;m=m.focusOffset;try{h.nodeType,q.nodeType}catch(sb){h=null;break a}var ba=0,w=-1,y=-1,B=0,D=0,r=g,z=null;b:for(;;){for(var v;;){r!==h||0!==n&&3!==r.nodeType||(w=ba+n);r!==q||0!==m&&3!==r.nodeType||(y=ba+m);3===r.nodeType&&(ba+=r.nodeValue.length);if(null===(v=r.firstChild))break;z=r;
r=v}for(;;){if(r===g)break b;z===h&&++B===n&&(w=ba);z===q&&++D===m&&(y=ba);if(null!==(v=r.nextSibling))break;r=z;z=r.parentNode}r=v}h=-1===w||-1===y?null:{start:w,end:y}}else h=null}h=h||{start:0,end:0}}else h=null;$e={activeElementDetached:null,focusedElem:g,selectionRange:h};tc=!1;l=e;do try{Bj()}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=e;do try{for(g=a,h=b;null!==l;){var x=l.effectTag;x&16&&Wb(l.stateNode,"");if(x&128){var A=l.alternate;if(null!==A){var u=
A.ref;null!==u&&("function"===typeof u?u(null):u.current=null)}}switch(x&1038){case 2:Gh(l);l.effectTag&=-3;break;case 6:Gh(l);l.effectTag&=-3;Qe(l.alternate,l);break;case 1024:l.effectTag&=-1025;break;case 1028:l.effectTag&=-1025;Qe(l.alternate,l);break;case 4:Qe(l.alternate,l);break;case 8:n=l,Dh(g,n,h),Eh(n)}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);u=$e;A=kg();x=u.focusedElem;h=u.selectionRange;if(A!==x&&x&&x.ownerDocument&&jg(x.ownerDocument.documentElement,
x)){null!==h&&Xd(x)&&(A=h.start,u=h.end,void 0===u&&(u=A),"selectionStart"in x?(x.selectionStart=A,x.selectionEnd=Math.min(u,x.value.length)):(u=(A=x.ownerDocument||document)&&A.defaultView||window,u.getSelection&&(u=u.getSelection(),n=x.textContent.length,g=Math.min(h.start,n),h=void 0===h.end?g:Math.min(h.end,n),!u.extend&&g>h&&(n=h,h=g,g=n),n=ig(x,g),q=ig(x,h),n&&q&&(1!==u.rangeCount||u.anchorNode!==n.node||u.anchorOffset!==n.offset||u.focusNode!==q.node||u.focusOffset!==q.offset)&&(A=A.createRange(),
A.setStart(n.node,n.offset),u.removeAllRanges(),g>h?(u.addRange(A),u.extend(q.node,q.offset)):(A.setEnd(q.node,q.offset),u.addRange(A))))));A=[];for(u=x;u=u.parentNode;)1===u.nodeType&&A.push({element:u,left:u.scrollLeft,top:u.scrollTop});"function"===typeof x.focus&&x.focus();for(x=0;x<A.length;x++)u=A[x],u.element.scrollLeft=u.left,u.element.scrollTop=u.top}tc=!!Ze;$e=Ze=null;a.current=c;l=e;do try{for(x=a;null!==l;){var F=l.effectTag;F&36&&oj(x,l.alternate,l);if(F&128){A=void 0;var E=l.ref;if(null!==
E){var G=l.stateNode;switch(l.tag){case 5:A=G;break;default:A=G}"function"===typeof E?E(A):E.current=A}}l=l.nextEffect}}catch(sb){if(null===l)throw Error(k(330));Za(l,sb);l=l.nextEffect}while(null!==l);l=null;Cj();p=f}else a.current=c;if(ld)ld=!1,Zb=a,$b=b;else for(l=e;null!==l;)b=l.nextEffect,l.nextEffect=null,l=b;b=a.firstPendingTime;0===b&&(La=null);1073741823===b?a===af?ac++:(ac=0,af=a):ac=0;"function"===typeof bf&&bf(c.stateNode,d);V(a);if(cd)throw cd=!1,a=Se,Se=null,a;if((p&Ye)!==H)return null;
ha();return null}function Bj(){for(;null!==l;){var a=l.effectTag;0!==(a&256)&&nj(l.alternate,l);0===(a&512)||ld||(ld=!0,Ng(97,function(){xb();return null}));l=l.nextEffect}}function xb(){if(90!==$b){var a=97<$b?97:$b;$b=90;return Da(a,Dj)}}function Dj(){if(null===Zb)return!1;var a=Zb;Zb=null;if((p&(ca|ma))!==H)throw Error(k(331));var b=p;p|=ma;for(a=a.current.firstEffect;null!==a;){try{var c=a;if(0!==(c.effectTag&512))switch(c.tag){case 0:case 11:case 15:case 22:Ah(5,c),Bh(5,c)}}catch(d){if(null===
a)throw Error(k(330));Za(a,d)}c=a.nextEffect;a.nextEffect=null;a=c}p=b;ha();return!0}function Vh(a,b,c){b=Le(c,b);b=Ih(a,b,1073741823);Fa(a,b);a=ed(a,1073741823);null!==a&&V(a)}function Za(a,b){if(3===a.tag)Vh(a,a,b);else for(var c=a.return;null!==c;){if(3===c.tag){Vh(c,a,b);break}else if(1===c.tag){var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===La||!La.has(d))){a=Le(b,a);a=Jh(c,a,1073741823);Fa(c,a);c=ed(c,1073741823);null!==
c&&V(c);break}}c=c.return}}function xj(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);U===a&&P===c?F===bd||F===ad&&1073741823===ta&&Y()-Re<Ph?$a(a,P):jd=!0:Kh(a,c)&&(b=a.lastPingedTime,0!==b&&b<c||(a.lastPingedTime=c,V(a)))}function qj(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=0;0===b&&(b=ka(),b=Va(b,a,null));a=ed(a,b);null!==a&&V(a)}function Ej(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=
b.inject(a);bf=function(a,e){try{b.onCommitFiberRoot(c,a,void 0,64===(a.current.effectTag&64))}catch(f){}};Ne=function(a){try{b.onCommitFiberUnmount(c,a)}catch(e){}}}catch(d){}return!0}function Fj(a,b,c,d){this.tag=a;this.key=c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=
null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Ge(a){a=a.prototype;return!(!a||!a.isReactComponent)}function Gj(a){if("function"===typeof a)return Ge(a)?1:0;if(void 0!==a&&null!==a){a=a.$$typeof;if(a===zd)return 11;if(a===Ad)return 14}return 2}function Sa(a,b){var c=a.alternate;null===c?(c=la(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=
null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;b=a.dependencies;c.dependencies=null===b?null:{expirationTime:b.expirationTime,firstContext:b.firstContext,responders:b.responders};c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function Oc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Ge(a)&&(g=1);else if("string"===typeof a)g=
5;else a:switch(a){case Ma:return Ha(c.children,e,f,b);case Hj:g=8;e|=7;break;case Af:g=8;e|=1;break;case kc:return a=la(12,c,b,e|8),a.elementType=kc,a.type=kc,a.expirationTime=f,a;case lc:return a=la(13,c,b,e),a.type=lc,a.elementType=lc,a.expirationTime=f,a;case yd:return a=la(19,c,b,e),a.elementType=yd,a.expirationTime=f,a;default:if("object"===typeof a&&null!==a)switch(a.$$typeof){case Cf:g=10;break a;case Bf:g=9;break a;case zd:g=11;break a;case Ad:g=14;break a;case Ef:g=16;d=null;break a;case Df:g=
22;break a}throw Error(k(130,null==a?a:typeof a,""));}b=la(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function Ha(a,b,c,d){a=la(7,a,d,b);a.expirationTime=c;return a}function qe(a,b,c){a=la(6,a,null,b);a.expirationTime=c;return a}function re(a,b,c){b=la(4,null!==a.children?a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Ij(a,b,c){this.tag=b;this.current=null;this.containerInfo=
a;this.pingCache=this.pendingChildren=null;this.finishedExpirationTime=0;this.finishedWork=null;this.timeoutHandle=-1;this.pendingContext=this.context=null;this.hydrate=c;this.callbackNode=null;this.callbackPriority=90;this.lastExpiredTime=this.lastPingedTime=this.nextKnownPendingLevel=this.lastSuspendedTime=this.firstSuspendedTime=this.firstPendingTime=0}function Kh(a,b){var c=a.firstSuspendedTime;a=a.lastSuspendedTime;return 0!==c&&c>=b&&a<=b}function Ya(a,b){var c=a.firstSuspendedTime,d=a.lastSuspendedTime;
c<b&&(a.firstSuspendedTime=b);if(d>b||0===c)a.lastSuspendedTime=b;b<=a.lastPingedTime&&(a.lastPingedTime=0);b<=a.lastExpiredTime&&(a.lastExpiredTime=0)}function yh(a,b){b>a.firstPendingTime&&(a.firstPendingTime=b);var c=a.firstSuspendedTime;0!==c&&(b>=c?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:b>=a.lastSuspendedTime&&(a.lastSuspendedTime=b+1),b>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=b))}function Ue(a,b){var c=a.lastExpiredTime;if(0===c||c>b)a.lastExpiredTime=b}
function md(a,b,c,d){var e=b.current,f=ka(),g=Vb.suspense;f=Va(f,e,g);a:if(c){c=c._reactInternalFiber;b:{if(Na(c)!==c||1!==c.tag)throw Error(k(170));var h=c;do{switch(h.tag){case 3:h=h.stateNode.context;break b;case 1:if(N(h.type)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}}h=h.return}while(null!==h);throw Error(k(171));}if(1===c.tag){var m=c.type;if(N(m)){c=Gg(c,m,h);break a}}c=h}else c=Ca;null===b.context?b.context=c:b.pendingContext=c;b=Ea(f,g);b.payload={element:a};d=void 0===
d?null:d;null!==d&&(b.callback=d);Fa(e,b);Ja(e,f);return f}function cf(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Wh(a,b){a=a.memoizedState;null!==a&&null!==a.dehydrated&&a.retryTime<b&&(a.retryTime=b)}function df(a,b){Wh(a,b);(a=a.alternate)&&Wh(a,b)}function ef(a,b,c){c=null!=c&&!0===c.hydrate;var d=new Ij(a,b,c),e=la(3,null,null,2===b?7:1===b?3:0);d.current=e;e.stateNode=d;ne(e);a[Lb]=d.current;c&&0!==b&&
xi(a,9===a.nodeType?a:a.ownerDocument);this._internalRoot=d}function bc(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||" react-mount-point-unstable "!==a.nodeValue))}function Jj(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new ef(a,0,b?{hydrate:!0}:void 0)}function nd(a,b,c,d,e){var f=c._reactRootContainer;if(f){var g=f._internalRoot;
if("function"===typeof e){var h=e;e=function(){var a=cf(g);h.call(a)}}md(b,g,a,e)}else{f=c._reactRootContainer=Jj(c,d);g=f._internalRoot;if("function"===typeof e){var m=e;e=function(){var a=cf(g);m.call(a)}}Rh(function(){md(b,g,a,e)})}return cf(g)}function Kj(a,b,c){var d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:gb,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Xh(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;
if(!bc(b))throw Error(k(200));return Kj(a,b,null,c)}if(!ea)throw Error(k(227));var ki=function(a,b,c,d,e,f,g,h,m){var n=Array.prototype.slice.call(arguments,3);try{b.apply(c,n)}catch(C){this.onError(C)}},yb=!1,gc=null,hc=!1,pd=null,li={onError:function(a){yb=!0;gc=a}},td=null,rf=null,mf=null,ic=null,cb={},jc=[],qd={},db={},rd={},wa=!("undefined"===typeof window||"undefined"===typeof window.document||"undefined"===typeof window.document.createElement),M=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign,
sd=null,eb=null,fb=null,ee=function(a,b){return a(b)},eg=function(a,b,c,d,e){return a(b,c,d,e)},vd=function(){},vf=ee,Oa=!1,wd=!1,Z=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Lj=Z.unstable_cancelCallback,ff=Z.unstable_now,$f=Z.unstable_scheduleCallback,Mj=Z.unstable_shouldYield,Yh=Z.unstable_requestPaint,Pd=Z.unstable_runWithPriority,Nj=Z.unstable_getCurrentPriorityLevel,Oj=Z.unstable_ImmediatePriority,Zh=Z.unstable_UserBlockingPriority,ag=Z.unstable_NormalPriority,Pj=Z.unstable_LowPriority,
Qj=Z.unstable_IdlePriority,oi=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,wf=Object.prototype.hasOwnProperty,yf={},xf={},E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){E[a]=
new L(a,0,!1,a,null,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];E[b]=new L(b,1,!1,a[1],null,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){E[a]=new L(a,2,!1,a.toLowerCase(),null,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){E[a]=new L(a,2,!1,a,null,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){E[a]=
new L(a,3,!1,a.toLowerCase(),null,!1)});["checked","multiple","muted","selected"].forEach(function(a){E[a]=new L(a,3,!0,a,null,!1)});["capture","download"].forEach(function(a){E[a]=new L(a,4,!1,a,null,!1)});["cols","rows","size","span"].forEach(function(a){E[a]=new L(a,6,!1,a,null,!1)});["rowSpan","start"].forEach(function(a){E[a]=new L(a,5,!1,a.toLowerCase(),null,!1)});var gf=/[\-:]([a-z])/g,hf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
a.replace(gf,hf);E[b]=new L(b,1,!1,a,null,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/1999/xlink",!1)});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(gf,hf);E[b]=new L(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1)});["tabIndex","crossOrigin"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!1)});E.xlinkHref=new L("xlinkHref",1,
!1,"xlink:href","http://www.w3.org/1999/xlink",!0);["src","href","action","formAction"].forEach(function(a){E[a]=new L(a,1,!1,a.toLowerCase(),null,!0)});var da=ea.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;da.hasOwnProperty("ReactCurrentDispatcher")||(da.ReactCurrentDispatcher={current:null});da.hasOwnProperty("ReactCurrentBatchConfig")||(da.ReactCurrentBatchConfig={suspense:null});var si=/^(.*)[\\\/]/,Q="function"===typeof Symbol&&Symbol.for,Pc=Q?Symbol.for("react.element"):60103,gb=Q?Symbol.for("react.portal"):
60106,Ma=Q?Symbol.for("react.fragment"):60107,Af=Q?Symbol.for("react.strict_mode"):60108,kc=Q?Symbol.for("react.profiler"):60114,Cf=Q?Symbol.for("react.provider"):60109,Bf=Q?Symbol.for("react.context"):60110,Hj=Q?Symbol.for("react.concurrent_mode"):60111,zd=Q?Symbol.for("react.forward_ref"):60112,lc=Q?Symbol.for("react.suspense"):60113,yd=Q?Symbol.for("react.suspense_list"):60120,Ad=Q?Symbol.for("react.memo"):60115,Ef=Q?Symbol.for("react.lazy"):60116,Df=Q?Symbol.for("react.block"):60121,zf="function"===
typeof Symbol&&Symbol.iterator,od,xh=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{od=od||document.createElement("div");od.innerHTML="<svg>"+b.valueOf().toString()+"</svg>";for(b=od.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),Wb=function(a,
b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},ib={animationend:nc("Animation","AnimationEnd"),animationiteration:nc("Animation","AnimationIteration"),animationstart:nc("Animation","AnimationStart"),transitionend:nc("Transition","TransitionEnd")},Id={},Of={};wa&&(Of=document.createElement("div").style,"AnimationEvent"in window||(delete ib.animationend.animation,delete ib.animationiteration.animation,delete ib.animationstart.animation),"TransitionEvent"in
window||delete ib.transitionend.transition);var $h=oc("animationend"),ai=oc("animationiteration"),bi=oc("animationstart"),ci=oc("transitionend"),Db="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Pf=new ("function"===typeof WeakMap?WeakMap:Map),Ab=null,wi=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;
if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)lf(a,b[d],c[d]);else b&&lf(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},qc=[],Rd=!1,fa=[],xa=null,ya=null,za=null,Eb=new Map,Fb=new Map,Jb=[],Nd="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput close cancel copy cut paste click change contextmenu reset submit".split(" "),
yi="focus blur dragenter dragleave mouseover mouseout pointerover pointerout gotpointercapture lostpointercapture".split(" "),dg={},cg=new Map,Td=new Map,Rj=["abort","abort",$h,"animationEnd",ai,"animationIteration",bi,"animationStart","canplay","canPlay","canplaythrough","canPlayThrough","durationchange","durationChange","emptied","emptied","encrypted","encrypted","ended","ended","error","error","gotpointercapture","gotPointerCapture","load","load","loadeddata","loadedData","loadedmetadata","loadedMetadata",
"loadstart","loadStart","lostpointercapture","lostPointerCapture","playing","playing","progress","progress","seeking","seeking","stalled","stalled","suspend","suspend","timeupdate","timeUpdate",ci,"transitionEnd","waiting","waiting"];Sd("blur blur cancel cancel click click close close contextmenu contextMenu copy copy cut cut auxclick auxClick dblclick doubleClick dragend dragEnd dragstart dragStart drop drop focus focus input input invalid invalid keydown keyDown keypress keyPress keyup keyUp mousedown mouseDown mouseup mouseUp paste paste pause pause play play pointercancel pointerCancel pointerdown pointerDown pointerup pointerUp ratechange rateChange reset reset seeked seeked submit submit touchcancel touchCancel touchend touchEnd touchstart touchStart volumechange volumeChange".split(" "),
0);Sd("drag drag dragenter dragEnter dragexit dragExit dragleave dragLeave dragover dragOver mousemove mouseMove mouseout mouseOut mouseover mouseOver pointermove pointerMove pointerout pointerOut pointerover pointerOver scroll scroll toggle toggle touchmove touchMove wheel wheel".split(" "),1);Sd(Rj,2);(function(a,b){for(var c=0;c<a.length;c++)Td.set(a[c],b)})("change selectionchange textInput compositionstart compositionend compositionupdate".split(" "),0);var Hi=Zh,Gi=Pd,tc=!0,Kb={animationIterationCount:!0,
borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,
strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Sj=["Webkit","ms","Moz","O"];Object.keys(Kb).forEach(function(a){Sj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);Kb[b]=Kb[a]})});var Ii=M({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),ng="$",og="/$",$d="$?",Zd="$!",Ze=null,$e=null,We="function"===typeof setTimeout?setTimeout:void 0,vj="function"===
typeof clearTimeout?clearTimeout:void 0,jf=Math.random().toString(36).slice(2),Aa="__reactInternalInstance$"+jf,vc="__reactEventHandlers$"+jf,Lb="__reactContainere$"+jf,Ba=null,ce=null,wc=null;M(R.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=xc)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==
typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=xc)},persist:function(){this.isPersistent=xc},isPersistent:yc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=yc;this._dispatchInstances=this._dispatchListeners=null}});R.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||
Date.now()},defaultPrevented:null,isTrusted:null};R.extend=function(a){function b(){return c.apply(this,arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;M(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=M({},c.Interface,a);b.extend=c.extend;sg(b);return b};sg(R);var Tj=R.extend({data:null}),Uj=R.extend({data:null}),Ni=[9,13,27,32],de=wa&&"CompositionEvent"in window,cc=null;wa&&"documentMode"in document&&(cc=document.documentMode);var Vj=wa&&"TextEvent"in window&&
!cc,xg=wa&&(!de||cc&&8<cc&&11>=cc),wg=String.fromCharCode(32),ua={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},
dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},vg=!1,mb=!1,Wj={eventTypes:ua,extractEvents:function(a,b,c,d,e){var f;if(de)b:{switch(a){case "compositionstart":var g=ua.compositionStart;break b;case "compositionend":g=ua.compositionEnd;break b;case "compositionupdate":g=
ua.compositionUpdate;break b}g=void 0}else mb?tg(a,c)&&(g=ua.compositionEnd):"keydown"===a&&229===c.keyCode&&(g=ua.compositionStart);g?(xg&&"ko"!==c.locale&&(mb||g!==ua.compositionStart?g===ua.compositionEnd&&mb&&(f=rg()):(Ba=d,ce="value"in Ba?Ba.value:Ba.textContent,mb=!0)),e=Tj.getPooled(g,b,c,d),f?e.data=f:(f=ug(c),null!==f&&(e.data=f)),lb(e),f=e):f=null;(a=Vj?Oi(a,c):Pi(a,c))?(b=Uj.getPooled(ua.beforeInput,b,c,d),b.data=a,lb(b)):b=null;return null===f?b:null===b?f:[f,b]}},Qi={color:!0,date:!0,
datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ag={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},Mb=null,Nb=null,kf=!1;wa&&(kf=Tf("input")&&(!document.documentMode||9<document.documentMode));var Xj={eventTypes:Ag,_isInputEventSupported:kf,extractEvents:function(a,b,c,d,e){e=b?Pa(b):window;var f=
e.nodeName&&e.nodeName.toLowerCase();if("select"===f||"input"===f&&"file"===e.type)var g=Si;else if(yg(e))if(kf)g=Wi;else{g=Ui;var h=Ti}else(f=e.nodeName)&&"input"===f.toLowerCase()&&("checkbox"===e.type||"radio"===e.type)&&(g=Vi);if(g&&(g=g(a,b)))return zg(g,c,d);h&&h(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&Ed(e,"number",e.value)}},dc=R.extend({view:null,detail:null}),Yi={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},di=0,ei=0,fi=!1,gi=!1,ec=dc.extend({screenX:null,
screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:fe,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=di;di=a.screenX;return fi?"mousemove"===a.type?a.screenX-b:0:(fi=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=ei;ei=a.screenY;return gi?"mousemove"===
a.type?a.screenY-b:0:(gi=!0,0)}}),hi=ec.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),fc={mouseEnter:{registrationName:"onMouseEnter",dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout",
"pointerover"]}},Yj={eventTypes:fc,extractEvents:function(a,b,c,d,e){var f="mouseover"===a||"pointerover"===a,g="mouseout"===a||"pointerout"===a;if(f&&0===(e&32)&&(c.relatedTarget||c.fromElement)||!g&&!f)return null;f=d.window===d?d:(f=d.ownerDocument)?f.defaultView||f.parentWindow:window;if(g){if(g=b,b=(b=c.relatedTarget||c.toElement)?Bb(b):null,null!==b){var h=Na(b);if(b!==h||5!==b.tag&&6!==b.tag)b=null}}else g=null;if(g===b)return null;if("mouseout"===a||"mouseover"===a){var m=ec;var n=fc.mouseLeave;
var l=fc.mouseEnter;var k="mouse"}else if("pointerout"===a||"pointerover"===a)m=hi,n=fc.pointerLeave,l=fc.pointerEnter,k="pointer";a=null==g?f:Pa(g);f=null==b?f:Pa(b);n=m.getPooled(n,g,c,d);n.type=k+"leave";n.target=a;n.relatedTarget=f;c=m.getPooled(l,b,c,d);c.type=k+"enter";c.target=f;c.relatedTarget=a;d=g;k=b;if(d&&k)a:{m=d;l=k;g=0;for(a=m;a;a=pa(a))g++;a=0;for(b=l;b;b=pa(b))a++;for(;0<g-a;)m=pa(m),g--;for(;0<a-g;)l=pa(l),a--;for(;g--;){if(m===l||m===l.alternate)break a;m=pa(m);l=pa(l)}m=null}else m=
null;l=m;for(m=[];d&&d!==l;){g=d.alternate;if(null!==g&&g===l)break;m.push(d);d=pa(d)}for(d=[];k&&k!==l;){g=k.alternate;if(null!==g&&g===l)break;d.push(k);k=pa(k)}for(k=0;k<m.length;k++)be(m[k],"bubbled",n);for(k=d.length;0<k--;)be(d[k],"captured",c);return 0===(e&64)?[n]:[n,c]}},Qa="function"===typeof Object.is?Object.is:Zi,$i=Object.prototype.hasOwnProperty,Zj=wa&&"documentMode"in document&&11>=document.documentMode,Eg={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},
dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},nb=null,he=null,Pb=null,ge=!1,ak={eventTypes:Eg,extractEvents:function(a,b,c,d,e,f){e=f||(d.window===d?d.document:9===d.nodeType?d:d.ownerDocument);if(!(f=!e)){a:{e=Jd(e);f=rd.onSelect;for(var g=0;g<f.length;g++)if(!e.has(f[g])){e=!1;break a}e=!0}f=!e}if(f)return null;e=b?Pa(b):window;switch(a){case "focus":if(yg(e)||"true"===e.contentEditable)nb=e,he=b,Pb=null;break;case "blur":Pb=he=nb=null;
break;case "mousedown":ge=!0;break;case "contextmenu":case "mouseup":case "dragend":return ge=!1,Dg(c,d);case "selectionchange":if(Zj)break;case "keydown":case "keyup":return Dg(c,d)}return null}},bk=R.extend({animationName:null,elapsedTime:null,pseudoElement:null}),ck=R.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),dk=dc.extend({relatedTarget:null}),ek={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",
Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},fk={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",
224:"Meta"},gk=dc.extend({key:function(a){if(a.key){var b=ek[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=Ac(a),13===a?"Enter":String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?fk[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:fe,charCode:function(a){return"keypress"===a.type?Ac(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===
a.type?Ac(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),hk=ec.extend({dataTransfer:null}),ik=dc.extend({touches:null,targetTouches:null,changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:fe}),jk=R.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),kk=ec.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?
-a.wheelDelta:0},deltaZ:null,deltaMode:null}),lk={eventTypes:dg,extractEvents:function(a,b,c,d,e){e=cg.get(a);if(!e)return null;switch(a){case "keypress":if(0===Ac(c))return null;case "keydown":case "keyup":a=gk;break;case "blur":case "focus":a=dk;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=ec;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=
hk;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=ik;break;case $h:case ai:case bi:a=bk;break;case ci:a=jk;break;case "scroll":a=dc;break;case "wheel":a=kk;break;case "copy":case "cut":case "paste":a=ck;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=hi;break;default:a=R}b=a.getPooled(e,b,c,d);lb(b);return b}};(function(a){if(ic)throw Error(k(101));
ic=Array.prototype.slice.call(a);nf()})("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){td=a;rf=b;mf=c})(ae,Hb,Pa);pf({SimpleEventPlugin:lk,EnterLeaveEventPlugin:Yj,ChangeEventPlugin:Xj,SelectEventPlugin:ak,BeforeInputEventPlugin:Wj});var ie=[],ob=-1,Ca={},B={current:Ca},G={current:!1},Ra=Ca,bj=Pd,je=$f,Rg=Lj,aj=Nj,Dc=Oj,Ig=Zh,Jg=ag,Kg=Pj,Lg=Qj,Qg={},yj=Mj,Cj=void 0!==Yh?Yh:function(){},qa=null,
Ec=null,ke=!1,ii=ff(),Y=1E4>ii?ff:function(){return ff()-ii},Ic={current:null},Hc=null,qb=null,Gc=null,Tg=0,Jc=2,Ga=!1,Vb=da.ReactCurrentBatchConfig,$g=(new ea.Component).refs,Mc={isMounted:function(a){return(a=a._reactInternalFiber)?Na(a)===a:!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;d=Va(d,a,e);e=Ea(d,e);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka(),e=Vb.suspense;
d=Va(d,a,e);e=Ea(d,e);e.tag=1;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);Fa(a,e);Ja(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka(),d=Vb.suspense;c=Va(c,a,d);d=Ea(c,d);d.tag=Jc;void 0!==b&&null!==b&&(d.callback=b);Fa(a,d);Ja(a,c)}},Qc=Array.isArray,wb=ah(!0),Fe=ah(!1),Sb={},ja={current:Sb},Ub={current:Sb},Tb={current:Sb},D={current:0},Sc=da.ReactCurrentDispatcher,X=da.ReactCurrentBatchConfig,Ia=0,z=null,K=null,J=null,Uc=!1,Tc={readContext:W,useCallback:S,useContext:S,
useEffect:S,useImperativeHandle:S,useLayoutEffect:S,useMemo:S,useReducer:S,useRef:S,useState:S,useDebugValue:S,useResponder:S,useDeferredValue:S,useTransition:S},dj={readContext:W,useCallback:ih,useContext:W,useEffect:eh,useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return ze(4,2,gh.bind(null,b,a),c)},useLayoutEffect:function(a,b){return ze(4,2,a,b)},useMemo:function(a,b){var c=ub();b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=
ub();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={pending:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=ch.bind(null,z,a);return[d.memoizedState,a]},useRef:function(a){var b=ub();a={current:a};return b.memoizedState=a},useState:xe,useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=xe(a),d=c[0],e=c[1];eh(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=
xe(!1),c=b[0];b=b[1];return[ih(Ce.bind(null,b,a),[b,a]),c]}},ej={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Vc,useRef:dh,useState:function(a){return Vc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Vc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Vc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,
b,a),[b,a]),c]}},fj={readContext:W,useCallback:Yc,useContext:W,useEffect:Xc,useImperativeHandle:hh,useLayoutEffect:fh,useMemo:jh,useReducer:Wc,useRef:dh,useState:function(a){return Wc(Ua)},useDebugValue:Be,useResponder:ue,useDeferredValue:function(a,b){var c=Wc(Ua),d=c[0],e=c[1];Xc(function(){var c=X.suspense;X.suspense=void 0===b?null:b;try{e(a)}finally{X.suspense=c}},[a,b]);return d},useTransition:function(a){var b=Wc(Ua),c=b[0];b=b[1];return[Yc(Ce.bind(null,b,a),[b,a]),c]}},ra=null,Ka=null,Wa=
!1,gj=da.ReactCurrentOwner,ia=!1,Je={dehydrated:null,retryTime:0};var jj=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};var wh=function(a){};var ij=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ta(ja.current);a=null;switch(c){case "input":f=
Cd(g,f);d=Cd(g,d);a=[];break;case "option":f=Fd(g,f);d=Fd(g,d);a=[];break;case "select":f=M({},f,{value:void 0});d=M({},d,{value:void 0});a=[];break;case "textarea":f=Gd(g,f);d=Gd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=uc)}Ud(c,d);var h,m;c=null;for(h in f)if(!d.hasOwnProperty(h)&&f.hasOwnProperty(h)&&null!=f[h])if("style"===h)for(m in g=f[h],g)g.hasOwnProperty(m)&&(c||(c={}),c[m]="");else"dangerouslySetInnerHTML"!==h&&"children"!==h&&"suppressContentEditableWarning"!==
h&&"suppressHydrationWarning"!==h&&"autoFocus"!==h&&(db.hasOwnProperty(h)?a||(a=[]):(a=a||[]).push(h,null));for(h in d){var k=d[h];g=null!=f?f[h]:void 0;if(d.hasOwnProperty(h)&&k!==g&&(null!=k||null!=g))if("style"===h)if(g){for(m in g)!g.hasOwnProperty(m)||k&&k.hasOwnProperty(m)||(c||(c={}),c[m]="");for(m in k)k.hasOwnProperty(m)&&g[m]!==k[m]&&(c||(c={}),c[m]=k[m])}else c||(a||(a=[]),a.push(h,c)),c=k;else"dangerouslySetInnerHTML"===h?(k=k?k.__html:void 0,g=g?g.__html:void 0,null!=k&&g!==k&&(a=a||
[]).push(h,k)):"children"===h?g===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(h,""+k):"suppressContentEditableWarning"!==h&&"suppressHydrationWarning"!==h&&(db.hasOwnProperty(h)?(null!=k&&oa(e,h),a||g===k||(a=[])):(a=a||[]).push(h,k))}c&&(a=a||[]).push("style",c);e=a;if(b.updateQueue=e)b.effectTag|=4}};var kj=function(a,b,c,d){c!==d&&(b.effectTag|=4)};var pj="function"===typeof WeakSet?WeakSet:Set,wj="function"===typeof WeakMap?WeakMap:Map,sj=Math.ceil,gd=da.ReactCurrentDispatcher,
Uh=da.ReactCurrentOwner,H=0,Ye=8,ca=16,ma=32,Xa=0,hd=1,Oh=2,ad=3,bd=4,Xe=5,p=H,U=null,t=null,P=0,F=Xa,id=null,ta=1073741823,Yb=1073741823,kd=null,Xb=0,jd=!1,Re=0,Ph=500,l=null,cd=!1,Se=null,La=null,ld=!1,Zb=null,$b=90,bb=null,ac=0,af=null,dd=0,Ja=function(a,b){if(50<ac)throw ac=0,af=null,Error(k(185));a=ed(a,b);if(null!==a){var c=Cc();1073741823===b?(p&Ye)!==H&&(p&(ca|ma))===H?Te(a):(V(a),p===H&&ha()):V(a);(p&4)===H||98!==c&&99!==c||(null===bb?bb=new Map([[a,b]]):(c=bb.get(a),(void 0===c||c>b)&&bb.set(a,
b)))}};var zj=function(a,b,c){var d=b.expirationTime;if(null!==a){var e=b.pendingProps;if(a.memoizedProps!==e||G.current)ia=!0;else{if(d<c){ia=!1;switch(b.tag){case 3:sh(b);Ee();break;case 5:bh(b);if(b.mode&4&&1!==c&&e.hidden)return b.expirationTime=b.childExpirationTime=1,null;break;case 1:N(b.type)&&Bc(b);break;case 4:se(b,b.stateNode.containerInfo);break;case 10:d=b.memoizedProps.value;e=b.type._context;y(Ic,e._currentValue);e._currentValue=d;break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;
if(0!==d&&d>=c)return th(a,b,c);y(D,D.current&1);b=sa(a,b,c);return null!==b?b.sibling:null}y(D,D.current&1);break;case 19:d=b.childExpirationTime>=c;if(0!==(a.effectTag&64)){if(d)return vh(a,b,c);b.effectTag|=64}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null);y(D,D.current);if(!d)return null}return sa(a,b,c)}ia=!1}}else ia=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;e=pb(b,B.current);rb(b,c);e=we(null,
b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(N(d)){var f=!0;Bc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;ne(b);var g=d.getDerivedStateFromProps;"function"===typeof g&&Lc(b,d,g,a);e.updater=Mc;b.stateNode=e;e._reactInternalFiber=b;pe(b,d,a,c);b=Ie(null,b,d,!0,f,c)}else b.tag=0,T(null,b,e,c),b=b.child;return b;case 16:a:{e=b.elementType;null!==a&&(a.alternate=
null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;ri(e);if(1!==e._status)throw e._result;e=e._result;b.type=e;f=b.tag=Gj(e);a=aa(e,a);switch(f){case 0:b=He(null,b,e,a,c);break a;case 1:b=rh(null,b,e,a,c);break a;case 11:b=nh(null,b,e,a,c);break a;case 14:b=oh(null,b,e,aa(e.type,a),d,c);break a}throw Error(k(306,e,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),He(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),rh(a,b,d,e,c);
case 3:sh(b);d=b.updateQueue;if(null===a||null===d)throw Error(k(282));d=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;oe(a,b);Qb(b,d,null,c);d=b.memoizedState.element;if(d===e)Ee(),b=sa(a,b,c);else{if(e=b.stateNode.hydrate)Ka=kb(b.stateNode.containerInfo.firstChild),ra=b,e=Wa=!0;if(e)for(c=Fe(b,null,d,c),b.child=c;c;)c.effectTag=c.effectTag&-3|1024,c=c.sibling;else T(a,b,d,c),Ee();b=b.child}return b;case 5:return bh(b),null===a&&De(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:
null,g=e.children,Yd(d,e)?g=null:null!==f&&Yd(d,f)&&(b.effectTag|=16),qh(a,b),b.mode&4&&1!==c&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(T(a,b,g,c),b=b.child),b;case 6:return null===a&&De(b),null;case 13:return th(a,b,c);case 4:return se(b,b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=wb(b,null,d,c):T(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),nh(a,b,d,e,c);case 7:return T(a,b,b.pendingProps,c),b.child;case 8:return T(a,
b,b.pendingProps.children,c),b.child;case 12:return T(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;var h=b.type._context;y(Ic,h._currentValue);h._currentValue=f;if(null!==g)if(h=g.value,f=Qa(h,f)?0:("function"===typeof d._calculateChangedBits?d._calculateChangedBits(h,f):1073741823)|0,0===f){if(g.children===e.children&&!G.current){b=sa(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var m=h.dependencies;if(null!==
m){g=h.child;for(var l=m.firstContext;null!==l;){if(l.context===d&&0!==(l.observedBits&f)){1===h.tag&&(l=Ea(c,null),l.tag=Jc,Fa(h,l));h.expirationTime<c&&(h.expirationTime=c);l=h.alternate;null!==l&&l.expirationTime<c&&(l.expirationTime=c);Sg(h.return,c);m.expirationTime<c&&(m.expirationTime=c);break}l=l.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=
g}T(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,d=f.children,rb(b,c),e=W(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,T(a,b,d,c),b.child;case 14:return e=b.type,f=aa(e,b.pendingProps),f=aa(e.type,f),oh(a,b,e,f,d,c);case 15:return ph(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:aa(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,N(d)?(a=!0,Bc(b)):a=!1,rb(b,c),Yg(b,d,e),pe(b,d,e,c),Ie(null,
b,d,!0,a,c);case 19:return vh(a,b,c)}throw Error(k(156,b.tag));};var bf=null,Ne=null,la=function(a,b,c,d){return new Fj(a,b,c,d)};ef.prototype.render=function(a){md(a,this._internalRoot,null,null)};ef.prototype.unmount=function(){var a=this._internalRoot,b=a.containerInfo;md(null,a,null,function(){b[Lb]=null})};var Di=function(a){if(13===a.tag){var b=Fc(ka(),150,100);Ja(a,b);df(a,b)}};var Yf=function(a){13===a.tag&&(Ja(a,3),df(a,3))};var Bi=function(a){if(13===a.tag){var b=ka();b=Va(b,a,null);Ja(a,
b);df(a,b)}};sd=function(a,b,c){switch(b){case "input":Dd(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=ae(d);if(!e)throw Error(k(90));Gf(d);Dd(d,e)}}}break;case "textarea":Lf(a,c);break;case "select":b=c.value,null!=b&&hb(a,!!c.multiple,b,!1)}};(function(a,b,c,d){ee=a;eg=b;vd=c;vf=d})(Qh,function(a,b,c,d,e){var f=p;p|=4;
try{return Da(98,a.bind(null,b,c,d,e))}finally{p=f,p===H&&ha()}},function(){(p&(1|ca|ma))===H&&(uj(),xb())},function(a,b){var c=p;p|=2;try{return a(b)}finally{p=c,p===H&&ha()}});var mk={Events:[Hb,Pa,ae,pf,qd,lb,function(a){Kd(a,Ki)},sf,tf,sc,pc,xb,{current:!1}]};(function(a){var b=a.findFiberByHostInstance;return Ej(M({},a,{overrideHookState:null,overrideProps:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:da.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=Sf(a);
return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null}))})({findFiberByHostInstance:Bb,bundleType:0,version:"16.13.1",rendererPackageName:"react-dom"});I.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=mk;I.createPortal=Xh;I.findDOMNode=function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;if(void 0===
b){if("function"===typeof a.render)throw Error(k(188));throw Error(k(268,Object.keys(a)));}a=Sf(b);a=null===a?null:a.stateNode;return a};I.flushSync=function(a,b){if((p&(ca|ma))!==H)throw Error(k(187));var c=p;p|=1;try{return Da(99,a.bind(null,b))}finally{p=c,ha()}};I.hydrate=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!0,c)};I.render=function(a,b,c){if(!bc(b))throw Error(k(200));return nd(null,a,b,!1,c)};I.unmountComponentAtNode=function(a){if(!bc(a))throw Error(k(40));return a._reactRootContainer?
(Rh(function(){nd(null,null,a,!1,function(){a._reactRootContainer=null;a[Lb]=null})}),!0):!1};I.unstable_batchedUpdates=Qh;I.unstable_createPortal=function(a,b){return Xh(a,b,2<arguments.length&&void 0!==arguments[2]?arguments[2]:null)};I.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!bc(c))throw Error(k(200));if(null==a||void 0===a._reactInternalFiber)throw Error(k(38));return nd(a,b,c,!1,d)};I.version="16.13.1"});
</script>
    <script>const e = React.createElement;

function pathToString(path) {
  if (path[0] === '/') {
    return '/' + path.slice(1).join('/');
  } else {
    return path.join('/');
  }
}

function findCommonPath(files) {
  if (!files || !files.length) {
    return [];
  }

  function isPrefix(arr, prefix) {
    if (arr.length < prefix.length) {
      return false;
    }
    for (let i = prefix.length - 1; i >= 0; --i) {
      if (arr[i] !== prefix[i]) {
        return false;
      }
    }
    return true;
  }

  let commonPath = files[0].path.slice(0, -1);
  while (commonPath.length) {
    if (files.every(file => isPrefix(file.path, commonPath))) {
      break;
    }
    commonPath.pop();
  }
  return commonPath;
}

function findFolders(files) {
  if (!files || !files.length) {
    return [];
  }

  let folders = files.filter(file => file.path.length > 1).map(file => file.path[0]);
  folders = [...new Set(folders)]; // unique
  folders.sort();

  folders = folders.map(folder => {
    let filesInFolder = files
      .filter(file => file.path[0] === folder)
      .map(file => ({
        ...file,
        path: file.path.slice(1),
        parent: [...file.parent, file.path[0]],
      }));

    const children = findFolders(filesInFolder); // recursion

    return {
      is_folder: true,
      path: [folder],
      parent: files[0].parent,
      children,
      covered: children.reduce((sum, file) => sum + file.covered, 0),
      coverable: children.reduce((sum, file) => sum + file.coverable, 0),
      prevRun: {
        covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
        coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
      }
    };
  });

  return [
    ...folders,
    ...files.filter(file => file.path.length === 1),
  ];
}

class App extends React.Component {
  constructor(...args) {
    super(...args);

    this.state = {
      current: [],
    };
  }

  componentDidMount() {
    this.updateStateFromLocation();
    window.addEventListener("hashchange", () => this.updateStateFromLocation(), false);
  }

  updateStateFromLocation() {
    if (window.location.hash.length > 1) {
      const current = window.location.hash.substr(1).split('/');
      this.setState({current});
    } else {
      this.setState({current: []});
    }
  }

  getCurrentPath() {
    let file = this.props.root;
    let path = [file];
    for (let p of this.state.current) {
      file = file.children.find(file => file.path[0] === p);
      if (!file) {
        return path;
      }
      path.push(file);
    }
    return path;
  }

  render() {
    const path = this.getCurrentPath();
    const file = path[path.length - 1];

    let w = null;
    if (file.is_folder) {
      w = e(FilesList, {
        folder: file,
        onSelectFile: this.selectFile.bind(this),
        onBack: path.length > 1 ? this.back.bind(this) : null,
      });
    } else {
      w = e(DisplayFile, {
        file,
        onBack: this.back.bind(this),
      });
    }

    return e('div', {className: 'app'}, w);
  }

  selectFile(file) {
    this.setState(({current}) => {
      return {current: [...current, file.path[0]]};
    }, () => this.updateHash());
  }

  back(file) {
    this.setState(({current}) => {
      return {current: current.slice(0, current.length - 1)};
    }, () => this.updateHash());
  }

  updateHash() {
    if (!this.state.current || !this.state.current.length) {
      window.location = '#';
    } else {
      window.location = '#' + this.state.current.join('/');
    }
  }
}

function FilesList({folder, onSelectFile, onBack}) {
  let files = folder.children;
  return e('div', {className: 'display-folder'},
    e(FileHeader, {file: folder, onBack}),
    e('table', {className: 'files-list'},
      e('thead', {className: 'files-list__head'},
        e('tr', null,
          e('th', null, "Path"),
          e('th', null, "Coverage")
        )
      ),
      e('tbody', {className: 'files-list__body'},
        files.map(file => e(File, {file, onClick: onSelectFile}))
      )
    )
  );
}

function File({file, onClick}) {
  const coverage = file.coverable ? file.covered / file.coverable * 100 : -1;
  const coverageDelta = file.prevRun &&
    (file.covered / file.coverable * 100 - file.prevRun.covered / file.prevRun.coverable * 100);

  return e('tr', {
      className: 'files-list__file'
        + (coverage >= 0 && coverage < 50 ? ' files-list__file_low': '')
        + (coverage >= 50 && coverage < 80 ? ' files-list__file_medium': '')
        + (coverage >= 80 ? ' files-list__file_high': '')
        + (file.is_folder ? ' files-list__file_folder': ''),
      onClick: () => onClick(file),
    },
    e('td', null, pathToString(file.path)),
    e('td', null,
      file.covered + ' / ' + file.coverable +
      (coverage >= 0 ? ' (' + coverage.toFixed(2) + '%)' : ''),
      e('span', {title: 'Change from the previous run'},
        (coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
    )
  );
}

function DisplayFile({file, onBack}) {
  return e('div', {className: 'display-file'},
    e(FileHeader, {file, onBack}),
    e(FileContent, {file})
  );
}

function FileHeader({file, onBack}) {
  const coverage = file.covered / file.coverable * 100;
  const coverageDelta = file.prevRun && (coverage - file.prevRun.covered / file.prevRun.coverable * 100);

  return e('div', {className: 'file-header'},
    onBack ? e('a', {className: 'file-header__back', onClick: onBack}, 'Back') : null,
    e('div', {className: 'file-header__name'}, pathToString([...file.parent, ...file.path])),
    e('div', {className: 'file-header__stat'},
      'Covered: ' + file.covered + ' of ' + file.coverable +
      (file.coverable ? ' (' + coverage.toFixed(2) + '%)' : ''),
      e('span', {title: 'Change from the previous run'},
        (coverageDelta ? ` (${coverageDelta > 0 ? '+' : ''}${coverageDelta.toFixed(2)}%)` : ''))
    )
  );
}

function FileContent({file}) {
  return e('div', {className: 'file-content'},
    file.content.split(/\r?\n/).map((line, index) => {
      const trace = file.traces.find(trace => trace.line === index + 1);
      const covered = trace && trace.stats.Line;
      const uncovered = trace && !trace.stats.Line;
      return e('pre', {
          className: 'code-line'
            + (covered ? ' code-line_covered' : '')
            + (uncovered ? ' code-line_uncovered' : ''),
          title: trace ? JSON.stringify(trace.stats, null, 2) : null,
        }, line);
    })
  );
}

(function(){
  const commonPath = findCommonPath(data.files);
  const prevFilesMap = new Map();

  previousData && previousData.files.forEach((file) => {
    const path = file.path.slice(commonPath.length).join('/');
    prevFilesMap.set(path, file);
  });

  const files = data.files.map((file) => {
    const path = file.path.slice(commonPath.length);
    const { covered = 0, coverable = 0 } = prevFilesMap.get(path.join('/')) || {};
    return {
      ...file,
      path,
      parent: commonPath,
      prevRun: { covered, coverable },
    };
  });

  const children = findFolders(files);

  const root = {
    is_folder: true,
    children,
    path: commonPath,
    parent: [],
    covered: children.reduce((sum, file) => sum + file.covered, 0),
    coverable: children.reduce((sum, file) => sum + file.coverable, 0),
    prevRun: {
      covered: children.reduce((sum, file) => sum + file.prevRun.covered, 0),
      coverable: children.reduce((sum, file) => sum + file.prevRun.coverable, 0),
    }
  };

  ReactDOM.render(e(App, {root, prevFilesMap}), document.getElementById('root'));
}());
</script>
</body>
</html>