use std::fmt;
use std::io;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCode {
RepoNotFound,
RepoCorrupt,
RefNotFound,
RefConflict,
MergeConflict,
IndexLocked,
AuthFailed,
TransportDenied,
CryptoError,
ObjectNotFound,
InvalidInput,
NotImplemented,
IoError,
ConfigError,
GeneralError,
}
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::RepoNotFound => "REPO_NOT_FOUND",
ErrorCode::RepoCorrupt => "REPO_CORRUPT",
ErrorCode::RefNotFound => "REF_NOT_FOUND",
ErrorCode::RefConflict => "REF_CONFLICT",
ErrorCode::MergeConflict => "MERGE_CONFLICT",
ErrorCode::IndexLocked => "INDEX_LOCKED",
ErrorCode::AuthFailed => "AUTH_FAILED",
ErrorCode::TransportDenied => "TRANSPORT_DENIED",
ErrorCode::CryptoError => "CRYPTO_ERROR",
ErrorCode::ObjectNotFound => "OBJECT_NOT_FOUND",
ErrorCode::InvalidInput => "INVALID_INPUT",
ErrorCode::NotImplemented => "NOT_IMPLEMENTED",
ErrorCode::IoError => "IO_ERROR",
ErrorCode::ConfigError => "CONFIG_ERROR",
ErrorCode::GeneralError => "GENERAL_ERROR",
}
}
}
#[derive(Debug)]
pub enum LitError {
Encryption(String),
IO(String),
Config(String),
Network(String),
Repository(String),
Object(String),
Index(String),
General(String),
}
impl LitError {
pub fn encryption(internal_msg: impl Into<String>) -> Self {
LitError::Encryption(internal_msg.into())
}
pub fn io(internal_msg: impl Into<String>) -> Self {
LitError::IO(internal_msg.into())
}
pub fn config(internal_msg: impl Into<String>) -> Self {
LitError::Config(internal_msg.into())
}
pub fn network(internal_msg: impl Into<String>) -> Self {
LitError::Network(internal_msg.into())
}
pub fn repository(internal_msg: impl Into<String>) -> Self {
LitError::Repository(internal_msg.into())
}
pub fn object(internal_msg: impl Into<String>) -> Self {
LitError::Object(internal_msg.into())
}
pub fn index(internal_msg: impl Into<String>) -> Self {
LitError::Index(internal_msg.into())
}
pub fn general(internal_msg: impl Into<String>) -> Self {
LitError::General(internal_msg.into())
}
pub fn internal_message(&self) -> &str {
match self {
LitError::Encryption(msg) => msg,
LitError::IO(msg) => msg,
LitError::Config(msg) => msg,
LitError::Network(msg) => msg,
LitError::Repository(msg) => msg,
LitError::Object(msg) => msg,
LitError::Index(msg) => msg,
LitError::General(msg) => msg,
}
}
pub fn error_code(&self) -> &'static str {
match self {
LitError::Encryption(_) => ErrorCode::CryptoError.as_str(),
LitError::IO(_) => ErrorCode::IoError.as_str(),
LitError::Config(_) => ErrorCode::ConfigError.as_str(),
LitError::Network(_) => ErrorCode::TransportDenied.as_str(),
LitError::Repository(msg) => {
if msg.contains("not found")
|| msg.contains("No .lit directory")
|| msg.contains("find_repo_root")
{
ErrorCode::RepoNotFound.as_str()
} else {
ErrorCode::RepoCorrupt.as_str()
}
}
LitError::Object(msg) => {
if msg.contains("not found") || msg.contains("No such") {
ErrorCode::ObjectNotFound.as_str()
} else {
ErrorCode::GeneralError.as_str()
}
}
LitError::Index(_) => ErrorCode::GeneralError.as_str(),
LitError::General(msg) => {
if msg.contains("not yet implemented") || msg.contains("not yet fully implemented")
{
ErrorCode::NotImplemented.as_str()
} else if msg.contains("not found") {
ErrorCode::RefNotFound.as_str()
} else {
ErrorCode::GeneralError.as_str()
}
}
}
}
pub fn user_message(&self) -> &str {
match self {
LitError::Encryption(_) => "Encryption operation failed",
LitError::IO(_) => "I/O operation failed",
LitError::Config(_) => "Configuration error",
LitError::Network(_) => "Network operation failed",
LitError::Repository(msg) => {
if msg.contains("not found") || msg.contains("No .lit directory") {
"Not in a Lit repository"
} else {
"Repository error"
}
}
LitError::Object(msg) => {
if msg.contains("not found") || msg.contains("No such") {
"Object not found"
} else {
"Object error"
}
}
LitError::Index(_) => "Index error",
LitError::General(msg) => {
if msg.contains("not yet implemented") || msg.contains("not yet fully implemented")
{
"Feature not yet implemented"
} else if msg.contains("not found") {
"Resource not found"
} else {
"Operation failed"
}
}
}
}
pub fn suggestions(&self) -> Vec<&'static str> {
match self {
LitError::General(msg) | LitError::IO(msg)
if msg.contains("no Lit encryption header") =>
{
vec![
"Encryption cannot be enabled for a repository that already has commits",
"Create a new repository with encryption enabled and import into it",
]
}
LitError::General(msg) | LitError::IO(msg)
if msg.contains("Encryption not initialized") =>
{
vec![
"Set LIT_PASSPHRASE or LIT_PASSPHRASE_FILE to unlock the repository",
"Check encryption settings in .lit/encryption.toml",
]
}
LitError::Repository(msg)
if msg.contains("not found") || msg.contains("No .lit directory") =>
{
vec![
"Run 'lit init' to create a repository",
"Check that you are in the correct directory",
]
}
LitError::Object(_) => {
vec![
"Verify the object hash is correct",
"Run 'lit verify' to check repository integrity",
]
}
LitError::Network(_) => {
vec![
"Check remote URL configuration with 'lit remote list'",
"Verify network/airgap settings with 'lit config show'",
]
}
LitError::Encryption(_) => {
vec![
"Verify passphrase is correct",
"Check encryption configuration",
]
}
LitError::General(msg) if msg.contains("not yet implemented") => {
vec!["This feature is planned for a future release"]
}
_ => vec![],
}
}
pub fn log_detailed(&self) {
if std::env::var("LIT_DEBUG").is_ok() {
let log_path = get_secure_log_path();
if let Ok(path) = log_path {
use std::fs::OpenOptions;
use std::io::Write;
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let log_entry =
format!("[{}] {:?}: {}\n", timestamp, self, self.internal_message());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = file.write_all(log_entry.as_bytes());
}
}
}
}
}
impl fmt::Display for LitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LitError::Encryption(_) => write!(f, "Encryption operation failed"),
LitError::IO(_) => write!(f, "I/O operation failed"),
LitError::Config(_) => write!(f, "Configuration error"),
LitError::Network(_) => write!(f, "Network operation failed"),
LitError::Repository(_) => write!(f, "Repository operation failed"),
LitError::Object(_) => write!(f, "Object operation failed"),
LitError::Index(_) => write!(f, "Index operation failed"),
LitError::General(_) => write!(f, "Operation failed"),
}
}
}
impl std::error::Error for LitError {}
impl From<io::Error> for LitError {
fn from(err: io::Error) -> Self {
LitError::IO(err.to_string())
}
}
impl From<String> for LitError {
fn from(msg: String) -> Self {
LitError::General(msg)
}
}
impl From<&str> for LitError {
fn from(msg: &str) -> Self {
LitError::General(msg.to_string())
}
}
fn get_secure_log_path() -> Result<std::path::PathBuf, String> {
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
let log_dir = home.join(".lit").join("logs");
std::fs::create_dir_all(&log_dir)
.map_err(|e| format!("Failed to create log directory: {}", e))?;
let log_file = log_dir.join("debug.log");
#[cfg(unix)]
{
use std::fs::OpenOptions;
use std::os::unix::fs::PermissionsExt;
if !log_file.exists() {
OpenOptions::new()
.create_new(true)
.write(true)
.open(&log_file)
.ok();
}
if let Ok(metadata) = std::fs::metadata(&log_file) {
let mut perms = metadata.permissions();
perms.set_mode(0o600); std::fs::set_permissions(&log_file, perms).ok();
}
}
Ok(log_file)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display_sanitization() {
let err = LitError::encryption("Detailed: failed to decrypt /home/user/secret/file.txt");
assert_eq!(err.to_string(), "Encryption operation failed");
assert!(!err.to_string().contains("/home"));
assert!(!err.to_string().contains("secret"));
}
#[test]
fn test_internal_message_access() {
let err = LitError::io("Failed to read /etc/shadow");
assert_eq!(err.internal_message(), "Failed to read /etc/shadow");
}
#[test]
fn test_error_conversion() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let lit_err: LitError = io_err.into();
assert_eq!(lit_err.to_string(), "I/O operation failed");
}
}