1use std::collections::BTreeMap;
19use std::fmt::Debug;
20use std::fmt::Formatter;
21use std::sync::Arc;
22
23use bytes::Buf;
24use bytes::Bytes;
25use http::header;
26use http::header::IF_MATCH;
27use http::header::IF_NONE_MATCH;
28use http::Request;
29use http::Response;
30use http::StatusCode;
31use serde::Deserialize;
32use serde::Serialize;
33use serde_json::json;
34use tokio::sync::Mutex;
35
36use crate::raw::*;
37use crate::*;
38
39static ACCOUNT_COUNTRY_HEADER: &str = "X-Apple-ID-Account-Country";
40static OAUTH_STATE_HEADER: &str = "X-Apple-OAuth-State";
41static SESSION_ID_HEADER: &str = "X-Apple-ID-Session-Id";
42static SCNT_HEADER: &str = "scnt";
43static SESSION_TOKEN_HEADER: &str = "X-Apple-Session-Token";
44static APPLE_RESPONSE_HEADER: &str = "X-Apple-I-Rscd";
45
46static AUTH_ENDPOINT: &str = "https://idmsa.apple.com/appleauth/auth";
47static SETUP_ENDPOINT: &str = "https://setup.icloud.com/setup/ws/1";
48
49const AUTH_HEADERS: [(&str, &str); 7] = [
50 (
51 "X-Apple-OAuth-Client-Id",
54 "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d",
55 ),
56 ("X-Apple-OAuth-Client-Type", "firstPartyAuth"),
57 ("X-Apple-OAuth-Redirect-URI", "https://www.icloud.com"),
58 ("X-Apple-OAuth-Require-Grant-Code", "true"),
59 ("X-Apple-OAuth-Response-Mode", "web_message"),
60 ("X-Apple-OAuth-Response-Type", "code"),
61 (
62 "X-Apple-Widget-Key",
63 "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d",
64 ),
65];
66
67#[derive(Clone)]
68pub struct SessionData {
69 oauth_state: String,
70 session_id: Option<String>,
71 session_token: Option<String>,
72
73 scnt: Option<String>,
74 account_country: Option<String>,
75 cookies: BTreeMap<String, String>,
76 drivews_url: String,
77 docws_url: String,
78}
79
80impl Default for SessionData {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl SessionData {
87 pub fn new() -> SessionData {
88 Self {
89 oauth_state: format!("auth-{}", uuid::Uuid::new_v4()).to_string(),
90 session_id: None,
91 session_token: None,
92 scnt: None,
93 account_country: None,
94
95 cookies: BTreeMap::default(),
96 drivews_url: String::new(),
97 docws_url: String::new(),
98 }
99 }
100}
101
102#[derive(Clone)]
103pub struct IcloudSigner {
104 pub info: Arc<AccessorInfo>,
105
106 pub apple_id: String,
107 pub password: String,
108 pub is_china_mainland: bool,
109 pub trust_token: Option<String>,
110 pub ds_web_auth_token: Option<String>,
111
112 pub data: SessionData,
113 pub initiated: bool,
114}
115
116impl Debug for IcloudSigner {
117 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118 let mut de = f.debug_struct("icloud signer");
119 de.field("is_china_mainland", &self.is_china_mainland);
120 de.finish()
121 }
122}
123
124impl IcloudSigner {
125 pub async fn drivews_url(&mut self) -> Result<&str> {
128 self.init().await?;
129 Ok(&self.data.drivews_url)
130 }
131
132 pub async fn docws_url(&mut self) -> Result<&str> {
135 self.init().await?;
136 Ok(&self.data.docws_url)
137 }
138
139 pub fn client_id(&self) -> &str {
141 &self.data.oauth_state
142 }
143
144 async fn init(&mut self) -> Result<()> {
145 if self.initiated {
146 return Ok(());
147 }
148
149 let uri = format!("{}/signin?isRememberMeEnable=true", AUTH_ENDPOINT);
151 let body = serde_json::to_vec(&json!({
152 "accountName" : self.apple_id,
153 "password" : self.password,
154 "rememberMe": true,
155 "trustTokens": [self.trust_token.clone().unwrap()],
156 }))
157 .map_err(new_json_serialize_error)?;
158
159 let mut req = Request::post(uri)
160 .header(header::CONTENT_TYPE, "application/json")
161 .body(Buffer::from(Bytes::from(body)))
162 .map_err(new_request_build_error)?;
163 self.sign(&mut req)?;
164
165 let resp = self.info.http_client().send(req).await?;
166 if resp.status() != StatusCode::OK {
167 return Err(parse_error(resp));
168 }
169
170 if let Some(rscd) = resp.headers().get(APPLE_RESPONSE_HEADER) {
171 let status_code = StatusCode::from_bytes(rscd.as_bytes()).unwrap();
172 if status_code != StatusCode::CONFLICT {
173 return Err(parse_error(resp));
174 }
175 }
176
177 let uri = format!("{}/accountLogin", SETUP_ENDPOINT);
179 let body = serde_json::to_vec(&json!({
180 "accountCountryCode": self.data.account_country.clone().unwrap_or_default(),
181 "dsWebAuthToken":self.ds_web_auth_token.clone().unwrap_or_default(),
182 "extended_login": true,
183 "trustToken": self.trust_token.clone().unwrap_or_default(),}))
184 .map_err(new_json_serialize_error)?;
185
186 let mut req = Request::post(uri)
187 .header(header::CONTENT_TYPE, "application/json")
188 .body(Buffer::from(Bytes::from(body)))
189 .map_err(new_request_build_error)?;
190 self.sign(&mut req)?;
191
192 let resp = self.info.http_client().send(req).await?;
193 if resp.status() != StatusCode::OK {
194 return Err(parse_error(resp));
195 }
196
197 self.update(&resp)?;
199
200 let bs = resp.into_body();
201 let auth_info: IcloudWebservicesResponse =
202 serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
203
204 if auth_info.hsa_challenge_required && !auth_info.hsa_trusted_browser {
206 return Err(Error::new(ErrorKind::Unexpected, "Apple icloud AuthenticationFailed:Unauthorized request:Needs two-factor authentication"));
207 }
208
209 if let Some(v) = &auth_info.webservices.drivews.url {
210 self.data.drivews_url = v.to_string();
211 }
212 if let Some(v) = &auth_info.webservices.docws.url {
213 self.data.docws_url = v.to_string();
214 }
215
216 self.initiated = true;
217 Ok(())
218 }
219
220 fn sign<T>(&mut self, req: &mut Request<T>) -> Result<()> {
221 let headers = req.headers_mut();
222
223 headers.insert(
224 OAUTH_STATE_HEADER,
225 build_header_value(&self.data.oauth_state)?,
226 );
227
228 if let Some(session_id) = &self.data.session_id {
229 headers.insert(SESSION_ID_HEADER, build_header_value(session_id)?);
230 }
231 if let Some(scnt) = &self.data.scnt {
232 headers.insert(SCNT_HEADER, build_header_value(scnt)?);
233 }
234
235 if self.is_china_mainland {
237 headers.insert(
238 header::ORIGIN,
239 build_header_value("https://www.icloud.com.cn")?,
240 );
241 headers.insert(
242 header::REFERER,
243 build_header_value("https://www.icloud.com.cn/")?,
244 );
245 } else {
246 headers.insert(
247 header::ORIGIN,
248 build_header_value("https://www.icloud.com")?,
249 );
250 headers.insert(
251 header::REFERER,
252 build_header_value("https://www.icloud.com/")?,
253 );
254 }
255
256 if !self.data.cookies.is_empty() {
257 let cookies: Vec<String> = self
258 .data
259 .cookies
260 .iter()
261 .map(|(k, v)| format!("{}={}", k, v))
262 .collect();
263 headers.insert(
264 header::COOKIE,
265 build_header_value(&cookies.as_slice().join("; "))?,
266 );
267 }
268
269 for (key, value) in AUTH_HEADERS {
270 headers.insert(key, build_header_value(value)?);
271 }
272
273 Ok(())
274 }
275
276 fn update(&mut self, resp: &Response<Buffer>) -> Result<()> {
278 if let Some(account_country) = parse_header_to_str(resp.headers(), ACCOUNT_COUNTRY_HEADER)?
279 {
280 self.data.account_country = Some(account_country.to_string());
281 }
282 if let Some(session_id) = parse_header_to_str(resp.headers(), SESSION_ID_HEADER)? {
283 self.data.session_id = Some(session_id.to_string());
284 }
285 if let Some(session_token) = parse_header_to_str(resp.headers(), SESSION_TOKEN_HEADER)? {
286 self.data.session_token = Some(session_token.to_string());
287 }
288
289 if let Some(scnt) = parse_header_to_str(resp.headers(), SCNT_HEADER)? {
290 self.data.scnt = Some(scnt.to_string());
291 }
292
293 let cookies: Vec<String> = resp
294 .headers()
295 .get_all(header::SET_COOKIE)
296 .iter()
297 .map(|v| v.to_str().unwrap().to_string())
298 .collect();
299
300 for cookie in cookies {
301 if let Some((key, value)) = cookie.split_once('=') {
302 self.data.cookies.insert(key.into(), value.into());
303 }
304 }
305
306 Ok(())
307 }
308
309 pub async fn send(&mut self, mut req: Request<Buffer>) -> Result<Response<Buffer>> {
315 self.sign(&mut req)?;
316 let resp = self.info.http_client().send(req).await?;
317
318 Ok(resp)
319 }
320}
321
322pub struct IcloudCore {
323 pub info: Arc<AccessorInfo>,
324 pub signer: Arc<Mutex<IcloudSigner>>,
325 pub root: String,
326 pub path_cache: PathCacher<IcloudPathQuery>,
327}
328
329impl Debug for IcloudCore {
330 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
331 let mut de = f.debug_struct("IcloudCore");
332 de.field("root", &self.root);
333 de.finish()
334 }
335}
336
337impl IcloudCore {
338 pub async fn get_root(&self, id: &str) -> Result<IcloudRoot> {
341 let mut signer = self.signer.lock().await;
342
343 let uri = format!(
344 "{}/retrieveItemDetailsInFolders",
345 signer.drivews_url().await?
346 );
347
348 let body = serde_json::to_vec(&json!([
349 {
350 "drivewsid": id,
351 "partialData": false
352 }
353 ]))
354 .map_err(new_json_serialize_error)?;
355
356 let req = Request::post(uri)
357 .body(Buffer::from(Bytes::from(body)))
358 .map_err(new_request_build_error)?;
359
360 let resp = signer.send(req).await?;
361 if resp.status() != StatusCode::OK {
362 return Err(parse_error(resp));
363 }
364
365 let body = resp.into_body();
366 let drive_node: Vec<IcloudRoot> =
367 serde_json::from_slice(body.chunk()).map_err(new_json_deserialize_error)?;
368 Ok(drive_node[0].clone())
369 }
370
371 pub async fn get_file(
372 &self,
373 id: &str,
374 zone: &str,
375 range: BytesRange,
376 args: OpRead,
377 ) -> Result<Response<HttpBody>> {
378 let mut signer = self.signer.lock().await;
379
380 let uri = format!(
381 "{}/ws/{}/download/by_id?document_id={}",
382 signer.docws_url().await?,
383 zone,
384 id
385 );
386
387 let req = Request::get(uri)
388 .body(Buffer::new())
389 .map_err(new_request_build_error)?;
390
391 let resp = signer.send(req).await?;
392 if resp.status() != StatusCode::OK {
393 return Err(parse_error(resp));
394 }
395
396 let body = resp.into_body();
397 let object: IcloudObject =
398 serde_json::from_slice(body.chunk()).map_err(new_json_deserialize_error)?;
399
400 let url = object.data_token.url.to_string();
401
402 let mut req = Request::get(url);
403
404 if let Some(if_match) = args.if_match() {
405 req = req.header(IF_MATCH, if_match);
406 }
407
408 if range.is_full() {
409 req = req.header(header::RANGE, range.to_header())
410 }
411 if let Some(if_none_match) = args.if_none_match() {
412 req = req.header(IF_NONE_MATCH, if_none_match);
413 }
414
415 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
416
417 let resp = self.info.http_client().fetch(req).await?;
418
419 Ok(resp)
420 }
421
422 pub async fn read(
423 &self,
424 path: &str,
425 range: BytesRange,
426 args: &OpRead,
427 ) -> Result<Response<HttpBody>> {
428 let path = build_rooted_abs_path(&self.root, path);
429 let base = get_basename(&path);
430
431 let path_id = self.path_cache.get(base).await?.ok_or(Error::new(
432 ErrorKind::NotFound,
433 format!("read path not found: {}", base),
434 ))?;
435
436 if let Some(docwsid) = path_id.strip_prefix("FILE::com.apple.CloudDocs::") {
437 Ok(self
438 .get_file(docwsid, "com.apple.CloudDocs", range, args.clone())
439 .await?)
440 } else {
441 Err(Error::new(
442 ErrorKind::NotFound,
443 "icloud DriveService read error",
444 ))
445 }
446 }
447
448 pub async fn stat(&self, path: &str) -> Result<IcloudItem> {
449 let path = build_rooted_abs_path(&self.root, path);
450
451 let mut base = get_basename(&path);
452 let parent = get_parent(&path);
453
454 if base.ends_with('/') {
455 base = base.trim_end_matches('/');
456 }
457
458 let file_id = self.path_cache.get(base).await?.ok_or(Error::new(
459 ErrorKind::NotFound,
460 format!("stat path not found: {}", base),
461 ))?;
462
463 let folder_id = self.path_cache.get(parent).await?.ok_or(Error::new(
464 ErrorKind::NotFound,
465 format!("stat path not found: {}", parent),
466 ))?;
467
468 let node = self.get_root(&folder_id).await?;
469
470 match node.items.iter().find(|it| it.drivewsid == file_id.clone()) {
471 Some(it) => Ok(it.clone()),
472 None => Err(Error::new(
473 ErrorKind::NotFound,
474 "icloud DriveService stat get parent items error",
475 )),
476 }
477 }
478}
479
480pub struct IcloudPathQuery {
481 pub signer: Arc<Mutex<IcloudSigner>>,
482}
483
484impl IcloudPathQuery {
485 pub fn new(signer: Arc<Mutex<IcloudSigner>>) -> Self {
486 IcloudPathQuery { signer }
487 }
488}
489
490impl PathQuery for IcloudPathQuery {
491 async fn root(&self) -> Result<String> {
492 Ok("FOLDER::com.apple.CloudDocs::root".to_string())
493 }
494
495 async fn query(&self, parent_id: &str, name: &str) -> Result<Option<String>> {
500 let mut signer = self.signer.lock().await;
501
502 let uri = format!(
503 "{}/retrieveItemDetailsInFolders",
504 signer.drivews_url().await?
505 );
506
507 let body = serde_json::to_vec(&json!([
508 {
509 "drivewsid": parent_id,
510 "partialData": false
511 }
512 ]))
513 .map_err(new_json_serialize_error)?;
514
515 let req = Request::post(uri)
516 .body(Buffer::from(Bytes::from(body)))
517 .map_err(new_request_build_error)?;
518
519 let resp = signer.send(req).await?;
520 if resp.status() != StatusCode::OK {
521 return Err(parse_error(resp));
522 }
523
524 let body = resp.into_body();
525 let root: Vec<IcloudRoot> =
526 serde_json::from_slice(body.chunk()).map_err(new_json_deserialize_error)?;
527
528 let node = &root[0];
529
530 Ok(node
531 .items
532 .iter()
533 .find(|it| it.name == name)
534 .map(|it| it.drivewsid.clone()))
535 }
536
537 async fn create_dir(&self, parent_id: &str, name: &str) -> Result<String> {
538 let mut signer = self.signer.lock().await;
539
540 let client_id = signer.client_id().to_string();
541
542 let uri = format!("{}/createFolders", signer.drivews_url().await?);
543 let body = serde_json::to_vec(&json!(
544 {
545 "destinationDrivewsId": parent_id,
546 "folders": [
547 {
548 "clientId": client_id,
549 "name": name,
550 }
551 ],
552 }
553 ))
554 .map_err(new_json_serialize_error)?;
555
556 let req = Request::post(uri)
557 .body(Buffer::from(Bytes::from(body)))
558 .map_err(new_request_build_error)?;
559
560 let resp = signer.send(req).await?;
561 if resp.status() != StatusCode::OK {
562 return Err(parse_error(resp));
563 }
564
565 let body = resp.into_body();
566 let create_folder: IcloudCreateFolder =
567 serde_json::from_slice(body.chunk()).map_err(new_json_deserialize_error)?;
568 Ok(create_folder.destination_drivews_id)
569 }
570}
571
572pub(super) fn parse_error(resp: Response<Buffer>) -> Error {
573 let (parts, body) = resp.into_parts();
574 let bs = body.to_bytes();
575
576 let mut kind = match parts.status.as_u16() {
577 421 | 450 | 500 => ErrorKind::NotFound,
578 401 => ErrorKind::Unexpected,
579 _ => ErrorKind::Unexpected,
580 };
581
582 let (message, icloud_err) = serde_json::from_reader::<_, IcloudError>(bs.clone().reader())
583 .map(|icloud_err| (format!("{icloud_err:?}"), Some(icloud_err)))
584 .unwrap_or_else(|_| (String::from_utf8_lossy(&bs).into_owned(), None));
585
586 if let Some(icloud_err) = &icloud_err {
587 kind = match icloud_err.status_code.as_str() {
588 "NOT_FOUND" => ErrorKind::NotFound,
589 "PERMISSION_DENIED" => ErrorKind::PermissionDenied,
590 _ => ErrorKind::Unexpected,
591 }
592 }
593
594 let mut err = Error::new(kind, message);
595
596 err = with_error_response_context(err, parts);
597
598 err
599}
600
601#[derive(Default, Debug, Deserialize)]
602#[allow(dead_code)]
603struct IcloudError {
604 status_code: String,
605 message: String,
606}
607
608#[derive(Default, Deserialize, Clone)]
609#[serde(rename_all = "camelCase")]
610pub struct IcloudWebservicesResponse {
611 #[serde(default)]
612 pub hsa_challenge_required: bool,
613 #[serde(default)]
614 pub hsa_trusted_browser: bool,
615 pub webservices: Webservices,
616}
617
618#[derive(Deserialize, Default, Clone, Debug)]
619pub struct Webservices {
620 pub drivews: Drivews,
621 pub docws: Docws,
622}
623
624#[derive(Deserialize, Default, Clone, Debug)]
625#[serde(rename_all = "camelCase")]
626pub struct Drivews {
627 pub url: Option<String>,
628}
629
630#[derive(Deserialize, Default, Clone, Debug)]
631#[serde(rename_all = "camelCase")]
632pub struct Docws {
633 pub url: Option<String>,
634}
635
636#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct IcloudRoot {
639 #[serde(default)]
640 pub asset_quota: i64,
641 #[serde(default)]
642 pub date_created: String,
643 #[serde(default)]
644 pub direct_children_count: i64,
645 pub docwsid: String,
646 pub drivewsid: String,
647 pub etag: String,
648 #[serde(default)]
649 pub file_count: i64,
650 pub items: Vec<IcloudItem>,
651 pub name: String,
652 pub number_of_items: i64,
653 pub status: String,
654 #[serde(rename = "type")]
655 pub type_field: String,
656 pub zone: String,
657}
658
659#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase")]
661pub struct IcloudItem {
662 #[serde(default)]
663 pub asset_quota: Option<i64>,
664 #[serde(default)]
665 pub date_created: String,
666 #[serde(default)]
667 pub date_modified: String,
668 #[serde(default)]
669 pub direct_children_count: Option<i64>,
670 pub docwsid: String,
671 pub drivewsid: String,
672 pub etag: String,
673 #[serde(default)]
674 pub file_count: Option<i64>,
675 pub item_id: Option<String>,
676 pub name: String,
677 pub parent_id: String,
678 #[serde(default)]
679 pub size: u64,
680 #[serde(rename = "type")]
681 pub type_field: String,
682 pub zone: String,
683 #[serde(default)]
684 pub max_depth: Option<String>,
685 #[serde(default)]
686 pub is_chained_to_parent: Option<bool>,
687}
688
689#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
690pub struct IcloudObject {
691 pub document_id: String,
692 pub item_id: String,
693 pub owner_dsid: i64,
694 pub data_token: DataToken,
695 pub double_etag: String,
696}
697
698#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
699pub struct DataToken {
700 pub url: String,
701 pub token: String,
702 pub signature: String,
703 pub wrapping_key: String,
704 pub reference_signature: String,
705}
706
707#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
708pub struct IcloudCreateFolder {
709 pub destination_drivews_id: String,
710 pub folders: Vec<IcloudItem>,
711}
712
713#[cfg(test)]
714mod tests {
715 use pretty_assertions::assert_eq;
716
717 use super::IcloudRoot;
718 use super::IcloudWebservicesResponse;
719
720 #[test]
721 fn test_parse_icloud_drive_root_json() {
722 let data = r#"{
723 "assetQuota": 19603579,
724 "dateCreated": "2019-06-10T14:17:49Z",
725 "directChildrenCount": 3,
726 "docwsid": "root",
727 "drivewsid": "FOLDER::com.apple.CloudDocs::root",
728 "etag": "w7",
729 "fileCount": 22,
730 "items": [
731 {
732 "assetQuota": 19603579,
733 "dateCreated": "2021-02-05T08:30:58Z",
734 "directChildrenCount": 22,
735 "docwsid": "1E013608-C669-43DB-AC14-3D7A4E0A0500",
736 "drivewsid": "FOLDER::com.apple.CloudDocs::1E013608-C669-43DB-AC14-3D7A4E0A0500",
737 "etag": "sn",
738 "fileCount": 22,
739 "item_id": "CJWdk48eEAAiEB4BNgjGaUPbrBQ9ek4KBQAoAQ",
740 "name": "Downloads",
741 "parentId": "FOLDER::com.apple.CloudDocs::root",
742 "shareAliasCount": 0,
743 "shareCount": 0,
744 "type": "FOLDER",
745 "zone": "com.apple.CloudDocs"
746 },
747 {
748 "dateCreated": "2019-06-10T14:17:54Z",
749 "docwsid": "documents",
750 "drivewsid": "FOLDER::com.apple.Keynote::documents",
751 "etag": "1v",
752 "maxDepth": "ANY",
753 "name": "Keynote",
754 "parentId": "FOLDER::com.apple.CloudDocs::root",
755 "type": "APP_LIBRARY",
756 "zone": "com.apple.Keynote"
757 },
758 {
759 "assetQuota": 0,
760 "dateCreated": "2024-01-06T02:35:08Z",
761 "directChildrenCount": 0,
762 "docwsid": "21E4A15E-DA77-472A-BAC8-B0C35A91F237",
763 "drivewsid": "FOLDER::com.apple.CloudDocs::21E4A15E-DA77-472A-BAC8-B0C35A91F237",
764 "etag": "w8",
765 "fileCount": 0,
766 "isChainedToParent": true,
767 "item_id": "CJWdk48eEAAiECHkoV7ad0cqusiww1qR8jcoAQ",
768 "name": "opendal",
769 "parentId": "FOLDER::com.apple.CloudDocs::root",
770 "shareAliasCount": 0,
771 "shareCount": 0,
772 "type": "FOLDER",
773 "zone": "com.apple.CloudDocs"
774 }
775 ],
776 "name": "",
777 "numberOfItems": 16,
778 "shareAliasCount": 0,
779 "shareCount": 0,
780 "status": "OK",
781 "type": "FOLDER",
782 "zone": "com.apple.CloudDocs"
783 }"#;
784
785 let response: IcloudRoot = serde_json::from_str(data).unwrap();
786 assert_eq!(response.name, "");
787 assert_eq!(response.type_field, "FOLDER");
788 assert_eq!(response.zone, "com.apple.CloudDocs");
789 assert_eq!(response.docwsid, "root");
790 assert_eq!(response.drivewsid, "FOLDER::com.apple.CloudDocs::root");
791 assert_eq!(response.etag, "w7");
792 assert_eq!(response.file_count, 22);
793 }
794
795 #[test]
796 fn test_parse_icloud_drive_folder_file() {
797 let data = r#"{
798 "assetQuota": 19603579,
799 "dateCreated": "2021-02-05T08:34:21Z",
800 "directChildrenCount": 22,
801 "docwsid": "1E013608-C669-43DB-AC14-3D7A4E0A0500",
802 "drivewsid": "FOLDER::com.apple.CloudDocs::1E013608-C669-43DB-AC14-3D7A4E0A0500",
803 "etag": "w9",
804 "fileCount": 22,
805 "items": [
806 {
807 "dateChanged": "2021-02-18T14:10:46Z",
808 "dateCreated": "2021-02-10T07:01:34Z",
809 "dateModified": "2021-02-10T07:01:34Z",
810 "docwsid": "9605331E-7BF3-41A0-A128-A68FFA377C50",
811 "drivewsid": "FILE::com.apple.CloudDocs::9605331E-7BF3-41A0-A128-A68FFA377C50",
812 "etag": "5b::5a",
813 "extension": "pdf",
814 "item_id": "CJWdk48eEAAiEJYFMx5780GgoSimj_o3fFA",
815 "lastOpenTime": "2021-02-10T10:28:42Z",
816 "name": "1-11-ARP-notes",
817 "parentId": "FOLDER::com.apple.CloudDocs::1E013608-C669-43DB-AC14-3D7A4E0A0500",
818 "size": 639483,
819 "type": "FILE",
820 "zone": "com.apple.CloudDocs"
821 }
822 ],
823 "name": "Downloads",
824 "numberOfItems": 22,
825 "parentId": "FOLDER::com.apple.CloudDocs::root",
826 "shareAliasCount": 0,
827 "shareCount": 0,
828 "status": "OK",
829 "type": "FOLDER",
830 "zone": "com.apple.CloudDocs"
831 }"#;
832
833 let response = serde_json::from_str::<IcloudRoot>(data).unwrap();
834
835 assert_eq!(response.name, "Downloads");
836 assert_eq!(response.type_field, "FOLDER");
837 assert_eq!(response.zone, "com.apple.CloudDocs");
838 assert_eq!(response.docwsid, "1E013608-C669-43DB-AC14-3D7A4E0A0500");
839 assert_eq!(
840 response.drivewsid,
841 "FOLDER::com.apple.CloudDocs::1E013608-C669-43DB-AC14-3D7A4E0A0500"
842 );
843 assert_eq!(response.etag, "w9");
844 assert_eq!(response.file_count, 22);
845 }
846
847 #[test]
848 fn test_parse_icloud_webservices() {
849 let data = r#"
850 {
851 "hsaChallengeRequired": false,
852 "hsaTrustedBrowser": true,
853 "webservices": {
854 "docws": {
855 "pcsRequired": true,
856 "status": "active",
857 "url": "https://p219-docws.icloud.com.cn:443"
858 },
859 "drivews": {
860 "pcsRequired": true,
861 "status": "active",
862 "url": "https://p219-drivews.icloud.com.cn:443"
863 }
864 }
865 }
866 "#;
867 let response = serde_json::from_str::<IcloudWebservicesResponse>(data).unwrap();
868 assert!(!response.hsa_challenge_required);
869 assert!(response.hsa_trusted_browser);
870 assert_eq!(
871 response.webservices.docws.url,
872 Some("https://p219-docws.icloud.com.cn:443".to_string())
873 );
874 assert_eq!(
875 response.webservices.drivews.url,
876 Some("https://p219-drivews.icloud.com.cn:443".to_string())
877 );
878 }
879}