cp-cli 0.6.0

Human-first command-line client for competitive-programming platforms
Documentation
use crate::{
    domain::{AuthLogout, AuthStatus},
    error::{Error, Result},
};

#[cfg(feature = "leetcode")]
use crate::domain::AuthSource;

#[cfg(feature = "leetcode")]
const KEYRING_SERVICE: &str = "cp-cli";
#[cfg(feature = "leetcode")]
const KEYRING_USER: &str = "leetcode";
#[cfg(feature = "leetcode")]
const STORED_CREDENTIAL_VERSION: u8 = 1;

pub(crate) async fn status() -> Result<AuthStatus> {
    #[cfg(feature = "leetcode")]
    {
        let Some((credentials, source)) = credentials()? else {
            return Ok(AuthStatus {
                configured: false,
                authenticated: false,
                source: None,
            });
        };
        let authenticated = match platform_leetcode::Client::new()?
            .auth_status(&credentials)
            .await
        {
            Ok(authenticated) => authenticated,
            Err(platform_leetcode::Error::Authentication) => false,
            Err(error) => return Err(error.into()),
        };
        Ok(AuthStatus {
            configured: true,
            authenticated,
            source: Some(source),
        })
    }
    #[cfg(not(feature = "leetcode"))]
    {
        Err(Error::LeetCodeDisabled)
    }
}

pub(crate) fn logout() -> Result<AuthLogout> {
    #[cfg(feature = "leetcode")]
    {
        let entry = keyring_entry()?;
        let removed = match entry.delete_credential() {
            Ok(()) => true,
            Err(keyring::Error::NoEntry) => false,
            Err(error) => return Err(Error::CredentialStore(error)),
        };
        Ok(AuthLogout {
            removed,
            environment_present: authentication_environment_present(),
        })
    }
    #[cfg(not(feature = "leetcode"))]
    {
        Err(Error::LeetCodeDisabled)
    }
}

#[cfg(feature = "leetcode")]
pub(crate) fn required_credentials() -> Result<platform_leetcode::Credentials> {
    credentials()?
        .map(|(credentials, _)| credentials)
        .ok_or(Error::AuthenticationRequired)
}

#[cfg(feature = "leetcode")]
fn credentials() -> Result<Option<(platform_leetcode::Credentials, AuthSource)>> {
    if let Some(credentials) = environment_credentials()? {
        return Ok(Some((credentials, AuthSource::Environment)));
    }
    saved_credentials()
        .map(|credentials| credentials.map(|credentials| (credentials, AuthSource::SecureStore)))
}

#[cfg(feature = "leetcode")]
fn environment_credentials() -> Result<Option<platform_leetcode::Credentials>> {
    let session = std::env::var_os("LEETCODE_SESSION");
    let csrf =
        std::env::var_os("LEETCODE_CSRFTOKEN").or_else(|| std::env::var_os("LEETCODE_CSRF_TOKEN"));
    match (session, csrf) {
        (None, None) => Ok(None),
        (Some(session), Some(csrf)) => {
            let session = session
                .into_string()
                .map_err(|_| Error::InvalidAuthenticationEnvironment)?;
            let csrf = csrf
                .into_string()
                .map_err(|_| Error::InvalidAuthenticationEnvironment)?;
            Ok(Some(platform_leetcode::Credentials::new(&session, &csrf)?))
        }
        _ => Err(Error::InvalidAuthenticationEnvironment),
    }
}

#[cfg(feature = "leetcode")]
fn authentication_environment_present() -> bool {
    [
        "LEETCODE_SESSION",
        "LEETCODE_CSRFTOKEN",
        "LEETCODE_CSRF_TOKEN",
    ]
    .iter()
    .any(|name| std::env::var_os(name).is_some())
}

#[cfg(feature = "leetcode")]
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct StoredCredentials {
    version: u8,
    session: String,
    csrf: String,
}

#[cfg(feature = "leetcode")]
fn keyring_entry() -> Result<keyring::Entry> {
    keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER).map_err(Error::CredentialStore)
}

#[cfg(feature = "leetcode")]
fn saved_credentials() -> Result<Option<platform_leetcode::Credentials>> {
    let encoded = match keyring_entry()?.get_secret() {
        Ok(encoded) => encoded,
        Err(keyring::Error::NoEntry) => return Ok(None),
        Err(error) => return Err(Error::CredentialStore(error)),
    };
    decode_credentials(&encoded).map(Some)
}

#[cfg(feature = "leetcode")]
fn decode_credentials(encoded: &[u8]) -> Result<platform_leetcode::Credentials> {
    let stored: StoredCredentials =
        serde_json::from_slice(encoded).map_err(|_| Error::InvalidStoredCredentials)?;
    if stored.version != STORED_CREDENTIAL_VERSION {
        return Err(Error::InvalidStoredCredentials);
    }
    platform_leetcode::Credentials::new(&stored.session, &stored.csrf)
        .map_err(|_| Error::InvalidStoredCredentials)
}

#[cfg(all(test, feature = "leetcode"))]
mod tests {
    use super::*;

    #[test]
    fn stored_credential_contract() {
        assert!(
            decode_credentials(br#"{"version":1,"session":"session-value","csrf":"csrf-value"}"#)
                .is_ok()
        );
        assert!(matches!(
            decode_credentials(br#"{"version":2,"session":"session","csrf":"csrf"}"#),
            Err(Error::InvalidStoredCredentials)
        ));
        assert!(matches!(
            decode_credentials(br#"{"version":1,"session":"bad value","csrf":"csrf"}"#),
            Err(Error::InvalidStoredCredentials)
        ));
    }
}