1#[derive(Debug, thiserror::Error)]
2pub enum CliError {
3 #[error("{}", display_steam_error(.0))]
4 Steam(#[from] steamroom::error::Error),
5
6 #[error("{0}")]
7 Io(#[from] std::io::Error),
8
9 #[error("cryptography error: {0}")]
10 Crypto(#[from] steamroom::error::CryptoError),
11
12 #[error("internal task error: {0}")]
13 Join(#[from] tokio::task::JoinError),
14
15 #[error("{}", display_manifest_error(.0))]
16 Manifest(#[from] steamroom::error::ManifestError),
17
18 #[error("chunk processing failed: {0}")]
19 Chunk(#[from] steamroom::depot::chunk::ChunkError),
20
21 #[error("failed to decode server response: {0}")]
22 Protobuf(#[from] prost::DecodeError),
23
24 #[error("invalid regex pattern: {0}")]
25 Regex(#[from] regex::Error),
26
27 #[error("JSON error: {0}")]
28 Json(#[from] serde_json::Error),
29
30 #[error("{}", display_http_error(.0))]
31 Http(#[from] reqwest::Error),
32
33 #[error("failed to parse KeyValue data: {0}")]
34 Kv(#[from] steamroom::types::key_value::TextKvError),
35
36 #[error("Steam returned no product info for app {0} (does the app exist?)")]
37 NoProductInfo(u32),
38
39 #[error("app {0} has no metadata")]
40 NoKvData(u32),
41
42 #[error("no depots found in app info")]
43 NoDepots,
44
45 #[error("depot {0} was not found in the app info")]
46 DepotNotFound(u32),
47
48 #[error("no manifest found for depot {depot} on branch \"{branch}\"")]
49 ManifestNotFound { depot: u32, branch: String },
50
51 #[error("the manifest ID is not a valid number")]
52 InvalidManifestId,
53
54 #[error("no cached decryption key for depot {0} in config.vdf")]
55 NoLocalKey(u32),
56
57 #[error("Steam installation not found")]
58 SteamNotFound,
59
60 #[error("{}", display_login_error(.0))]
61 Login(#[from] steamroom_client::login::LoginError),
62
63 #[error(
64 "authentication requires an interactive terminal. Re-run on a TTY, \
65 supply --password / STEAM_PASS for credentials login, or use a \
66 valid saved refresh token."
67 )]
68 InteractiveAuthRequired,
69
70 #[error("Steam returned no CDN servers")]
71 NoCdnServers,
72
73 #[error(
74 "--use-daemon: {0} are not supported via the daemon; pass them to --daemon at launch instead"
75 )]
76 DaemonRejectedFlag(&'static str),
77
78 #[error("--priority is only valid with --use-daemon")]
79 PriorityWithoutDaemon,
80
81 #[error("--detach is only valid with --use-daemon")]
82 DetachWithoutDaemon,
83
84 #[error("--daemon and --use-daemon are mutually exclusive")]
85 DaemonModeConflict,
86
87 #[error(
88 "daemon RPC: incompatible wire-protocol version (peer={peer}, ours={ours}); restart the daemon"
89 )]
90 ProtocolVersionMismatch { peer: u16, ours: u16 },
91
92 #[error("daemon RPC: frame exceeds {limit_bytes} byte cap (got {len_bytes})")]
93 FrameTooLarge { len_bytes: u64, limit_bytes: u64 },
94
95 #[error("daemon RPC: malformed frame: {0}")]
96 MalformedFrame(String),
97
98 #[error("daemon RPC: socket closed before frame complete")]
99 SocketClosed,
100
101 #[error("operation cancelled")]
102 Cancelled,
103
104 #[error("a steamroom daemon is already running on this socket")]
105 DaemonAlreadyRunning,
106
107 #[error("no daemon running on this socket; start one with `steamroom daemon start`")]
108 NoDaemonRunning,
109
110 #[error("daemon returned error: {0}")]
111 DaemonError(String),
112}
113
114impl From<steamroom::error::ConnectionError> for CliError {
115 fn from(e: steamroom::error::ConnectionError) -> Self {
116 Self::Steam(steamroom::error::Error::Connection(e))
117 }
118}
119
120fn display_login_error(e: &steamroom_client::login::LoginError) -> String {
121 use steamroom_client::login::LoginError;
122 match e {
123 LoginError::InvalidPassword => "invalid password".into(),
124 LoginError::InvalidGuardCode => "two-factor code rejected".into(),
125 LoginError::LogonFailed(r) => format!("login failed: {}", eresult_message(r)),
126 LoginError::Transport(inner) => display_steam_error(inner),
127 LoginError::MissingField(f) => format!("Steam response missing field: {f}"),
128 LoginError::NoCmServers => "could not find any Steam CM servers to connect to".into(),
129 _ => e.to_string(),
130 }
131}
132
133fn display_steam_error(e: &steamroom::error::Error) -> String {
134 use steamroom::error::ConnectionError;
135 use steamroom::error::Error;
136
137 match e {
138 Error::Connection(ConnectionError::LogonFailed(r)) => {
139 format!("login failed: {}", eresult_message(r))
140 }
141 Error::Connection(ConnectionError::ServiceMethodFailed(r)) => {
142 format!("Steam API call failed: {}", eresult_message(r))
143 }
144 Error::Connection(ConnectionError::DepotAccessDenied(depot)) => {
145 format!("access denied for depot {depot} (do you own this app? try logging in with -u)")
146 }
147 Error::Connection(ConnectionError::Disconnected) => "disconnected from Steam".into(),
148 Error::Connection(ConnectionError::DnsResolutionFailed) => {
149 "DNS resolution failed (check your network connection)".into()
150 }
151 Error::Connection(ConnectionError::EncryptionFailed) => {
152 "failed to establish encrypted connection to Steam".into()
153 }
154 Error::Connection(ConnectionError::MissingField(field)) => {
155 format!("Steam response is missing required field: {field}")
156 }
157 Error::CdnStatus { status, .. } => {
158 use reqwest::StatusCode;
159 let code = status.as_u16();
160 if *status == StatusCode::UNAUTHORIZED || *status == StatusCode::FORBIDDEN {
161 format!("CDN access denied (HTTP {code})")
162 } else if *status == StatusCode::NOT_FOUND {
163 "content not found on CDN (HTTP 404)".into()
164 } else if *status == StatusCode::TOO_MANY_REQUESTS {
165 "rate limited by CDN (HTTP 429), retries exhausted".into()
166 } else if status.is_server_error() {
167 format!("CDN server error (HTTP {code})")
168 } else {
169 format!("CDN returned HTTP {code}")
170 }
171 }
172 other => other.to_string(),
173 }
174}
175
176fn display_manifest_error(e: &steamroom::error::ManifestError) -> String {
177 use steamroom::error::ManifestError;
178 match e {
179 ManifestError::MissingSection => "manifest is missing required data sections".into(),
180 ManifestError::ChecksumMismatch { .. } => {
181 "manifest checksum mismatch (corrupt download?)".into()
182 }
183 ManifestError::DecryptFailed(_) => {
184 "failed to decrypt manifest filenames (wrong depot key?)".into()
185 }
186 other => format!("manifest error: {other}"),
187 }
188}
189
190fn display_http_error(e: &reqwest::Error) -> String {
191 if e.is_connect() {
192 format!("connection failed (check your network): {e}")
193 } else if e.is_timeout() {
194 format!("request timed out: {e}")
195 } else {
196 format!("HTTP error: {e}")
197 }
198}
199
200fn eresult_message(r: &steamroom::enums::EResultError) -> &'static str {
201 use steamroom::enums::EResultError;
202 match r {
203 EResultError::InvalidPassword => "invalid password",
204 EResultError::AccessDenied => "access denied",
205 EResultError::Banned => "account is banned",
206 EResultError::AccountNotFound => "account not found",
207 EResultError::InvalidSteamID => "invalid Steam ID",
208 EResultError::ServiceUnavailable => "Steam service is temporarily unavailable",
209 EResultError::Timeout => "request timed out",
210 EResultError::LimitExceeded => "rate limit exceeded, try again later",
211 EResultError::Expired => "session expired, please log in again",
212 EResultError::InsufficientPrivilege => "insufficient privileges",
213 EResultError::NotLoggedOn => "not logged in",
214 EResultError::Busy => "Steam is busy, try again later",
215 EResultError::Revoked => "access has been revoked",
216 EResultError::NoConnection => "no connection to Steam",
217 _ => "unknown error",
218 }
219}