use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
pub const SEPARATOR: &str = "::";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessKey {
pub address: String,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyFormatError {
Empty,
NotBase64,
MissingSeparator,
MissingField(&'static str),
}
impl KeyFormatError {
pub fn code(&self) -> &'static str {
match self {
Self::Empty => "empty_key",
Self::NotBase64 => "not_base64",
Self::MissingSeparator => "missing_separator",
Self::MissingField(_) => "missing_field",
}
}
pub fn message(&self) -> String {
match self {
Self::Empty => "nenhum token de conexão informado".into(),
Self::NotBase64 => "o token de conexão não é base64 válido — foi cortado ou \
ganhou caracteres no caminho?"
.into(),
Self::MissingSeparator => format!(
"o token de conexão decodificado não tem o formato host:porta{SEPARATOR}token \
— cole o token de conexão inteiro, não só o token da API"
),
Self::MissingField(parte) => {
format!("o {parte} está vazio dentro do token de conexão")
}
}
}
}
impl AccessKey {
pub fn encode(&self) -> String {
STANDARD.encode(format!("{}{SEPARATOR}{}", self.address, self.token))
}
pub fn decode(raw: &str) -> Result<Self, KeyFormatError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(KeyFormatError::Empty);
}
let normalized = raw
.trim_end_matches('=')
.replace('+', "-")
.replace('/', "_");
let bytes = URL_SAFE_NO_PAD
.decode(&normalized)
.map_err(|_| KeyFormatError::NotBase64)?;
let text = String::from_utf8(bytes).map_err(|_| KeyFormatError::MissingSeparator)?;
let (address, token) = split_address(&text).ok_or(KeyFormatError::MissingSeparator)?;
let address = address.trim();
let address = address
.split_once("://")
.map_or(address, |(_, rest)| rest)
.trim_end_matches('/');
let token = token.trim();
if address.is_empty() {
return Err(KeyFormatError::MissingField("endereço"));
}
if token.is_empty() {
return Err(KeyFormatError::MissingField("token"));
}
Ok(AccessKey {
address: address.to_owned(),
token: token.to_owned(),
})
}
}
fn split_address(text: &str) -> Option<(&str, &str)> {
let authority_start = text.find("://").map_or(0, |i| i + 3);
let search_from = if text[authority_start..].starts_with('[') {
authority_start + text[authority_start..].find(']')?
} else {
authority_start
};
let at = search_from + text[search_from..].find(SEPARATOR)?;
Some((&text[..at], &text[at + SEPARATOR.len()..]))
}
#[cfg(test)]
mod tests {
use super::*;
fn key(address: &str, token: &str) -> AccessKey {
AccessKey {
address: address.into(),
token: token.into(),
}
}
#[test]
fn igual_ao_b64encode_do_python() {
let chave = key("rustploy.exemplo.com:9797", "a3f9_x-y").encode();
assert_eq!(chave, "cnVzdHBsb3kuZXhlbXBsby5jb206OTc5Nzo6YTNmOV94LXk=");
assert_eq!(
AccessKey::decode(&chave).unwrap(),
key("rustploy.exemplo.com:9797", "a3f9_x-y")
);
assert_eq!(
AccessKey::decode(&format!(" {chave}\n")).unwrap(),
key("rustploy.exemplo.com:9797", "a3f9_x-y")
);
}
#[test]
fn aceita_url_safe_sem_padding_e_esquema_posto_a_mao() {
let chave = URL_SAFE_NO_PAD.encode("https://a.b:9797::t~?");
assert_eq!(AccessKey::decode(&chave).unwrap(), key("a.b:9797", "t~?"));
}
#[test]
fn ipv6_e_token_com_dois_pontos_duplos() {
let chave = key("[::1]:9797", "x::y").encode();
assert_eq!(AccessKey::decode(&chave).unwrap(), key("[::1]:9797", "x::y"));
}
#[test]
fn cada_erro_diz_qual_parte_falhou() {
assert_eq!(AccessKey::decode(""), Err(KeyFormatError::Empty));
assert_eq!(AccessKey::decode("!!!"), Err(KeyFormatError::NotBase64));
assert_eq!(
AccessKey::decode(&STANDARD.encode("so-o-token")),
Err(KeyFormatError::MissingSeparator)
);
assert_eq!(
AccessKey::decode(&STANDARD.encode("a.b:1::")),
Err(KeyFormatError::MissingField("token"))
);
assert_eq!(
AccessKey::decode(&STANDARD.encode("::t")),
Err(KeyFormatError::MissingField("endereço"))
);
}
}