1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use thiserror::Error;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
/// One error entry as returned by a server-side `failure` response.
pub struct ServerErrorItem {
/// Subversion error code.
pub code: u64,
/// Human-readable error message (UTF-8, lossy-decoded).
pub message: Option<String>,
/// Source file on the server side, if provided.
pub file: Option<String>,
/// Source line on the server side, if provided.
pub line: Option<u64>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
/// A structured server error returned by `svnserve`.
///
/// `context` is typically the command name (or a higher-level operation) and
/// `chain` is the server-provided error stack.
pub struct ServerError {
/// High-level context for the failure (for example, the command name).
pub context: Option<String>,
/// The server-provided error chain.
pub chain: Vec<ServerErrorItem>,
}
impl ServerError {
/// Attaches additional context to this error.
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
/// Returns a single-line, human-readable message.
///
/// This is a best-effort summary of the server-provided error chain.
pub fn message_summary(&self) -> String {
let mut messages = Vec::new();
for err in &self.chain {
if let Some(message) = err.message.as_deref()
&& !message.is_empty()
{
messages.push(message);
}
}
if messages.is_empty() {
"unknown error".to_string()
} else {
messages.join("; ")
}
}
/// Returns `true` if any server-provided message contains `needle`,
/// ignoring ASCII case.
pub fn message_contains_case_insensitive(&self, needle: &str) -> bool {
self.chain
.iter()
.filter_map(|item| item.message.as_deref())
.any(|message| {
message
.to_ascii_lowercase()
.contains(&needle.to_ascii_lowercase())
})
}
/// Returns `true` if the server reported a missing revision.
pub fn is_missing_revision(&self) -> bool {
self.message_contains_case_insensitive("missing revision")
}
/// Returns `true` if the server rejected an unsupported command.
pub fn is_unknown_command(&self) -> bool {
self.message_contains_case_insensitive("unknown command")
|| self.message_contains_case_insensitive("unknown cmd")
}
}
impl std::fmt::Display for ServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ctx) = self.context.as_deref()
&& !ctx.is_empty()
{
write!(f, "{ctx}: ")?;
}
write!(f, "{}", self.message_summary())
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
/// Errors returned by this crate.
pub enum SvnError {
/// The provided URL is syntactically invalid or unsupported.
#[error("invalid svn url: {0}")]
InvalidUrl(String),
/// The provided repository path is invalid or unsafe.
#[error("invalid path: {0}")]
InvalidPath(String),
/// An I/O error occurred while reading/writing the network stream.
#[error("io error: {0}")]
Io(#[from] std::io::Error),
/// The server response did not match the expected `ra_svn` protocol shape.
#[error("protocol error: {0}")]
Protocol(String),
/// The server requested authentication but offered no supported mechanisms.
#[error("auth required but no supported mechanism")]
AuthUnavailable,
/// Authentication failed (for example, invalid username/password).
#[error("auth failed: {0}")]
AuthFailed(String),
/// The server returned a `failure` response.
#[error("server error: {0}")]
Server(ServerError),
}