use std::fmt;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::services::auth::TokenResponse;
use crate::services::default_paths::resolve_sce_default_locations;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StoredTokens {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
pub refresh_token: String,
pub scope: Option<String>,
pub stored_at_unix_seconds: u64,
}
impl StoredTokens {
fn from_token_response(token: &TokenResponse) -> Result<Self, TokenStorageError> {
let stored_at_unix_seconds = current_unix_timestamp_seconds()?;
Ok(Self {
access_token: token.access_token.clone(),
token_type: token.token_type.clone(),
expires_in: token.expires_in,
refresh_token: token.refresh_token.clone(),
scope: token.scope.clone(),
stored_at_unix_seconds,
})
}
}
#[derive(Debug)]
pub enum TokenStorageError {
PathResolution(String),
Io(std::io::Error),
Serialization(serde_json::Error),
CorruptedTokenFile(String),
#[allow(dead_code)]
Permission(String),
}
impl fmt::Display for TokenStorageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PathResolution(reason) => write!(
f,
"Unable to resolve token storage path: {reason}. Try: set a valid user home/state directory and retry."
),
Self::Io(error) => write!(
f,
"Failed to read or write authentication tokens: {error}. Try: verify file permissions for the auth state directory."
),
Self::Serialization(error) => write!(
f,
"Failed to serialize authentication tokens: {error}. Try: rerun login to regenerate credentials."
),
Self::CorruptedTokenFile(reason) => write!(
f,
"Stored authentication tokens are invalid: {reason}. Try: run 'sce logout' and then 'sce login'."
),
Self::Permission(reason) => write!(
f,
"Unable to apply secure token file permissions: {reason}. Try: verify local account permissions and retry."
),
}
}
}
impl std::error::Error for TokenStorageError {}
impl From<std::io::Error> for TokenStorageError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<serde_json::Error> for TokenStorageError {
fn from(value: serde_json::Error) -> Self {
Self::Serialization(value)
}
}
pub fn save_tokens(token: &TokenResponse) -> Result<StoredTokens, TokenStorageError> {
let token_path = token_file_path()?;
let stored = StoredTokens::from_token_response(token)?;
save_tokens_at_path(&token_path, &stored)?;
Ok(stored)
}
pub fn load_tokens() -> Result<Option<StoredTokens>, TokenStorageError> {
let token_path = token_file_path()?;
load_tokens_from_path(&token_path)
}
pub fn delete_tokens() -> Result<bool, TokenStorageError> {
let token_path = token_file_path()?;
delete_tokens_at_path(&token_path)
}
pub fn token_file_path() -> Result<PathBuf, TokenStorageError> {
resolve_sce_default_locations()
.map(|locations| locations.auth_tokens_file())
.map_err(|error| TokenStorageError::PathResolution(error.to_string()))
}
fn save_tokens_at_path(path: &Path, stored: &StoredTokens) -> Result<(), TokenStorageError> {
ensure_parent_directory(path)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
apply_secure_file_permissions(path)?;
let encoded = serde_json::to_vec_pretty(stored)?;
file.write_all(&encoded)?;
file.write_all(b"\n")?;
file.sync_all()?;
Ok(())
}
fn load_tokens_from_path(path: &Path) -> Result<Option<StoredTokens>, TokenStorageError> {
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(TokenStorageError::Io(error)),
};
let parsed: StoredTokens = serde_json::from_str(&content).map_err(|error| {
TokenStorageError::CorruptedTokenFile(format!("{} ({error})", path.display()))
})?;
Ok(Some(parsed))
}
fn delete_tokens_at_path(path: &Path) -> Result<bool, TokenStorageError> {
match fs::remove_file(path) {
Ok(()) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(TokenStorageError::Io(error)),
}
}
fn ensure_parent_directory(path: &Path) -> Result<(), TokenStorageError> {
let Some(parent) = path.parent() else {
return Err(TokenStorageError::PathResolution(format!(
"token path '{}' has no parent directory",
path.display()
)));
};
fs::create_dir_all(parent)?;
apply_secure_directory_permissions(parent)?;
Ok(())
}
fn current_unix_timestamp_seconds() -> Result<u64, TokenStorageError> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| {
TokenStorageError::PathResolution(format!("system clock is invalid: {error}"))
})?
.as_secs())
}
#[cfg(unix)]
fn apply_secure_directory_permissions(path: &Path) -> Result<(), TokenStorageError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
Ok(())
}
#[cfg(not(unix))]
fn apply_secure_directory_permissions(_path: &Path) -> Result<(), TokenStorageError> {
Ok(())
}
#[cfg(unix)]
fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
Ok(())
}
#[cfg(windows)]
fn apply_secure_file_permissions(path: &Path) -> Result<(), TokenStorageError> {
use std::process::Command;
let username = std::env::var("USERNAME").map_err(|_| {
TokenStorageError::Permission(
"USERNAME environment variable is unavailable on Windows".to_string(),
)
})?;
let grant_rule = format!("{username}:(R,W)");
let output = Command::new("icacls")
.arg(path)
.arg("/inheritance:r")
.arg("/grant:r")
.arg(grant_rule)
.output()
.map_err(|error| {
TokenStorageError::Permission(format!("failed to execute icacls: {error}"))
})?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(TokenStorageError::Permission(format!(
"icacls failed for '{}': {stderr}",
path.display()
)))
}
}
#[cfg(not(any(unix, windows)))]
fn apply_secure_file_permissions(_path: &Path) -> Result<(), TokenStorageError> {
Ok(())
}