use anyhow::{Context, Result, bail};
use base32::Alphabet;
use colored::{Color, Colorize};
use smol::{Unblock, fs::*, io::AsyncWriteExt, unblock};
use std::{
env::{home_dir, var_os},
io::{Error, ErrorKind, Write, stderr, stdin, stdout},
path::PathBuf,
};
use willow25::prelude::*;
pub const SNEAKERWEB_NAMESPACE_ID_BYTES: [u8; NAMESPACE_ID_WIDTH] = [
159, 196, 204, 134, 202, 217, 77, 17, 2, 90, 252, 247, 94, 13, 171, 36, 188, 60, 108, 145, 240,
205, 146, 251, 224, 202, 87, 77, 70, 156, 104, 30,
];
pub const _SNEAKERWEB_SERVICE_NAME: &str = "org.worm-blossom.sneakerweb";
pub const BASE32_ALPHABET: Alphabet = Alphabet::Rfc4648Lower { padding: false };
pub fn user_input<F>(message: &str, prompt_style: Option<F>) -> Result<Option<String>, Error>
where
F: Fn(&str) -> String,
{
println!("{}", message);
match prompt_style {
Some(style) => print!("{} ", style(">")),
None => print!("{} ", ">".bold()),
}
stdout().flush()?;
stdin().lines().next().transpose()
}
pub async fn sneakerweb_dir(collection: Option<&PathBuf>) -> Result<PathBuf, Error> {
if let Some(dir) = collection {
create_dir_all(&dir).await?;
return Ok(dir.into());
}
if let Some(default) = unblock(|| var_os("DEFAULT_SNEAKERWEB_COLLECTION")).await {
create_dir_all(&default).await?;
return Ok(default.into());
}
let mut dir = home_dir().ok_or(Error::new(
ErrorKind::NotFound,
"could not determine user home directory",
))?;
dir.push(".sneakerweb");
create_dir_all(&dir).await?;
Ok(dir)
}
pub fn domain_style(message: &str) -> String {
format!("{}", message.bold().color(Color::Green))
}
pub fn secret_style(message: &str) -> String {
format!("{}", message.bold().color(Color::Magenta))
}
pub fn yay_style(message: &str) -> String {
format!("{}", message.bold().color(Color::Green))
}
pub fn oh_no_style(message: &str) -> String {
format!("{}", message.bold().color(Color::Magenta))
}
pub fn hmm_style(message: &str) -> String {
format!("{}", message.bold().color(Color::Yellow))
}
pub fn emph_style(message: &str) -> String {
format!("{}", message.bold())
}
pub async fn yay(message: &str) {
let mut stdout = Unblock::new(stdout());
let _ = stdout
.write_all(format!("{} {}\n", yay_style("Yay:"), message).as_bytes())
.await;
let _ = stdout.flush().await;
}
pub async fn oh_no(message: &str) {
let mut stderr = Unblock::new(stderr());
let _ = stderr
.write_all(format!("{} {}\n", oh_no_style("Oh No:"), message).as_bytes())
.await;
let _ = stderr.flush().await;
}
pub async fn hmm(message: &str) {
let mut stdout = Unblock::new(stdout());
let _ = stdout
.write_all(format!("{} {}\n", hmm_style("Hmm:"), message).as_bytes())
.await;
let _ = stdout.flush().await;
}
pub fn parse_domain(encoded: &str) -> Result<(SubspaceId, String)> {
let decoded = decode_encoded(encoded).context("invalid domain")?;
Ok((SubspaceId::from_bytes(&decoded), encoded.to_owned()))
}
pub fn get_domain(encoded: Option<&str>, action: &str) -> Result<(SubspaceId, String)> {
let domain = match encoded {
Some(domain) => domain.trim().to_owned(),
None => user_input(
&format!(
"Which {} do you want to {}?",
domain_style("domain"),
action
),
Some(domain_style),
)
.context("failed to retrieve user input")?
.unwrap_or_default(),
};
parse_domain(&domain)
}
pub fn get_secret(encoded: Option<&str>, domain: &str) -> Result<SubspaceSecret> {
let secret_string = match encoded {
Some(secret) => secret.to_owned(),
None => {
user_input(
&format!(
"Please provide the {} for {}:",
secret_style("secret key"),
domain_style(domain)
),
Some(secret_style),
)
.context("failed to retrieve user input")?
.unwrap_or_default()
}
};
let decoded = decode_encoded(&secret_string).context("invalid secret")?;
Ok(SubspaceSecret::from_bytes(&decoded))
}
fn decode_encoded(s: &str) -> Result<[u8; 32]> {
let b32_alphabet = base32::Alphabet::Rfc4648Lower { padding: false };
let Some(decoded) = (match s.len() {
64 => base16::decode(s).ok(),
52 => base32::decode(b32_alphabet, s),
_ => None,
}) else {
bail!("invalid encoding")
};
if decoded.len() != 32 {
bail!("invalid encoding")
}
Ok(*decoded.as_array().expect("expected 32 bytes"))
}
pub async fn warn_deprecated_domain_encoding(encoding: &str, domain_id: &SubspaceId) {
if encoding.len() == 64 {
let updated_encoding = base32::encode(BASE32_ALPHABET, domain_id.as_bytes());
hmm(&format!("deprecated domain name {encoding}, consider replacing with the equivalent {updated_encoding}")).await
}
}