1#![allow(dead_code)]
9
10use crate::os_lib::json::{self, JsonValue};
11use alloc::format;
12use alloc::string::{String, ToString};
13use alloc::vec::Vec;
14
15fn obj_get<'a>(v: &'a JsonValue, key: &str) -> Option<&'a JsonValue> {
18 if let JsonValue::Object(m) = v {
19 m.get(key)
20 } else {
21 None
22 }
23}
24
25fn val_to_string(v: &JsonValue) -> Option<String> {
26 match v {
27 JsonValue::String(s) => Some(s.clone()),
28 JsonValue::Number(n) => Some(crate::os_lib::js::value::fmt_number(*n)),
29 JsonValue::Bool(b) => Some(b.to_string()),
30 _ => None,
31 }
32}
33
34fn field_str(v: &JsonValue, key: &str) -> String {
35 obj_get(v, key).and_then(val_to_string).unwrap_or_default()
36}
37
38fn field_str_any(v: &JsonValue, keys: &[&str]) -> String {
40 for k in keys {
41 if let Some(found) = obj_get(v, k).and_then(val_to_string) {
42 return found;
43 }
44 }
45 String::new()
46}
47
48fn field_i64(v: &JsonValue, key: &str) -> i64 {
49 match obj_get(v, key) {
50 Some(JsonValue::Number(n)) => *n as i64,
51 Some(JsonValue::String(s)) => s.parse().unwrap_or(0),
52 _ => 0,
53 }
54}
55
56fn field_bool(v: &JsonValue, key: &str) -> bool {
57 matches!(obj_get(v, key), Some(JsonValue::Bool(true)))
58}
59
60pub fn url_encode(s: &str) -> String {
64 let mut out = String::new();
65 for b in s.bytes() {
66 let c = b as char;
67 if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~') {
68 out.push(c);
69 } else {
70 out.push('%');
71 out.push(hex_digit(b >> 4));
72 out.push(hex_digit(b & 0xF));
73 }
74 }
75 out
76}
77
78fn hex_digit(n: u8) -> char {
79 match n {
80 0..=9 => (b'0' + n) as char,
81 _ => (b'A' + (n - 10)) as char,
82 }
83}
84
85pub fn form_encode(params: &[(&str, &str)]) -> String {
87 let mut parts: Vec<String> = Vec::new();
88 for (k, v) in params {
89 parts.push(format!("{}={}", url_encode(k), url_encode(v)));
90 }
91 parts.join("&")
92}
93
94#[derive(Default, Debug)]
97pub struct TokenResponse {
98 pub access_token: String,
99 pub refresh_token: String,
100 pub token_type: String,
101 pub expires_in: i64,
102 pub scope: String,
103 pub error: String,
105}
106
107pub fn parse_token_response(body: &str) -> TokenResponse {
108 let v = match json::parse(body) {
109 Ok(v) => v,
110 Err(_) => return TokenResponse::default(),
111 };
112 TokenResponse {
113 access_token: field_str(&v, "access_token"),
114 refresh_token: field_str(&v, "refresh_token"),
115 token_type: field_str(&v, "token_type"),
116 expires_in: field_i64(&v, "expires_in"),
117 scope: field_str(&v, "scope"),
118 error: field_str(&v, "error"),
119 }
120}
121
122#[derive(Default, Debug)]
123pub struct DeviceCode {
124 pub device_code: String,
125 pub user_code: String,
126 pub verification_url: String,
127 pub expires_in: i64,
128 pub interval: i64,
129}
130
131pub fn parse_device_code(body: &str) -> DeviceCode {
132 let v = match json::parse(body) {
133 Ok(v) => v,
134 Err(_) => return DeviceCode::default(),
135 };
136 DeviceCode {
137 device_code: field_str(&v, "device_code"),
138 user_code: field_str(&v, "user_code"),
139 verification_url: field_str_any(&v, &["verification_url", "verification_uri"]),
141 expires_in: field_i64(&v, "expires_in"),
142 interval: field_i64(&v, "interval"),
143 }
144}
145
146pub fn google_device_code_body(client_id: &str, scope: &str) -> String {
149 form_encode(&[("client_id", client_id), ("scope", scope)])
150}
151
152pub fn google_token_body(client_id: &str, client_secret: &str, device_code: &str) -> String {
153 form_encode(&[
154 ("client_id", client_id),
155 ("client_secret", client_secret),
156 ("device_code", device_code),
157 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
158 ])
159}
160
161pub fn google_refresh_body(client_id: &str, client_secret: &str, refresh_token: &str) -> String {
162 form_encode(&[
163 ("client_id", client_id),
164 ("client_secret", client_secret),
165 ("refresh_token", refresh_token),
166 ("grant_type", "refresh_token"),
167 ])
168}
169
170pub fn github_device_code_body(client_id: &str, scope: &str) -> String {
171 form_encode(&[("client_id", client_id), ("scope", scope)])
172}
173
174pub fn github_token_body(client_id: &str, device_code: &str) -> String {
175 form_encode(&[
176 ("client_id", client_id),
177 ("device_code", device_code),
178 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
179 ])
180}
181
182pub fn oauth_post(host: &str, path: &str, form: &str) -> Result<String, &'static str> {
186 let res = crate::kernel::tls::https_request(
187 "POST",
188 host,
189 path,
190 &[("Accept", "application/json")],
191 "application/x-www-form-urlencoded",
192 Some(form.as_bytes()),
193 )?;
194 Ok(res.body)
195}
196
197pub fn api_get(host: &str, path: &str, token: &str) -> Result<String, &'static str> {
199 let auth = format!("Bearer {}", token);
200 let res = crate::kernel::tls::https_request(
201 "GET",
202 host,
203 path,
204 &[("Authorization", &auth), ("Accept", "application/json")],
205 "",
206 None,
207 )?;
208 Ok(res.body)
209}
210
211#[derive(Default, Debug, Clone)]
214pub struct DriveFile {
215 pub id: String,
216 pub name: String,
217 pub mime_type: String,
218}
219
220pub fn parse_drive_file_list(body: &str) -> Vec<DriveFile> {
221 let v = match json::parse(body) {
222 Ok(v) => v,
223 Err(_) => return Vec::new(),
224 };
225 let mut out = Vec::new();
226 if let Some(JsonValue::Array(files)) = obj_get(&v, "files") {
227 for f in files {
228 out.push(DriveFile {
229 id: field_str(f, "id"),
230 name: field_str(f, "name"),
231 mime_type: field_str(f, "mimeType"),
232 });
233 }
234 }
235 out
236}
237
238pub fn drive_list_files(token: &str) -> Result<Vec<DriveFile>, &'static str> {
240 let body = api_get(
241 "www.googleapis.com",
242 "/drive/v3/files?pageSize=100&fields=files(id,name,mimeType)",
243 token,
244 )?;
245 Ok(parse_drive_file_list(&body))
246}
247
248pub fn drive_list_files_in_folder(
251 token: &str,
252 folder_id: Option<&str>,
253) -> Result<Vec<DriveFile>, &'static str> {
254 let parent = folder_id.unwrap_or("root");
255 let q = format!("'{}' in parents and trashed = false", parent);
256 let path = format!(
257 "/drive/v3/files?pageSize=100&fields=files(id,name,mimeType)&q={}",
258 url_encode(&q)
259 );
260 let body = api_get("www.googleapis.com", &path, token)?;
261 Ok(parse_drive_file_list(&body))
262}
263
264const GOOGLE_API_HOST: &str = "www.googleapis.com";
267pub const MIME_GOOGLE_DOC: &str = "application/vnd.google-apps.document";
269pub const MIME_FOLDER: &str = "application/vnd.google-apps.folder";
271
272fn api_send(
274 method: &str,
275 host: &str,
276 path: &str,
277 token: &str,
278 content_type: &str,
279 body: Option<&[u8]>,
280) -> Result<crate::kernel::net::HttpsGetResult, &'static str> {
281 let auth = format!("Bearer {}", token);
282 crate::kernel::tls::https_request(
283 method,
284 host,
285 path,
286 &[("Authorization", &auth), ("Accept", "application/json")],
287 content_type,
288 body,
289 )
290}
291
292fn ok_or_status(res: &crate::kernel::net::HttpsGetResult) -> Result<(), &'static str> {
295 match res.status_code {
296 200..=299 => Ok(()),
297 401 => Err("unauthorized (token expired?)"),
298 403 => Err("forbidden (scope/quota)"),
299 404 => Err("not found"),
300 _ => Err("drive API error"),
301 }
302}
303
304pub fn drive_download_text(token: &str, file_id: &str) -> Result<String, &'static str> {
307 let path = format!("/drive/v3/files/{}?alt=media", url_encode(file_id));
308 let res = api_send("GET", GOOGLE_API_HOST, &path, token, "", None)?;
309 ok_or_status(&res)?;
310 Ok(res.body)
311}
312
313pub fn drive_download_binary(token: &str, file_id: &str) -> Result<Vec<u8>, &'static str> {
316 let path = format!("/drive/v3/files/{}?alt=media", url_encode(file_id));
317 let res = api_send("GET", GOOGLE_API_HOST, &path, token, "", None)?;
318 ok_or_status(&res)?;
319 Ok(res.body_bytes)
320}
321
322pub fn drive_export_doc_text(token: &str, file_id: &str) -> Result<String, &'static str> {
324 let path = format!(
325 "/drive/v3/files/{}/export?mimeType=text%2Fplain",
326 url_encode(file_id)
327 );
328 let res = api_send("GET", GOOGLE_API_HOST, &path, token, "", None)?;
329 ok_or_status(&res)?;
330 Ok(res.body)
331}
332
333pub fn drive_get_text(token: &str, file_id: &str, mime_type: &str) -> Result<String, &'static str> {
335 if mime_type == MIME_GOOGLE_DOC {
336 drive_export_doc_text(token, file_id)
337 } else {
338 drive_download_text(token, file_id)
339 }
340}
341
342pub fn drive_get_mime_type(token: &str, file_id: &str) -> Result<String, &'static str> {
345 let path = format!(
346 "/drive/v3/files/{}?fields=mimeType",
347 url_encode(file_id)
348 );
349 let body = api_get(GOOGLE_API_HOST, &path, token)?;
350 let v = json::parse(&body).unwrap_or(JsonValue::Null);
351 Ok(field_str(&v, "mimeType"))
352}
353
354pub fn drive_cat(token: &str, file_id: &str) -> Result<String, &'static str> {
357 let mime_type = drive_get_mime_type(token, file_id)?;
358 drive_get_text(token, file_id, &mime_type)
359}
360
361fn json_escape(s: &str) -> String {
363 let mut out = String::new();
364 for c in s.chars() {
365 match c {
366 '"' => out.push_str("\\\""),
367 '\\' => out.push_str("\\\\"),
368 '\n' => out.push_str("\\n"),
369 '\r' => out.push_str("\\r"),
370 '\t' => out.push_str("\\t"),
371 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
372 c => out.push(c),
373 }
374 }
375 out
376}
377
378pub fn drive_multipart_body(name: &str, mime_type: &str, content: &[u8]) -> (String, Vec<u8>) {
381 let boundary = String::from("atmos_drive_boundary_7f3a9c1e2b");
383 let meta = format!("{{\"name\":\"{}\"}}", json_escape(name));
384
385 let mut body: Vec<u8> = Vec::new();
386 let push = |s: &str, b: &mut Vec<u8>| b.extend_from_slice(s.as_bytes());
387
388 push(&format!("--{}\r\n", boundary), &mut body);
389 push(
390 "Content-Type: application/json; charset=UTF-8\r\n\r\n",
391 &mut body,
392 );
393 push(&meta, &mut body);
394 push("\r\n", &mut body);
395 push(&format!("--{}\r\n", boundary), &mut body);
396 push(&format!("Content-Type: {}\r\n\r\n", mime_type), &mut body);
397 body.extend_from_slice(content);
398 push(&format!("\r\n--{}--\r\n", boundary), &mut body);
399
400 (boundary, body)
401}
402
403pub fn drive_create_text(token: &str, name: &str, content: &str) -> Result<String, &'static str> {
405 let (boundary, body) = drive_multipart_body(name, "text/plain", content.as_bytes());
406 let ctype = format!("multipart/related; boundary={}", boundary);
407 let res = api_send(
408 "POST",
409 GOOGLE_API_HOST,
410 "/upload/drive/v3/files?uploadType=multipart&fields=id",
411 token,
412 &ctype,
413 Some(&body),
414 )?;
415 ok_or_status(&res)?;
416 Ok(field_str(
417 &json::parse(&res.body).unwrap_or(JsonValue::Null),
418 "id",
419 ))
420}
421
422pub fn drive_update_text(token: &str, file_id: &str, content: &str) -> Result<(), &'static str> {
424 let path = format!(
425 "/upload/drive/v3/files/{}?uploadType=media",
426 url_encode(file_id)
427 );
428 let res = api_send(
429 "PATCH",
430 GOOGLE_API_HOST,
431 &path,
432 token,
433 "text/plain",
434 Some(content.as_bytes()),
435 )?;
436 ok_or_status(&res)
437}
438
439pub fn drive_delete(token: &str, file_id: &str) -> Result<(), &'static str> {
441 let path = format!("/drive/v3/files/{}", url_encode(file_id));
442 let res = api_send("DELETE", GOOGLE_API_HOST, &path, token, "", None)?;
443 ok_or_status(&res)
445}
446
447const AUTH_FILE: &str = "gdrive_auth.json";
450const GOOGLE_OAUTH_HOST: &str = "oauth2.googleapis.com";
451pub const DRIVE_SCOPE: &str = "https://www.googleapis.com/auth/drive";
453
454#[derive(Default, Debug, Clone)]
458pub struct GdriveAuth {
459 pub client_id: String,
460 pub client_secret: String,
461 pub access_token: String,
462 pub refresh_token: String,
463}
464
465impl GdriveAuth {
466 pub fn has_client(&self) -> bool {
467 !self.client_id.is_empty() && !self.client_secret.is_empty()
468 }
469 pub fn is_logged_in(&self) -> bool {
470 !self.access_token.is_empty()
471 }
472}
473
474pub fn auth_to_json(a: &GdriveAuth) -> String {
475 format!(
476 "{{\"client_id\":\"{}\",\"client_secret\":\"{}\",\"access_token\":\"{}\",\"refresh_token\":\"{}\"}}",
477 json_escape(&a.client_id), json_escape(&a.client_secret),
478 json_escape(&a.access_token), json_escape(&a.refresh_token)
479 )
480}
481
482pub fn parse_auth(body: &str) -> GdriveAuth {
483 let v = json::parse(body).unwrap_or(JsonValue::Null);
484 GdriveAuth {
485 client_id: field_str(&v, "client_id"),
486 client_secret: field_str(&v, "client_secret"),
487 access_token: field_str(&v, "access_token"),
488 refresh_token: field_str(&v, "refresh_token"),
489 }
490}
491
492pub fn load_auth() -> GdriveAuth {
494 let fs = crate::kernel::fs::get_fs();
495 if let Some((_m, bytes)) = fs.read_file(AUTH_FILE) {
496 if let Ok(s) = core::str::from_utf8(&bytes) {
497 return parse_auth(s);
498 }
499 }
500 GdriveAuth::default()
501}
502
503pub fn save_auth(a: &GdriveAuth) -> Result<(), &'static str> {
505 let json = auth_to_json(a);
506 let fs = crate::kernel::fs::get_fs();
507 fs.save_file(AUTH_FILE, json.as_bytes(), "system,auth")
508}
509
510pub fn drive_request_device_code(client_id: &str) -> Result<DeviceCode, &'static str> {
512 let body = google_device_code_body(client_id, DRIVE_SCOPE);
513 let resp = oauth_post(GOOGLE_OAUTH_HOST, "/device/code", &body)?;
514 Ok(parse_device_code(&resp))
515}
516
517pub fn drive_poll_token(
520 client_id: &str,
521 client_secret: &str,
522 device_code: &str,
523) -> Result<TokenResponse, &'static str> {
524 let body = google_token_body(client_id, client_secret, device_code);
525 let resp = oauth_post(GOOGLE_OAUTH_HOST, "/token", &body)?;
526 Ok(parse_token_response(&resp))
527}
528
529pub fn drive_refresh_token(
531 client_id: &str,
532 client_secret: &str,
533 refresh_token: &str,
534) -> Result<TokenResponse, &'static str> {
535 let body = google_refresh_body(client_id, client_secret, refresh_token);
536 let resp = oauth_post(GOOGLE_OAUTH_HOST, "/token", &body)?;
537 Ok(parse_token_response(&resp))
538}
539
540pub fn ms_device_code_body(client_id: &str, scope: &str) -> String {
546 form_encode(&[("client_id", client_id), ("scope", scope)])
547}
548
549pub fn ms_token_body(client_id: &str, device_code: &str) -> String {
550 form_encode(&[
551 ("client_id", client_id),
552 ("device_code", device_code),
553 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
554 ])
555}
556
557pub fn ms_refresh_body(client_id: &str, refresh_token: &str, scope: &str) -> String {
558 form_encode(&[
559 ("client_id", client_id),
560 ("refresh_token", refresh_token),
561 ("grant_type", "refresh_token"),
562 ("scope", scope),
563 ])
564}
565
566const MS_OAUTH_HOST: &str = "login.microsoftonline.com";
567const GRAPH_API_HOST: &str = "graph.microsoft.com";
568pub const ONEDRIVE_SCOPE: &str = "Files.ReadWrite offline_access";
570
571pub fn onedrive_request_device_code(client_id: &str) -> Result<DeviceCode, &'static str> {
572 let body = ms_device_code_body(client_id, ONEDRIVE_SCOPE);
573 let resp = oauth_post(MS_OAUTH_HOST, "/common/oauth2/v2.0/devicecode", &body)?;
574 Ok(parse_device_code(&resp))
575}
576
577pub fn onedrive_poll_token(client_id: &str, device_code: &str) -> Result<TokenResponse, &'static str> {
580 let body = ms_token_body(client_id, device_code);
581 let resp = oauth_post(MS_OAUTH_HOST, "/common/oauth2/v2.0/token", &body)?;
582 Ok(parse_token_response(&resp))
583}
584
585pub fn onedrive_refresh_token(
586 client_id: &str,
587 refresh_token: &str,
588) -> Result<TokenResponse, &'static str> {
589 let body = ms_refresh_body(client_id, refresh_token, ONEDRIVE_SCOPE);
590 let resp = oauth_post(MS_OAUTH_HOST, "/common/oauth2/v2.0/token", &body)?;
591 Ok(parse_token_response(&resp))
592}
593
594#[derive(Default, Debug, Clone)]
595pub struct OneDriveFile {
596 pub id: String,
597 pub name: String,
598 pub is_folder: bool,
599}
600
601fn parse_onedrive_file_list(body: &str) -> Vec<OneDriveFile> {
602 let v = match json::parse(body) {
603 Ok(v) => v,
604 Err(_) => return Vec::new(),
605 };
606 let mut out = Vec::new();
607 if let Some(JsonValue::Array(items)) = obj_get(&v, "value") {
608 for it in items {
609 out.push(OneDriveFile {
610 id: field_str(it, "id"),
611 name: field_str(it, "name"),
612 is_folder: obj_get(it, "folder").is_some(),
614 });
615 }
616 }
617 out
618}
619
620pub fn onedrive_list_files_in_folder(
622 token: &str,
623 folder_id: Option<&str>,
624) -> Result<Vec<OneDriveFile>, &'static str> {
625 let path = match folder_id {
626 Some(id) => format!("/v1.0/me/drive/items/{}/children", url_encode(id)),
627 None => String::from("/v1.0/me/drive/root/children"),
628 };
629 let body = api_get(GRAPH_API_HOST, &path, token)?;
630 Ok(parse_onedrive_file_list(&body))
631}
632
633fn split_https_url(url: &str) -> Option<(String, String)> {
635 let rest = url.strip_prefix("https://")?;
636 #[allow(clippy::string_slice)] match rest.find('/') {
638 Some(idx) => Some((String::from(&rest[..idx]), String::from(&rest[idx..]))),
639 None => Some((String::from(rest), String::from("/"))),
640 }
641}
642
643pub fn onedrive_download_binary(token: &str, file_id: &str) -> Result<Vec<u8>, &'static str> {
647 let path = format!("/v1.0/me/drive/items/{}/content", url_encode(file_id));
648 let res = api_send("GET", GRAPH_API_HOST, &path, token, "", None)?;
649 if matches!(res.status_code, 301 | 302 | 303 | 307 | 308) {
650 let location = res
651 .headers
652 .get("location")
653 .cloned()
654 .unwrap_or_default();
655 let (host, redir_path) =
656 split_https_url(&location).ok_or("onedrive: invalid redirect location")?;
657 let res2 = crate::kernel::tls::https_request("GET", &host, &redir_path, &[], "", None)?;
658 ok_or_status(&res2)?;
659 return Ok(res2.body_bytes);
660 }
661 ok_or_status(&res)?;
662 Ok(res.body_bytes)
663}
664
665pub fn onedrive_upload_binary(
668 token: &str,
669 folder_id: Option<&str>,
670 name: &str,
671 content: &[u8],
672) -> Result<String, &'static str> {
673 let path = match folder_id {
674 Some(id) => format!(
675 "/v1.0/me/drive/items/{}:/{}:/content",
676 url_encode(id),
677 url_encode(name)
678 ),
679 None => format!("/v1.0/me/drive/root:/{}:/content", url_encode(name)),
680 };
681 let res = api_send(
682 "PUT",
683 GRAPH_API_HOST,
684 &path,
685 token,
686 "application/octet-stream",
687 Some(content),
688 )?;
689 ok_or_status(&res)?;
690 Ok(field_str(
691 &json::parse(&res.body).unwrap_or(JsonValue::Null),
692 "id",
693 ))
694}
695
696pub fn onedrive_delete(token: &str, file_id: &str) -> Result<(), &'static str> {
698 let path = format!("/v1.0/me/drive/items/{}", url_encode(file_id));
699 let res = api_send("DELETE", GRAPH_API_HOST, &path, token, "", None)?;
700 ok_or_status(&res)
701}
702
703const MS_AUTH_FILE: &str = "onedrive_auth.json";
706
707#[derive(Default, Debug, Clone)]
710pub struct MsAuth {
711 pub client_id: String,
712 pub access_token: String,
713 pub refresh_token: String,
714}
715
716impl MsAuth {
717 pub fn has_client(&self) -> bool {
718 !self.client_id.is_empty()
719 }
720 pub fn is_logged_in(&self) -> bool {
721 !self.access_token.is_empty()
722 }
723}
724
725pub fn ms_auth_to_json(a: &MsAuth) -> String {
726 format!(
727 "{{\"client_id\":\"{}\",\"access_token\":\"{}\",\"refresh_token\":\"{}\"}}",
728 json_escape(&a.client_id),
729 json_escape(&a.access_token),
730 json_escape(&a.refresh_token)
731 )
732}
733
734pub fn parse_ms_auth(body: &str) -> MsAuth {
735 let v = json::parse(body).unwrap_or(JsonValue::Null);
736 MsAuth {
737 client_id: field_str(&v, "client_id"),
738 access_token: field_str(&v, "access_token"),
739 refresh_token: field_str(&v, "refresh_token"),
740 }
741}
742
743pub fn load_ms_auth() -> MsAuth {
745 let fs = crate::kernel::fs::get_fs();
746 if let Some((_m, bytes)) = fs.read_file(MS_AUTH_FILE) {
747 if let Ok(s) = core::str::from_utf8(&bytes) {
748 return parse_ms_auth(s);
749 }
750 }
751 MsAuth::default()
752}
753
754pub fn save_ms_auth(a: &MsAuth) -> Result<(), &'static str> {
756 let json = ms_auth_to_json(a);
757 let fs = crate::kernel::fs::get_fs();
758 fs.save_file(MS_AUTH_FILE, json.as_bytes(), "system,auth")
759}
760
761#[derive(Default, Debug)]
764pub struct GitHubUser {
765 pub login: String,
766 pub name: String,
767 pub id: i64,
768}
769
770pub fn parse_github_user(body: &str) -> GitHubUser {
771 let v = match json::parse(body) {
772 Ok(v) => v,
773 Err(_) => return GitHubUser::default(),
774 };
775 GitHubUser {
776 login: field_str(&v, "login"),
777 name: field_str(&v, "name"),
778 id: field_i64(&v, "id"),
779 }
780}
781
782#[derive(Default, Debug, Clone)]
783pub struct Repo {
784 pub name: String,
785 pub full_name: String,
786 pub private: bool,
787}
788
789pub fn parse_github_repos(body: &str) -> Vec<Repo> {
790 let v = match json::parse(body) {
791 Ok(v) => v,
792 Err(_) => return Vec::new(),
793 };
794 let mut out = Vec::new();
795 if let JsonValue::Array(repos) = v {
796 for r in &repos {
797 out.push(Repo {
798 name: field_str(r, "name"),
799 full_name: field_str(r, "full_name"),
800 private: field_bool(r, "private"),
801 });
802 }
803 }
804 out
805}
806
807pub fn github_get_user(token: &str) -> Result<GitHubUser, &'static str> {
809 let body = api_get("api.github.com", "/user", token)?;
810 Ok(parse_github_user(&body))
811}
812
813pub fn github_list_repos(token: &str) -> Result<Vec<Repo>, &'static str> {
814 let body = api_get("api.github.com", "/user/repos?per_page=100", token)?;
815 Ok(parse_github_repos(&body))
816}
817
818const GH_AUTH_FILE: &str = "gh_auth.json";
821const GITHUB_HOST: &str = "github.com";
822pub const GITHUB_DEFAULT_SCOPE: &str = "repo read:user";
824
825#[derive(Default, Debug, Clone)]
829pub struct GithubAuth {
830 pub client_id: String,
831 pub device_code: String,
832 pub access_token: String,
833}
834
835impl GithubAuth {
836 pub fn has_client(&self) -> bool {
837 !self.client_id.is_empty()
838 }
839 pub fn is_logged_in(&self) -> bool {
840 !self.access_token.is_empty()
841 }
842}
843
844pub fn gh_auth_to_json(a: &GithubAuth) -> String {
845 format!(
846 "{{\"client_id\":\"{}\",\"device_code\":\"{}\",\"access_token\":\"{}\"}}",
847 json_escape(&a.client_id),
848 json_escape(&a.device_code),
849 json_escape(&a.access_token)
850 )
851}
852
853pub fn parse_gh_auth(body: &str) -> GithubAuth {
854 let v = json::parse(body).unwrap_or(JsonValue::Null);
855 GithubAuth {
856 client_id: field_str(&v, "client_id"),
857 device_code: field_str(&v, "device_code"),
858 access_token: field_str(&v, "access_token"),
859 }
860}
861
862pub fn load_gh_auth() -> GithubAuth {
864 let fs = crate::kernel::fs::get_fs();
865 if let Some((_m, bytes)) = fs.read_file(GH_AUTH_FILE) {
866 if let Ok(s) = core::str::from_utf8(&bytes) {
867 return parse_gh_auth(s);
868 }
869 }
870 GithubAuth::default()
871}
872
873pub fn save_gh_auth(a: &GithubAuth) -> Result<(), &'static str> {
875 let json = gh_auth_to_json(a);
876 let fs = crate::kernel::fs::get_fs();
877 fs.save_file(GH_AUTH_FILE, json.as_bytes(), "system,auth")
878}
879
880pub fn github_request_device_code(client_id: &str) -> Result<DeviceCode, &'static str> {
882 let body = github_device_code_body(client_id, GITHUB_DEFAULT_SCOPE);
883 let resp = oauth_post(GITHUB_HOST, "/login/device/code", &body)?;
884 Ok(parse_device_code(&resp))
885}
886
887pub fn github_poll_token(
890 client_id: &str,
891 device_code: &str,
892) -> Result<TokenResponse, &'static str> {
893 let body = github_token_body(client_id, device_code);
894 let resp = oauth_post(GITHUB_HOST, "/login/oauth/access_token", &body)?;
895 Ok(parse_token_response(&resp))
896}
897
898pub fn selftest() -> (usize, usize) {
901 let mut passed = 0usize;
902 let mut total = 0usize;
903 let mut check = |cond: bool, label: &str| {
904 total += 1;
905 if cond {
906 passed += 1;
907 } else {
908 crate::println!("CLOUD_SELFTEST FAIL: {}", label);
909 }
910 };
911
912 check(url_encode("a b&c=d") == "a%20b%26c%3Dd", "url_encode");
914 check(
915 url_encode("safe-_.~") == "safe-_.~",
916 "url_encode unreserved",
917 );
918 check(
919 form_encode(&[("grant_type", "refresh_token"), ("code", "x y")])
920 == "grant_type=refresh_token&code=x%20y",
921 "form_encode",
922 );
923
924 let tok = parse_token_response(
926 r#"{"access_token":"ya29.AbC","expires_in":3599,"refresh_token":"1//rt","scope":"drive","token_type":"Bearer"}"#,
927 );
928 check(tok.access_token == "ya29.AbC", "token access_token");
929 check(tok.expires_in == 3599, "token expires_in");
930 check(tok.refresh_token == "1//rt", "token refresh_token");
931 let pend = parse_token_response(r#"{"error":"authorization_pending"}"#);
932 check(
933 pend.error == "authorization_pending" && pend.access_token.is_empty(),
934 "token pending",
935 );
936
937 let g = parse_device_code(
939 r#"{"device_code":"DC","user_code":"WDJB-MJHT","verification_url":"https://www.google.com/device","expires_in":1800,"interval":5}"#,
940 );
941 check(
942 g.user_code == "WDJB-MJHT"
943 && g.verification_url == "https://www.google.com/device"
944 && g.interval == 5,
945 "google device code",
946 );
947 let gh = parse_device_code(
948 r#"{"device_code":"DC2","user_code":"ABCD-1234","verification_uri":"https://github.com/login/device","expires_in":900,"interval":5}"#,
949 );
950 check(
951 gh.user_code == "ABCD-1234" && gh.verification_url == "https://github.com/login/device",
952 "github device code",
953 );
954
955 check(
957 google_token_body("cid", "sec", "dc")
958 .contains("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"),
959 "google token body",
960 );
961 check(
962 google_refresh_body("cid", "sec", "rt").contains("grant_type=refresh_token"),
963 "google refresh body",
964 );
965
966 let files = parse_drive_file_list(
968 r#"{"files":[{"id":"1","name":"memo.txt","mimeType":"text/plain"},{"id":"2","name":"work","mimeType":"application/vnd.google-apps.folder"}]}"#,
969 );
970 check(files.len() == 2, "drive file count");
971 check(
972 files.first().map(|f| f.name == "memo.txt").unwrap_or(false),
973 "drive file name",
974 );
975 check(
976 files
977 .get(1)
978 .map(|f| f.mime_type.contains("folder"))
979 .unwrap_or(false),
980 "drive folder mime",
981 );
982
983 check(json_escape(r#"a"b\c"#) == r#"a\"b\\c"#, "json_escape");
985 let (boundary, body) = drive_multipart_body("memo.txt", "text/plain", b"hello");
986 let body_str = alloc::string::String::from_utf8_lossy(&body);
987 check(
988 body_str.contains("--atmos_drive_boundary_7f3a9c1e2b"),
989 "multipart boundary",
990 );
991 check(
992 body_str.contains(r#"{"name":"memo.txt"}"#),
993 "multipart metadata name",
994 );
995 check(
996 body_str.contains("Content-Type: text/plain"),
997 "multipart media type",
998 );
999 check(
1000 body_str.contains("hello") && body_str.ends_with("--atmos_drive_boundary_7f3a9c1e2b--\r\n"),
1001 "multipart closing",
1002 );
1003 check(
1004 boundary == "atmos_drive_boundary_7f3a9c1e2b",
1005 "multipart boundary value",
1006 );
1007
1008 let a = GdriveAuth {
1010 client_id: String::from("cid.apps.googleusercontent.com"),
1011 client_secret: String::from("sec"),
1012 access_token: String::from("ya29.tok"),
1013 refresh_token: String::from("1//rt"),
1014 };
1015 let round = parse_auth(&auth_to_json(&a));
1016 check(
1017 round.client_id == a.client_id
1018 && round.access_token == "ya29.tok"
1019 && round.refresh_token == "1//rt",
1020 "auth round-trip",
1021 );
1022 check(a.has_client() && a.is_logged_in(), "auth flags");
1023 check(
1024 !GdriveAuth::default().is_logged_in(),
1025 "auth default logged-out",
1026 );
1027
1028 let ms_dc = parse_device_code(
1033 r#"{"device_code":"MSDC","user_code":"ABC-DEF-123","verification_uri":"https://microsoft.com/devicelogin","expires_in":900,"interval":5,"message":"..."}"#,
1034 );
1035 check(
1036 ms_dc.user_code == "ABC-DEF-123"
1037 && ms_dc.verification_url == "https://microsoft.com/devicelogin",
1038 "ms device code",
1039 );
1040
1041 check(
1043 ms_token_body("cid", "dc")
1044 .contains("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code"),
1045 "ms token body",
1046 );
1047 check(
1048 !ms_token_body("cid", "dc").contains("client_secret"),
1049 "ms token body no secret",
1050 );
1051 check(
1052 ms_refresh_body("cid", "rt", ONEDRIVE_SCOPE).contains("grant_type=refresh_token"),
1053 "ms refresh body",
1054 );
1055
1056 let od_files = parse_onedrive_file_list(
1058 r#"{"value":[{"id":"1","name":"memo.txt"},{"id":"2","name":"work","folder":{"childCount":3}}]}"#,
1059 );
1060 check(od_files.len() == 2, "onedrive file count");
1061 check(
1062 od_files.first().map(|f| !f.is_folder).unwrap_or(false),
1063 "onedrive file is not folder",
1064 );
1065 check(
1066 od_files.get(1).map(|f| f.is_folder).unwrap_or(false),
1067 "onedrive folder detection",
1068 );
1069
1070 check(
1072 split_https_url("https://cdn.example.com/blob/abc?sig=xyz")
1073 == Some((
1074 String::from("cdn.example.com"),
1075 String::from("/blob/abc?sig=xyz"),
1076 )),
1077 "split_https_url",
1078 );
1079 check(split_https_url("http://insecure.example.com/x").is_none(), "split_https_url rejects non-https");
1080
1081 let ms_a = MsAuth {
1083 client_id: String::from("11111111-2222-3333-4444-555555555555"),
1084 access_token: String::from("eyJ0eXAi.tok"),
1085 refresh_token: String::from("M.R3_BAY.rt"),
1086 };
1087 let ms_round = parse_ms_auth(&ms_auth_to_json(&ms_a));
1088 check(
1089 ms_round.client_id == ms_a.client_id
1090 && ms_round.access_token == "eyJ0eXAi.tok"
1091 && ms_round.refresh_token == "M.R3_BAY.rt",
1092 "ms auth round-trip",
1093 );
1094 check(ms_a.has_client() && ms_a.is_logged_in(), "ms auth flags");
1095 check(!MsAuth::default().is_logged_in(), "ms auth default logged-out");
1096
1097 let user = parse_github_user(r#"{"login":"octocat","name":"The Octocat","id":583231}"#);
1099 check(user.login == "octocat" && user.id == 583231, "github user");
1100 let repos = parse_github_repos(
1101 r#"[{"name":"r1","full_name":"u/r1","private":false},{"name":"r2","full_name":"u/r2","private":true}]"#,
1102 );
1103 check(
1104 repos.len() == 2 && repos[1].private && repos[0].full_name == "u/r1",
1105 "github repos",
1106 );
1107
1108 let gh = GithubAuth {
1110 client_id: String::from("Iv1.abc123"),
1111 device_code: String::from("dc-xyz"),
1112 access_token: String::from("gho_tok"),
1113 };
1114 let gh_round = parse_gh_auth(&gh_auth_to_json(&gh));
1115 check(
1116 gh_round.client_id == gh.client_id
1117 && gh_round.device_code == "dc-xyz"
1118 && gh_round.access_token == "gho_tok",
1119 "gh auth round-trip",
1120 );
1121 check(gh.has_client() && gh.is_logged_in(), "gh auth flags");
1122 check(
1123 !GithubAuth::default().is_logged_in(),
1124 "gh auth default logged-out",
1125 );
1126
1127 (passed, total)
1128}