view rust/hg-core/src/errors.rs @ 46637:bc08c2331f99

rust: Add a `ConfigValueParseError` variant to common errors Configuration files are parsed into sections of key/value pairs when they are read, but at that point values are still arbitrary bytes. Only when a value is accessed by various parts of the code do we know its expected type and syntax, so values are parsed at that point. Let?s make a new error type for this latter kind of parsing error, and add a variant to the common `HgError` so that most code can propagate it without much boilerplate. Differential Revision: https://phab.mercurial-scm.org/D10009
author Simon Sapin <simon.sapin@octobus.net>
date Tue, 16 Feb 2021 15:22:20 +0100
parents f031fe1c6ede
children 1f55cd5b292f
line wrap: on
line source

use crate::config::ConfigValueParseError;
use std::fmt;

/// Common error cases that can happen in many different APIs
#[derive(Debug, derive_more::From)]
pub enum HgError {
    IoError {
        error: std::io::Error,
        context: IoErrorContext,
    },

    /// A file under `.hg/` normally only written by Mercurial is not in the
    /// expected format. This indicates a bug in Mercurial, filesystem
    /// corruption, or hardware failure.
    ///
    /// The given string is a short explanation for users, not intended to be
    /// machine-readable.
    CorruptedRepository(String),

    /// The respository or requested operation involves a feature not
    /// supported by the Rust implementation. Falling back to the Python
    /// implementation may or may not work.
    ///
    /// The given string is a short explanation for users, not intended to be
    /// machine-readable.
    UnsupportedFeature(String),

    /// Operation cannot proceed for some other reason.
    ///
    /// The given string is a short explanation for users, not intended to be
    /// machine-readable.
    Abort(String),

    /// A configuration value is not in the expected syntax.
    ///
    /// These errors can happen in many places in the code because values are
    /// parsed lazily as the file-level parser does not know the expected type
    /// and syntax of each value.
    #[from]
    ConfigValueParseError(ConfigValueParseError),
}

/// Details about where an I/O error happened
#[derive(Debug, derive_more::From)]
pub enum IoErrorContext {
    /// A filesystem operation for the given file
    #[from]
    File(std::path::PathBuf),
    /// `std::env::current_dir`
    CurrentDir,
    /// `std::env::current_exe`
    CurrentExe,
}

impl HgError {
    pub fn corrupted(explanation: impl Into<String>) -> Self {
        // TODO: capture a backtrace here and keep it in the error value
        // to aid debugging?
        // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
        HgError::CorruptedRepository(explanation.into())
    }

    pub fn unsupported(explanation: impl Into<String>) -> Self {
        HgError::UnsupportedFeature(explanation.into())
    }
    pub fn abort(explanation: impl Into<String>) -> Self {
        HgError::Abort(explanation.into())
    }
}

// TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
impl fmt::Display for HgError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            HgError::Abort(explanation) => write!(f, "{}", explanation),
            HgError::IoError { error, context } => {
                write!(f, "{}: {}", error, context)
            }
            HgError::CorruptedRepository(explanation) => {
                write!(f, "corrupted repository: {}", explanation)
            }
            HgError::UnsupportedFeature(explanation) => {
                write!(f, "unsupported feature: {}", explanation)
            }
            HgError::ConfigValueParseError(ConfigValueParseError {
                origin: _,
                line: _,
                section,
                item,
                value,
                expected_type,
            }) => {
                // TODO: add origin and line number information, here and in
                // corresponding python code
                write!(
                    f,
                    "config error: {}.{} is not a {} ('{}')",
                    String::from_utf8_lossy(section),
                    String::from_utf8_lossy(item),
                    expected_type,
                    String::from_utf8_lossy(value)
                )
            }
        }
    }
}

// TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
impl fmt::Display for IoErrorContext {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            IoErrorContext::File(path) => path.display().fmt(f),
            IoErrorContext::CurrentDir => f.write_str("current directory"),
            IoErrorContext::CurrentExe => f.write_str("current executable"),
        }
    }
}

pub trait IoResultExt<T> {
    /// Annotate a possible I/O error as related to a file at the given path.
    ///
    /// This allows printing something like “File not found: example.txt”
    /// instead of just “File not found”.
    ///
    /// Converts a `Result` with `std::io::Error` into one with `HgError`.
    fn for_file(self, path: &std::path::Path) -> Result<T, HgError>;
}

impl<T> IoResultExt<T> for std::io::Result<T> {
    fn for_file(self, path: &std::path::Path) -> Result<T, HgError> {
        self.map_err(|error| HgError::IoError {
            error,
            context: IoErrorContext::File(path.to_owned()),
        })
    }
}

pub trait HgResultExt<T> {
    /// Handle missing files separately from other I/O error cases.
    ///
    /// Wraps the `Ok` type in an `Option`:
    ///
    /// * `Ok(x)` becomes `Ok(Some(x))`
    /// * An I/O "not found" error becomes `Ok(None)`
    /// * Other errors are unchanged
    fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
}

impl<T> HgResultExt<T> for Result<T, HgError> {
    fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
        match self {
            Ok(x) => Ok(Some(x)),
            Err(HgError::IoError { error, .. })
                if error.kind() == std::io::ErrorKind::NotFound =>
            {
                Ok(None)
            }
            Err(other_error) => Err(other_error),
        }
    }
}