Skip to main content

steamroom_cli/
errors.rs

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}