use std::fmt;
use std::io;
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Service {
UploadPack,
ReceivePack,
}
impl Service {
pub fn as_str(self) -> &'static str {
match self {
Service::UploadPack => "git-upload-pack",
Service::ReceivePack => "git-receive-pack",
}
}
pub fn is_write(self) -> bool {
matches!(self, Service::ReceivePack)
}
}
impl fmt::Display for Service {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[error(
"unknown git service {0:?} (expected \"git-upload-pack\" or \"git-receive-pack\", ADR-016)"
)]
pub struct ServiceParseError(pub String);
impl FromStr for Service {
type Err = ServiceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"git-upload-pack" => Ok(Service::UploadPack),
"git-receive-pack" => Ok(Service::ReceivePack),
other => Err(ServiceParseError(other.to_owned())),
}
}
}
#[derive(Debug, Error)]
pub enum SubstrateError {
#[error("transport io: {0}")]
Io(#[from] io::Error),
#[error("protocol error: {0}")]
Protocol(String),
#[error(
"the request reader completed a request; reset_request is required before the next request"
)]
ResetRequired,
#[error(
"the response writer is held by a sideband sink; finish_sideband must restore it first"
)]
WriterTaken,
#[error("the raw tail is unavailable: {0}")]
RawTailUnavailable(&'static str),
#[error("access denied")]
AccessDenied,
}
impl SubstrateError {
pub fn is_terminal(&self) -> bool {
matches!(
self,
SubstrateError::Io(_) | SubstrateError::Protocol(_) | SubstrateError::AccessDenied
)
}
}
impl Clone for SubstrateError {
fn clone(&self) -> Self {
match self {
SubstrateError::Io(e) => SubstrateError::Io(io::Error::new(e.kind(), e.to_string())),
SubstrateError::Protocol(m) => SubstrateError::Protocol(m.clone()),
SubstrateError::ResetRequired => SubstrateError::ResetRequired,
SubstrateError::WriterTaken => SubstrateError::WriterTaken,
SubstrateError::RawTailUnavailable(m) => SubstrateError::RawTailUnavailable(m),
SubstrateError::AccessDenied => SubstrateError::AccessDenied,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_wire_strings_round_trip() {
for (service, wire) in [
(Service::UploadPack, "git-upload-pack"),
(Service::ReceivePack, "git-receive-pack"),
] {
assert_eq!(service.as_str(), wire);
assert_eq!(wire.parse::<Service>().unwrap(), service);
assert_eq!(Service::from_str(wire).unwrap(), service);
assert_eq!(service.to_string(), wire);
}
}
#[test]
fn service_refuses_unknown_stages() {
let err = "git-upload-archive".parse::<Service>().unwrap_err();
assert_eq!(err, ServiceParseError("git-upload-archive".to_owned()));
assert!(
err.to_string().contains("git-upload-archive"),
"the message names the offending value: {err}"
);
}
#[test]
fn service_is_the_write_tier_selector() {
assert!(!Service::UploadPack.is_write());
assert!(Service::ReceivePack.is_write());
}
#[test]
fn taxonomy_labels_only_transport_and_protocol_errors_terminal() {
assert!(SubstrateError::Io(io::Error::other("gone")).is_terminal());
assert!(SubstrateError::Protocol("no command line".into()).is_terminal());
assert!(SubstrateError::AccessDenied.is_terminal());
assert!(!SubstrateError::ResetRequired.is_terminal());
assert!(!SubstrateError::WriterTaken.is_terminal());
assert!(!SubstrateError::RawTailUnavailable("gone").is_terminal());
assert!(
SubstrateError::RawTailUnavailable("gone")
.clone()
.to_string()
.contains("gone"),
"the misuse variant clones with its refusal message"
);
}
#[test]
fn io_errors_clone_preserves_kind() {
let original = SubstrateError::Io(io::Error::new(
io::ErrorKind::ConnectionReset,
"peer reset the stream",
));
let cloned = original.clone();
assert!(matches!(cloned, SubstrateError::Io(_)));
let SubstrateError::Io(ref e) = cloned else {
unreachable!()
};
assert_eq!(e.kind(), io::ErrorKind::ConnectionReset);
assert!(e.to_string().contains("peer reset the stream"));
}
}