1use http::StatusCode;
4use reqwest::Url;
5use std::{
6 fmt::{self, Display},
7 io,
8};
9use thiserror::Error;
10
11#[cfg(feature = "decode")]
12use crate::decode::{InnerItemError, InnerListError};
13use crate::{
14 auth::AuthError,
15 bucket::{BucketError, ExtractItemError},
16 builder::BuilderError,
17 config::InvalidConfig,
18 file::FileError,
19 object::{ExtractListError, ObjectListError},
20 types::{
21 object::{InvalidObjectDir, InvalidObjectPath},
22 InvalidBucketName, InvalidEndPoint,
23 },
24};
25
26#[derive(Debug)]
28#[non_exhaustive]
29pub struct OssError {
30 kind: OssErrorKind,
31}
32
33impl Display for OssError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 self.kind.fmt(f)
36 }
37}
38impl std::error::Error for OssError {
39 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40 use OssErrorKind::*;
41 match &self.kind {
42 Io(e) => Some(e),
43 #[cfg(test)]
44 Dotenv(e) => Some(e),
45 Builder(e) => Some(e),
46 EndPoint(e) => Some(e),
47 BucketName(e) => Some(e),
48 Config(e) => Some(e),
49 ObjectPath(e) => Some(e),
50 ObjectDir(e) => Some(e),
51 BuildInItemError(e) => Some(e),
52 InnerList(e) => e.get_source(),
53 InnerItem(e) => e.get_source(),
54 ExtractList(e) => Some(e),
55 ExtractItem(e) => Some(e),
56 File(e) => Some(e),
57 Auth(e) => Some(e),
58 Bucket(e) => Some(e),
59 ObjectList(e) => Some(e),
60 }
61 }
62}
63
64impl<T: Into<OssErrorKind>> From<T> for OssError {
65 fn from(value: T) -> Self {
66 Self { kind: value.into() }
67 }
68}
69
70#[derive(Debug, Error)]
72#[non_exhaustive]
73enum OssErrorKind {
74 #[error("io error")]
75 Io(#[from] io::Error),
76
77 #[doc(hidden)]
78 #[error("dotenv error")]
79 #[cfg(test)]
80 Dotenv(#[from] dotenv::Error),
81
82 #[doc(hidden)]
83 #[error("builder error")]
84 Builder(#[from] BuilderError),
85
86 #[doc(hidden)]
87 #[error("invalid endpoint")]
88 EndPoint(#[from] InvalidEndPoint),
89
90 #[doc(hidden)]
91 #[error("invalid bucket name")]
92 BucketName(#[from] InvalidBucketName),
93
94 #[doc(hidden)]
95 #[error("invalid config")]
96 Config(#[from] InvalidConfig),
97
98 #[doc(hidden)]
99 #[error("invalid object path")]
100 ObjectPath(#[from] InvalidObjectPath),
101
102 #[doc(hidden)]
103 #[error("invalid object dir")]
104 ObjectDir(#[from] InvalidObjectDir),
105
106 #[doc(hidden)]
107 #[error("build in item error")]
108 BuildInItemError(#[from] crate::object::BuildInItemError),
109
110 #[cfg(feature = "decode")]
111 #[doc(hidden)]
112 #[error("decode into list error")]
113 InnerList(InnerListError),
114
115 #[cfg(feature = "decode")]
116 #[doc(hidden)]
117 #[error("decode into list error")]
118 InnerItem(InnerItemError),
119
120 #[doc(hidden)]
121 #[error("extract list error")]
122 ExtractList(#[from] ExtractListError),
123
124 #[doc(hidden)]
125 #[error("extract item error")]
126 ExtractItem(#[from] ExtractItemError),
127
128 #[error("file error")]
129 File(#[from] FileError),
130
131 #[error("auth error")]
132 Auth(#[from] AuthError),
133
134 #[error("bucket error")]
136 Bucket(#[from] BucketError),
137
138 #[error("object list error")]
139 ObjectList(#[from] ObjectListError),
140}
141
142#[cfg(feature = "decode")]
143impl From<InnerListError> for OssErrorKind {
144 fn from(value: InnerListError) -> Self {
145 Self::InnerList(value)
146 }
147}
148
149#[cfg(feature = "decode")]
150impl From<InnerItemError> for OssErrorKind {
151 fn from(value: InnerItemError) -> Self {
152 Self::InnerItem(value)
153 }
154}
155
156#[derive(Debug, Error, PartialEq, Eq, Hash)]
161pub struct OssService {
162 pub(crate) code: String,
163 status: StatusCode,
164 message: String,
165 request_id: String,
166 url: Url,
167}
168
169impl Default for OssService {
170 fn default() -> Self {
171 Self {
172 code: "Undefined".to_owned(),
173 status: StatusCode::default(),
174 message: "Parse aliyun response xml error message failed.".to_owned(),
175 request_id: "XXXXXXXXXXXXXXXXXXXXXXXX".to_owned(),
176 url: {
177 #[allow(clippy::unwrap_used)]
178 "https://oss.aliyuncs.com".parse().unwrap()
179 },
180 }
181 }
182}
183
184impl fmt::Display for OssService {
185 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186 f.debug_struct("OssService")
187 .field("code", &self.code)
188 .field("status", &self.status)
189 .field("message", &self.message)
190 .field("request_id", &self.request_id)
191 .field("url", &self.url.as_str())
192 .finish()
193 }
194}
195
196impl AsRef<Url> for OssService {
197 fn as_ref(&self) -> &Url {
198 &self.url
199 }
200}
201
202impl<'a> OssService {
203 pub fn new(source: &'a str, status: &StatusCode, url: &Url) -> Self {
205 match (
206 source.find("<Code>"),
207 source.find("</Code>"),
208 source.find("<Message>"),
209 source.find("</Message>"),
210 source.find("<RequestId>"),
211 source.find("</RequestId>"),
212 ) {
213 (
214 Some(code0),
215 Some(code1),
216 Some(message0),
217 Some(message1),
218 Some(request_id0),
219 Some(request_id1),
220 ) => {
221 let code = &source[code0 + 6..code1].to_owned();
222 let mut message = source[message0 + 9..message1].to_owned();
223 if code == "SignatureDoesNotMatch" {
224 message.push_str(&format!(
225 "expect sign string is \"{}\"",
226 Self::sign_string(source)
227 ));
228 }
229 Self {
230 code: code.to_owned(),
231 status: *status,
232 message,
233 request_id: source[request_id0 + 11..request_id1].to_owned(),
234 url: url.to_owned(),
235 }
236 }
237 _ => Self::default(),
238 }
239 }
240
241 pub fn new2(source: String, status: &StatusCode, url: Url) -> Self {
243 match (
244 source.find("<Code>"),
245 source.find("</Code>"),
246 source.find("<Message>"),
247 source.find("</Message>"),
248 source.find("<RequestId>"),
249 source.find("</RequestId>"),
250 ) {
251 (
252 Some(code0),
253 Some(code1),
254 Some(message0),
255 Some(message1),
256 Some(request_id0),
257 Some(request_id1),
258 ) => {
259 let code = &source[code0 + 6..code1].to_owned();
260 let mut message = source[message0 + 9..message1].to_owned();
261 if code == "SignatureDoesNotMatch" {
262 message.push_str(&format!(
263 "expect sign string is \"{}\"",
264 Self::sign_string(&source)
265 ));
266 }
267 Self {
268 code: code.to_owned(),
269 status: *status,
270 message,
271 request_id: source[request_id0 + 11..request_id1].to_owned(),
272 url,
273 }
274 }
275 _ => Self::default(),
276 }
277 }
278
279 pub fn url(&self) -> &Url {
281 &self.url
282 }
283
284 fn sign_string(s: &str) -> &str {
285 if let (Some(start), Some(end)) = (s.find("<StringToSign>"), s.find("</StringToSign>")) {
286 &s[start + 14..end]
287 } else {
288 &s[0..0]
289 }
290 }
291}
292
293impl From<OssService> for std::io::Error {
294 fn from(err: OssService) -> Self {
295 use std::io::ErrorKind;
296 let kind = if err.status.is_client_error() {
297 ErrorKind::PermissionDenied
298 } else if err.status.is_server_error() {
299 ErrorKind::ConnectionReset
300 } else {
301 ErrorKind::ConnectionAborted
302 };
303 Self::new(kind, err)
304 }
305}
306
307pub type OssResult<T> = Result<T, OssError>;
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn oss_service_display() {
316 assert_eq!(
317 format!(
318 "{}",
319 OssService {
320 code: "abc".to_owned(),
321 status: StatusCode::OK,
322 message: "mes1".to_owned(),
323 request_id: "xx".to_owned(),
324 url: "https://oss.aliyuncs.com".parse().unwrap()
325 }
326 ),
327 "OssService { code: \"abc\", status: 200, message: \"mes1\", request_id: \"xx\", url: \"https://oss.aliyuncs.com/\" }"
328 );
329 }
330
331 #[test]
332 fn oss_service_default() {
333 let oss = OssService::default();
334 assert_eq!(oss.code, "Undefined".to_string());
335 assert_eq!(oss.status, StatusCode::OK);
336 assert_eq!(
337 oss.message,
338 "Parse aliyun response xml error message failed.".to_owned()
339 );
340 assert_eq!(oss.request_id, "XXXXXXXXXXXXXXXXXXXXXXXX".to_owned());
341 }
342
343 #[test]
344 fn test_oss_service_fmt() {
345 let oss_err = OssService {
346 code: "OSS_TEST_CODE".to_string(),
347 status: StatusCode::default(),
348 message: "foo_msg".to_string(),
349 request_id: "foo_req_id".to_string(),
350 url: "https://oss.aliyuncs.com".parse().unwrap(),
351 };
352
353 assert_eq!(
354 format!("{}", oss_err),
355 "OssService { code: \"OSS_TEST_CODE\", status: 200, message: \"foo_msg\", request_id: \"foo_req_id\", url: \"https://oss.aliyuncs.com/\" }"
356 .to_string()
357 );
358 let url = oss_err.as_ref();
359 assert_eq!(*url, Url::parse("https://oss.aliyuncs.com").unwrap());
360 let url = oss_err.url();
361 assert_eq!(*url, Url::parse("https://oss.aliyuncs.com").unwrap());
362 }
363
364 #[test]
365 fn test_oss_service_new() {
366 let content = r#"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
367 <Error>
368 <Code>RequestTimeTooSkewed</Code>
369 <Message>bar</Message>
370 <RequestId>63145DB90BFD85303279D56B</RequestId>
371 <HostId>xxx.oss-cn-shanghai.aliyuncs.com</HostId>
372 <MaxAllowedSkewMilliseconds>900000</MaxAllowedSkewMilliseconds>
373 <RequestTime>2022-09-04T07:11:33.000Z</RequestTime>
374 <ServerTime>2022-09-04T08:11:37.000Z</ServerTime>
375 </Error>
376 "#;
377 let url = "https://oss.aliyuncs.com".parse().unwrap();
378 let service = OssService::new(content, &StatusCode::default(), &url);
379 assert_eq!(service.code, format!("RequestTimeTooSkewed"));
380 assert_eq!(service.message, format!("bar"));
381 assert_eq!(service.request_id, format!("63145DB90BFD85303279D56B"))
382 }
383
384 #[test]
385 fn test_sign_match() {
386 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
387 <Error>
388 <Code>SignatureDoesNotMatch</Code>
389 <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
390 <RequestId>64C9CF1C8B62C239371D3E6B</RequestId>
391 <HostId>xxx.oss-cn-shanghai.aliyuncs.com</HostId>
392 <OSSAccessKeyId>9js44GwYF9P2ZFs4</OSSAccessKeyId>
393 <SignatureProvided>ZZ3e/hrGjFpOxRkDg+ugKVGMyoc=</SignatureProvided>
394 <StringToSign>PUT
395
396
397Wed, 02 Aug 2023 03:35:56 GMT
398/xxx/aaabbb.txt?partNumber=1</StringToSign>
399 <StringToSignBytes>50 55 54 0A 0A 0A 57 65 64 2C 20 30 32 20 41 75 67 20 32 30 32 33 20 30 33 3A 33 35 3A 35 36 20 47 4D 54 0A 2F 68 6F 6E 67 6C 65 69 31 32 33 2F 61 61 61 62 62 62 2E 74 78 74 3F 70 61 72 74 4E 75 6D 62 65 72 3D 31 </StringToSignBytes>
400 <EC>0002-00000040</EC>
401 </Error>"#;
402 let url = "https://oss.aliyuncs.com".parse().unwrap();
403 let service = OssService::new(xml, &StatusCode::default(), &url);
404 assert_eq!(service.code, format!("SignatureDoesNotMatch"));
405 assert_eq!(service.message, format!("The request signature we calculated does not match the signature you provided. Check your key and signing method.expect sign string is \"PUT\n\n\nWed, 02 Aug 2023 03:35:56 GMT\n/xxx/aaabbb.txt?partNumber=1\""));
406 assert_eq!(service.request_id, format!("64C9CF1C8B62C239371D3E6B"));
407
408 let service = OssService::new2(xml.to_owned(), &StatusCode::default(), url);
409 assert_eq!(service.code, format!("SignatureDoesNotMatch"));
410 assert_eq!(service.message, format!("The request signature we calculated does not match the signature you provided. Check your key and signing method.expect sign string is \"PUT\n\n\nWed, 02 Aug 2023 03:35:56 GMT\n/xxx/aaabbb.txt?partNumber=1\""));
411 assert_eq!(service.request_id, format!("64C9CF1C8B62C239371D3E6B"))
412 }
413
414 #[test]
415 fn oss_service_new() {
416 let url = "https://oss.aliyuncs.com".parse().unwrap();
417 assert_eq!(
418 OssService::new("abc", &StatusCode::OK, &url),
419 OssService::default()
420 );
421
422 assert_eq!(
423 OssService::new2("abc".to_string(), &StatusCode::OK, url),
424 OssService::default()
425 );
426 }
427
428 #[test]
429 fn sign_string() {
430 assert_eq!(OssService::sign_string("aaa"), "");
431 }
432
433 #[test]
434 fn from_oss_service() {
435 use std::io::{Error, ErrorKind};
436 let url: Url = "https://oss.aliyuncs.com".parse().unwrap();
437 let oss = OssService {
438 code: "aaa".to_string(),
439 message: "aaa".to_string(),
440 status: StatusCode::BAD_REQUEST,
441 request_id: "bbb".to_string(),
442 url: url.clone(),
443 };
444 let io_err = Error::from(oss);
445 assert_eq!(io_err.kind(), ErrorKind::PermissionDenied);
446
447 let oss = OssService {
448 code: "aaa".to_string(),
449 message: "aaa".to_string(),
450 status: StatusCode::BAD_GATEWAY,
451 request_id: "bbb".to_string(),
452 url: url.clone(),
453 };
454 let io_err = Error::from(oss);
455 assert_eq!(io_err.kind(), ErrorKind::ConnectionReset);
456
457 let oss = OssService {
458 code: "aaa".to_string(),
459 message: "aaa".to_string(),
460 status: StatusCode::NOT_MODIFIED,
461 request_id: "bbb".to_string(),
462 url: url.clone(),
463 };
464 let io_err = Error::from(oss);
465 assert_eq!(io_err.kind(), ErrorKind::ConnectionAborted);
466 }
467}