sunset 0.6.0

A SSH library suitable for embedded and larger programs
Documentation
use core::str::Utf8Error;
#[allow(unused_imports)]
use log::{debug, error, info, log, trace, warn};

use core::fmt::Arguments;

use snafu::prelude::*;

use crate::{channel::ChanNum, packets::MessageNumber};

#[cfg(feature = "backtrace")]
use snafu::Backtrace;

// TODO: can we make Snafu not require Debug?

/// The Sunset error type.
#[non_exhaustive]
#[derive(Snafu, Debug)]
#[snafu(context(suffix(false)))]
// TODO: maybe split this into a list of public vs private errors?
#[snafu(visibility(pub))]
pub enum Error {
    /// Input buffer ran out
    RanOut {
        #[cfg(feature = "backtrace")]
        backtrace: Backtrace,
    },

    /// Can't currently send a packet
    ///
    /// Either output buffer is full, or a key exchange is in progress.
    /// `unsupported` is set if it is not expected for Sunset to be
    /// able to defer that type of packet (it may indicate a bug in Sunset,
    //// or traffic that is unanticipated or difficult to handle).
    // Should only be returned from TrafOut::send_packet(),
    // NoRoom is for other similar circumstances.
    BusySend { packet: MessageNumber, unsupported: bool },

    /// No room to write
    NoRoom {
        #[cfg(feature = "backtrace")]
        backtrace: Backtrace,
    },

    /// Not a UTF-8 string
    BadString,

    /// Not a valid SSH ASCII string
    BadName,

    /// Key exchange incorrect
    BadKex,

    /// Packet integrity failed
    BadDecrypt,

    /// Signature is incorrect
    BadSig,

    /// Received a badly formatted number
    BadNumber,

    /// Error in received SSH protocol. Will disconnect.
    SSHProto {
        #[cfg(feature = "backtrace")]
        backtrace: Backtrace,
    },

    /// Peer sent something we don't handle. Will disconnect.
    ///
    /// This differs to `SSHProtoError`. In this case the peer may be
    /// behaved within the SSH specifications, but Sunset doesn't
    /// support it.
    // TODO: 'static disconnect message to return?
    SSHProtoUnsupported,

    /// Remote peer isn't SSH 2.0
    NotSSH,

    /// Bad key format
    BadKey,

    /// Ran out of channels
    NoChannels,

    #[snafu(display("Bad channel number {num}"))]
    BadChannel { num: ChanNum },

    /// Bad channel data type
    ///
    /// Returned from an API call that would imply ChanData::Stderr
    /// being sent to a server. This error will not be returned for
    /// network data in the incorrect direction, instead that data is dropped.
    BadChannelData,

    /// Bad application usage
    ///
    /// Returned from an API call when the API is used incorrectly.
    /// Examples could include:
    /// - A `ChanHandle` is used incorrectly, for example being cloned
    ///   (millions of times) and not released.
    // TODO: /// #[snafu(display("Failure from application: {msg}"))]
    BadUsage {
        #[cfg(feature = "backtrace")]
        backtrace: snafu::Backtrace,
        // TODO
        // msg: &'static str,
    },

    /// SSH packet contents doesn't match length
    WrongPacketLength,

    /// Channel EOF
    ///
    /// This is an expected error when a SSH channel completes. Can be returned
    /// by channel read/write functions. Any further calls in the same direction
    /// will fail similarly.
    ChannelEOF,

    /// Session EOF
    ///
    /// This is an expected error when the SSH session has finished.
    SessionEOF,

    // Used for unknown key types etc.
    #[snafu(display("{what} is not available"))]
    NotAvailable { what: &'static str },

    #[snafu(display("Unknown packet type {number}"))]
    UnknownPacket { number: u8 },

    /// Received packet at a disallowed time.
    // TODO: this is kind of a subset of SSHProtoError, maybe not needed
    PacketWrong {
        #[cfg(feature = "backtrace")]
        backtrace: Backtrace,
    },
    // #[snafu(display("Program bug {location}"))]
    // Bug { location: snafu::Location },
    #[snafu(display("No matching {algo} algorithm"))]
    AlgoNoMatch { algo: &'static str },

    #[snafu(display("Packet size {size} too large (or bad decrypt)"))]
    BigPacket { size: usize },

    /// Ran out of authentication methods
    ///
    /// This is only for the client.
    NoAuthMethods,

    /// An unknown SSH name is provided, for a key type, signature type,
    /// channel name etc.
    #[snafu(display("Unknown {kind} method"))]
    UnknownMethod { kind: &'static str },

    #[snafu(display("{msg}"))]
    // TODO: these could eventually get categorised
    Custom { msg: &'static str },

    /// std IO error
    #[cfg(feature = "std")]
    IoError { source: std::io::Error },

    /// `embedded-io` error
    #[cfg(feature = "embedded-io")]
    EmbeddedIoError { kind: embedded_io::ErrorKind },

    // Error::Bug should not be reached, previous logic should have prevented it.
    // Create this using [`Error::bug()`] or [`.trap()`](TrapBug::trap).
    // Location is currently disabled due to bloat of full paths.
    // https://github.com/rust-lang/rust/issues/95529 is having functions.
    // #[snafu(display("Program bug {location}"))]
    // Bug { location: snafu::Location },
    /// Program bug
    ///
    /// Hitting this may instead panic on builds with debug-assertions.
    Bug,
}

impl Error {
    pub fn msg(m: &'static str) -> Error {
        Error::Custom { msg: m }
    }

    #[cold]
    #[track_caller]
    /// Panics in debug builds, returns [`Result::Err(Error::Bug)`](Error::Bug) in release.
    pub fn bug<T>() -> Result<T, Error> {
        Err(Self::build_bug())
    }

    #[cold]
    #[track_caller]
    /// Panics in debug builds, returns [`Error::Bug`] in release.
    pub fn build_bug() -> Error {
        // Easier to track the source of errors in development,
        // but release builds shouldn't panic.
        if cfg!(debug_assertions) {
            panic!("Hit a bug");
        } else {
            Error::Bug
        }
    }

    /// Like [`bug()`](Error::bug) but with a message
    ///
    /// The message can be used instead of a code comment, is logged at `trace` level.
    #[cold]
    fn bug_fmt(args: Arguments) -> Error {
        // Easier to track the source of errors in development,
        // but release builds shouldn't panic.
        if cfg!(debug_assertions) {
            panic!("Hit a bug: {args}");
        } else {
            trace!("Hit a bug: {args}");
            // let caller = core::panic::Location::caller();
            Error::Bug
            // {
            //     location: snafu::Location::new(
            //         caller.file(),
            //         caller.line(),
            //         caller.column(),
            //     ),
            // }
        }
    }

    #[cold]
    pub fn bug_msg<T>(msg: &str) -> Result<T, Error> {
        Err(Self::bug_fmt(format_args!("{}", msg)))
    }

    #[cold]
    pub fn build_bug_msg(msg: &str) -> Error {
        Self::bug_fmt(format_args!("{}", msg))
    }
}

#[cfg(feature = "embedded-io")]
impl embedded_io::Error for Error {
    fn kind(&self) -> embedded_io::ErrorKind {
        embedded_io::ErrorKind::Other
    }
}

#[cfg(feature = "embedded-io")]
impl From<embedded_io::ErrorKind> for Error {
    fn from(kind: embedded_io::ErrorKind) -> Self {
        Self::EmbeddedIoError { kind }
    }
}

/// A Sunset-specific Result type.
pub type Result<T, E = Error> = core::result::Result<T, E>;

pub trait TrapBug<T> {
    /// `.trap()` should be used like `.unwrap()`, in situations
    /// never expected to fail. Instead it calls [`Error::bug()`].
    /// (or debug builds may panic)
    fn trap(self) -> Result<T, Error>;

    /// Like `trap()` but with a message, calls [`Error::bug_msg()`]
    /// The message can be used instead of a comment.
    fn trap_msg(self, args: Arguments) -> Result<T, Error>;
}

impl<T, E> TrapBug<T> for Result<T, E> {
    #[track_caller]
    fn trap(self) -> Result<T, Error> {
        // call directly so that Location::caller() works
        if let Ok(i) = self { Ok(i) } else { Error::bug() }
    }
    #[track_caller]
    fn trap_msg(self, args: Arguments) -> Result<T, Error> {
        // call directly so that Location::caller() works
        if let Ok(i) = self { Ok(i) } else { Err(Error::bug_fmt(args)) }
    }
}

impl<T> TrapBug<T> for Option<T> {
    #[track_caller]
    fn trap(self) -> Result<T, Error> {
        // call directly so that Location::caller() works
        if let Some(i) = self { Ok(i) } else { Error::bug() }
    }
    #[track_caller]
    fn trap_msg(self, args: Arguments) -> Result<T, Error> {
        // call directly so that Location::caller() works
        if let Some(i) = self { Ok(i) } else { Err(Error::bug_fmt(args)) }
    }
}

impl From<Utf8Error> for Error {
    fn from(_e: Utf8Error) -> Error {
        Error::BadString
    }
}

#[cfg(feature = "std")]
impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        Self::IoError { source: value }
    }
}

#[cfg(test)]
pub(crate) mod tests {}