1use bitwarden_auth::send_access::{
25 SendAccessCredentials, SendAccessTokenError, SendAccessTokenRequest, SendAccessTokenResponse,
26 SendEmailCredentials, SendEmailOtpCredentials, SendPasswordCredentials,
27 api::{
28 SendAccessTokenApiErrorResponse, SendAccessTokenInvalidGrantError,
29 SendAccessTokenInvalidRequestError,
30 },
31};
32use bitwarden_core::get_host_platform_info;
33use bitwarden_pm::PasswordManagerClient;
34use bitwarden_send::{SendAccessKey, SendAccessView, SendType};
35use color_eyre::eyre::{Context as _, Result, bail, eyre};
36use inquire::{Password, Text, validator::Validation};
37use url::Url;
38
39use super::send_access_token_cache;
40use crate::{
41 platform::{ConfigFile, read_config_json},
42 render::{CommandOutput, CommandResult},
43 tools::file_output::{default_send_file_name, reject_path_traversal, save_file},
44};
45
46const CLOUD_HOSTS: &[CloudRegion] = &[
53 CloudRegion {
54 hosts: &["send.bitwarden.com", "vault.bitwarden.com"],
55 api_url: "https://api.bitwarden.com",
56 identity_url: "https://identity.bitwarden.com",
57 },
58 CloudRegion {
59 hosts: &["send.bitwarden.eu", "vault.bitwarden.eu"],
60 api_url: "https://api.bitwarden.eu",
61 identity_url: "https://identity.bitwarden.eu",
62 },
63];
64
65struct CloudRegion {
66 hosts: &'static [&'static str],
67 api_url: &'static str,
68 identity_url: &'static str,
69}
70
71pub(crate) struct ReceiveInputs {
74 pub url: String,
76 pub password: Option<String>,
78 pub passwordenv: Option<String>,
80 pub passwordfile: Option<String>,
82 pub output_path: Option<String>,
88 pub full_object: bool,
90}
91
92pub(crate) async fn run_receive(inputs: ReceiveInputs) -> CommandResult {
96 let url = Url::parse(&inputs.url).wrap_err("Failed to parse the provided Send url")?;
97 let (send_id, key_b64) = parse_send_url(&url)?;
98
99 let access_key = SendAccessKey::from_url_b64(&key_b64)
103 .map_err(|_| eyre!("Failed to parse url, the url provided is not a valid Send url"))?;
104
105 let (api_url, identity_url, trusted) =
106 resolve_urls(&url, read_config_json().ok().flatten().as_ref());
107 let cache_host = format!("{api_url}|{identity_url}");
112 let client = PasswordManagerClient::new(Some(
113 get_host_platform_info().to_client_settings(api_url, identity_url),
114 ));
115
116 if trusted && let Some(token) = send_access_token_cache::get(&cache_host, &send_id) {
117 return match render_access(&client, &access_key, token, &inputs).await {
118 Ok(output) => Ok(output),
119 Err(err) => {
120 send_access_token_cache::evict(&cache_host, &send_id);
124 Err(err)
125 }
126 };
127 }
128
129 let token_response = attempt_access(&client, &send_id, &access_key, &inputs).await?;
130 if trusted {
131 send_access_token_cache::set(
132 &cache_host,
133 &send_id,
134 &token_response.token,
135 token_response.expires_at,
136 );
137 }
138 render_access(&client, &access_key, token_response.token, &inputs).await
139}
140
141fn parse_send_url(url: &Url) -> Result<(String, String)> {
148 let fragment = url.fragment().unwrap_or_default();
149 let mut trailing = fragment.rsplit('/');
150 let key = trailing.next().unwrap_or_default();
151 let id = trailing.next().unwrap_or_default();
152
153 if id.trim().is_empty() || key.trim().is_empty() {
154 return Err(eyre!(
155 "Failed to parse url, the url provided is not a valid Send url"
156 ));
157 }
158
159 Ok((id.to_string(), key.to_string()))
160}
161
162fn resolve_urls(url: &Url, config: Option<&ConfigFile>) -> (String, String, bool) {
187 if let Some(host) = url.host_str()
188 && let Some(region) = CLOUD_HOSTS
189 .iter()
190 .find(|region| region.hosts.contains(&host))
191 {
192 return (
193 region.api_url.to_string(),
194 region.identity_url.to_string(),
195 true,
196 );
197 }
198
199 let origin = url.origin().ascii_serialization();
200
201 if let Some(config) = config
202 && let Some(base) = [config.web_vault.as_deref(), config.server.as_deref()]
203 .into_iter()
204 .flatten()
205 .find(|configured| same_origin(configured, &origin))
206 {
207 let base = base.trim_end_matches('/');
208 let api = trimmed(config.api.as_deref()).unwrap_or_else(|| format!("{base}/api"));
209 let identity =
210 trimmed(config.identity.as_deref()).unwrap_or_else(|| format!("{base}/identity"));
211 return (api, identity, true);
212 }
213
214 (format!("{origin}/api"), format!("{origin}/identity"), false)
215}
216
217fn same_origin(configured: &str, origin: &str) -> bool {
221 match Url::parse(configured) {
222 Ok(parsed) => parsed.origin().ascii_serialization() == origin,
223 Err(_) => configured.trim_end_matches('/') == origin,
224 }
225}
226
227fn trimmed(value: Option<&str>) -> Option<String> {
228 value
229 .map(|v| v.trim_end_matches('/'))
230 .filter(|v| !v.is_empty())
231 .map(str::to_string)
232}
233
234async fn attempt_access(
245 client: &PasswordManagerClient,
246 send_id: &str,
247 access_key: &SendAccessKey,
248 inputs: &ReceiveInputs,
249) -> Result<SendAccessTokenResponse> {
250 match request_token(client, send_id, None).await {
251 Ok(token) => Ok(token),
252 Err(err) => match invalid_request_type(&err) {
253 Some(SendAccessTokenInvalidRequestError::PasswordHashB64Required) => {
254 access_with_password(client, send_id, access_key, inputs).await
255 }
256 Some(SendAccessTokenInvalidRequestError::EmailRequired) => {
257 access_with_email_otp(client, send_id).await
258 }
259 _ if invalid_grant_type(&err)
260 == Some(&SendAccessTokenInvalidGrantError::SendIdInvalid) =>
261 {
262 Err(eyre!("Not found."))
263 }
264 _ => Err(token_error(err)),
265 },
266 }
267}
268
269async fn access_with_password(
272 client: &PasswordManagerClient,
273 send_id: &str,
274 access_key: &SendAccessKey,
275 inputs: &ReceiveInputs,
276) -> Result<SendAccessTokenResponse> {
277 let password = resolve_password(inputs)?;
278 let credentials = SendAccessCredentials::Password(SendPasswordCredentials {
279 password_hash_b64: access_key.hash_password_b64(&password),
280 });
281
282 match request_token(client, send_id, Some(credentials)).await {
283 Ok(token) => Ok(token),
284 Err(err)
285 if invalid_grant_type(&err)
286 == Some(&SendAccessTokenInvalidGrantError::PasswordHashB64Invalid) =>
287 {
288 Err(eyre!("Invalid password"))
289 }
290 Err(err) => Err(token_error(err)),
291 }
292}
293
294async fn access_with_email_otp(
297 client: &PasswordManagerClient,
298 send_id: &str,
299) -> Result<SendAccessTokenResponse> {
300 if !can_interact() {
301 return Err(eyre!(
302 "Email verification required. Run in interactive mode."
303 ));
304 }
305
306 let email = prompt_email()?;
307 let credentials = SendAccessCredentials::Email(SendEmailCredentials {
308 email: email.clone(),
309 });
310
311 let err = match request_token(client, send_id, Some(credentials)).await {
312 Ok(_) => return Err(eyre!("Unexpected server response")),
315 Err(err) => err,
316 };
317
318 if invalid_request_type(&err) != Some(&SendAccessTokenInvalidRequestError::EmailAndOtpRequired)
319 {
320 return Err(token_error(err));
321 }
322
323 let otp = prompt_otp()?;
324 let credentials = SendAccessCredentials::EmailOtp(SendEmailOtpCredentials { email, otp });
325
326 match request_token(client, send_id, Some(credentials)).await {
327 Ok(token) => Ok(token),
328 Err(err)
330 if invalid_request_type(&err)
331 == Some(&SendAccessTokenInvalidRequestError::EmailAndOtpRequired) =>
332 {
333 Err(eyre!("Invalid email or verification code"))
334 }
335 Err(err) => Err(token_error(err)),
336 }
337}
338
339async fn request_token(
340 client: &PasswordManagerClient,
341 send_id: &str,
342 credentials: Option<SendAccessCredentials>,
343) -> Result<SendAccessTokenResponse, SendAccessTokenError> {
344 client
345 .auth()
346 .send_access()
347 .request_send_access_token(SendAccessTokenRequest {
348 send_id: send_id.to_string(),
349 send_access_credentials: credentials,
350 })
351 .await
352}
353
354fn invalid_request_type(err: &SendAccessTokenError) -> Option<&SendAccessTokenInvalidRequestError> {
360 match err {
361 SendAccessTokenError::Expected(SendAccessTokenApiErrorResponse::InvalidRequest {
362 send_access_error_type,
363 ..
364 }) => send_access_error_type.as_ref(),
365 _ => None,
366 }
367}
368
369fn invalid_grant_type(err: &SendAccessTokenError) -> Option<&SendAccessTokenInvalidGrantError> {
372 match err {
373 SendAccessTokenError::Expected(SendAccessTokenApiErrorResponse::InvalidGrant {
374 send_access_error_type,
375 ..
376 }) => send_access_error_type.as_ref(),
377 _ => None,
378 }
379}
380
381fn token_error(err: SendAccessTokenError) -> color_eyre::eyre::Error {
385 match err {
386 SendAccessTokenError::Unexpected(inner) => eyre!("Server error: {inner:?}"),
387 SendAccessTokenError::Expected(inner) => eyre!("Error: {inner:?}"),
388 }
389}
390
391async fn render_access(
393 client: &PasswordManagerClient,
394 access_key: &SendAccessKey,
395 token: String,
396 inputs: &ReceiveInputs,
397) -> CommandResult {
398 let response = client.sends().access_send(token.clone()).await?;
399 let view = access_key.decrypt_response(response)?;
400
401 if inputs.full_object {
402 return Ok(CommandOutput::Object(Box::new(view)));
403 }
404
405 match view.type_ {
406 Some(SendType::Text) => Ok(CommandOutput::Plain(
410 view.text.and_then(|text| text.text).unwrap_or_default(),
411 )),
412 Some(SendType::File) => {
413 save_file_send(
414 client,
415 access_key,
416 &token,
417 view,
418 inputs.output_path.as_deref(),
419 )
420 .await
421 }
422 Some(SendType::Item) => {
423 bail!("Accessing item Sends is not supported by the CLI.");
424 }
425 None => Ok(CommandOutput::Object(Box::new(view))),
428 }
429}
430
431async fn save_file_send(
433 client: &PasswordManagerClient,
434 access_key: &SendAccessKey,
435 token: &str,
436 view: SendAccessView,
437 output: Option<&str>,
438) -> CommandResult {
439 let file = view
440 .file
441 .ok_or_else(|| eyre!("The Send is a file Send but carries no file metadata."))?;
442 let file_id = file
443 .id
444 .ok_or_else(|| eyre!("The Send's file is missing an id; cannot download it."))?;
445
446 let download = client
447 .sends()
448 .get_file_download_data(token.to_string(), file_id)
449 .await?;
450 let download_url = download
451 .url
452 .ok_or_else(|| eyre!("The server did not return a download url for the Send's file."))?;
453
454 let encrypted = download_bytes(client, &download_url).await?;
455 let decrypted = access_key.decrypt_file_buffer(&encrypted)?;
456
457 let file_name = default_send_file_name(file.file_name.as_deref());
458 let path = save_file(output, &file_name, &decrypted)?;
459
460 Ok(CommandOutput::Plain(format!("Saved {}", path.display())))
461}
462
463async fn download_bytes(client: &PasswordManagerClient, url: &str) -> Result<Vec<u8>> {
470 let response = client
471 .0
472 .internal
473 .get_http_client()
474 .get(url)
475 .header("cache-control", "no-cache")
477 .send()
478 .await?;
479
480 if !response.status().is_success() {
481 return Err(eyre!(
482 "A {} error occurred while downloading the attachment.",
483 response.status().as_u16()
484 ));
485 }
486
487 Ok(response.bytes().await?.to_vec())
488}
489
490fn resolve_password(inputs: &ReceiveInputs) -> Result<String> {
495 let supplied = password_from_args(
496 inputs.password.as_deref(),
497 inputs.passwordfile.as_deref(),
498 inputs.passwordenv.as_deref(),
499 )?;
500
501 let password = match supplied {
502 Some(password) => password,
503 None if can_interact() => prompt_password()?,
504 None => return Err(eyre!("Password required")),
505 };
506
507 if password.is_empty() {
508 return Err(eyre!("Password required"));
509 }
510
511 Ok(password)
512}
513
514fn password_from_args(
521 password: Option<&str>,
522 passwordfile: Option<&str>,
523 passwordenv: Option<&str>,
524) -> Result<Option<String>> {
525 if let Some(password) = password.filter(|p| !p.is_empty()) {
526 return Ok(Some(password.to_string()));
527 }
528
529 if let Some(path) = passwordfile {
530 reject_path_traversal("--passwordfile", path)?;
531 let contents = std::fs::read_to_string(path)
532 .wrap_err_with(|| format!("Could not read --passwordfile {path}"))?;
533 return Ok(contents.lines().next().map(str::to_string));
537 }
538
539 if let Some(name) = passwordenv {
540 return match std::env::var(name) {
541 Ok(value) => Ok(Some(value)),
542 Err(_) => {
545 tracing::warn!("--passwordenv variable `{name}` is not set; ignoring it.");
546 Ok(None)
547 }
548 };
549 }
550
551 Ok(None)
552}
553
554fn can_interact() -> bool {
563 std::env::var("BW_NOINTERACTION").as_deref() != Ok("true")
564}
565
566fn prompt_password() -> Result<String> {
570 Ok(Password::new("Send password")
571 .without_confirmation()
572 .prompt()?)
573}
574
575fn prompt_email() -> Result<String> {
576 Ok(Text::new("Enter your email address:")
577 .with_validator(|input: &str| {
578 if input.contains('@') {
579 Ok(Validation::Valid)
580 } else {
581 Ok(Validation::Invalid(
582 "Please enter a valid email address".into(),
583 ))
584 }
585 })
586 .prompt()?)
587}
588
589fn prompt_otp() -> Result<String> {
590 Ok(Text::new("Enter the verification code sent to your email:").prompt()?)
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 fn parse(url: &str) -> Url {
598 Url::parse(url).expect("test url parses")
599 }
600
601 #[test]
604 fn parses_the_web_vault_send_route() {
605 let (id, key) = parse_send_url(&parse(
606 "https://vault.bitwarden.com/#/send/access-id/Pgui0FK85cNhBGWHAlBHBw",
607 ))
608 .unwrap();
609 assert_eq!(id, "access-id");
610 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
611 }
612
613 #[test]
614 fn parses_the_send_vanity_host_route() {
615 let (id, key) = parse_send_url(&parse(
617 "https://send.bitwarden.com/#access-id/Pgui0FK85cNhBGWHAlBHBw",
618 ))
619 .unwrap();
620 assert_eq!(id, "access-id");
621 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
622 }
623
624 #[test]
625 fn parses_self_hosted_links_with_a_path_prefix() {
626 let (id, key) = parse_send_url(&parse(
627 "https://example.com/bitwarden/#/send/access-id/Pgui0FK85cNhBGWHAlBHBw",
628 ))
629 .unwrap();
630 assert_eq!(id, "access-id");
631 assert_eq!(key, "Pgui0FK85cNhBGWHAlBHBw");
632 }
633
634 #[test]
635 fn rejects_a_url_without_a_fragment() {
636 let err = parse_send_url(&parse("https://vault.bitwarden.com/")).unwrap_err();
637 assert!(
638 err.to_string().contains("not a valid Send url"),
639 "got: {err}"
640 );
641 }
642
643 #[test]
644 fn rejects_a_fragment_with_only_one_segment() {
645 let err = parse_send_url(&parse("https://send.bitwarden.com/#access-id")).unwrap_err();
646 assert!(
647 err.to_string().contains("not a valid Send url"),
648 "got: {err}"
649 );
650 }
651
652 #[test]
653 fn rejects_a_fragment_with_a_blank_segment() {
654 assert!(
656 parse_send_url(&parse("https://vault.bitwarden.com/#/send//key")).is_err(),
657 "blank id must be rejected"
658 );
659 assert!(
660 parse_send_url(&parse("https://vault.bitwarden.com/#/send/id/")).is_err(),
661 "blank key must be rejected"
662 );
663 }
664
665 #[test]
668 fn cloud_send_and_vault_hosts_map_to_their_region() {
669 for host in ["send.bitwarden.com", "vault.bitwarden.com"] {
670 let (api, identity, trusted) =
671 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
672 assert_eq!(api, "https://api.bitwarden.com");
673 assert_eq!(identity, "https://identity.bitwarden.com");
674 assert!(trusted, "a known cloud host must be trusted");
675 }
676 for host in ["send.bitwarden.eu", "vault.bitwarden.eu"] {
677 let (api, identity, trusted) =
678 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
679 assert_eq!(api, "https://api.bitwarden.eu");
680 assert_eq!(identity, "https://identity.bitwarden.eu");
681 assert!(trusted, "a known cloud host must be trusted");
682 }
683 }
684
685 #[test]
689 fn a_lookalike_host_is_never_treated_as_cloud() {
690 for host in [
691 "send.bitwarden.com.evil.tld",
692 "evil-send.bitwarden.com.attacker.example",
693 "notsend.bitwarden.com.evil.tld",
694 ] {
695 let (api, identity, trusted) =
696 resolve_urls(&parse(&format!("https://{host}/#id/key")), None);
697 assert_eq!(api, format!("https://{host}/api"));
698 assert_eq!(identity, format!("https://{host}/identity"));
699 assert!(!trusted, "a lookalike host must not be trusted");
700 }
701 }
702
703 #[test]
704 fn unknown_host_falls_back_to_the_single_domain_convention() {
705 let (api, identity, trusted) =
706 resolve_urls(&parse("https://vault.example.com/#/send/id/key"), None);
707 assert_eq!(api, "https://vault.example.com/api");
708 assert_eq!(identity, "https://vault.example.com/identity");
709 assert!(
710 !trusted,
711 "an unknown, unconfigured host must not be trusted"
712 );
713 }
714
715 #[test]
716 fn a_non_default_port_is_preserved_in_the_fallback() {
717 let (api, identity, trusted) =
718 resolve_urls(&parse("https://localhost:8080/#/send/id/key"), None);
719 assert_eq!(api, "https://localhost:8080/api");
720 assert_eq!(identity, "https://localhost:8080/identity");
721 assert!(!trusted);
722 }
723
724 #[test]
725 fn a_matching_configured_server_supplies_the_base() {
726 let config = ConfigFile {
727 server: Some("https://bw.example.com".to_string()),
728 ..Default::default()
729 };
730 let (api, identity, trusted) = resolve_urls(
731 &parse("https://bw.example.com/#/send/id/key"),
732 Some(&config),
733 );
734 assert_eq!(api, "https://bw.example.com/api");
735 assert_eq!(identity, "https://bw.example.com/identity");
736 assert!(trusted, "the locally configured deployment must be trusted");
737 }
738
739 #[test]
740 fn a_matching_configured_web_vault_supplies_the_base() {
741 let config = ConfigFile {
742 web_vault: Some("https://vault.example.com/".to_string()),
743 ..Default::default()
744 };
745 let (api, identity, trusted) = resolve_urls(
746 &parse("https://vault.example.com/#/send/id/key"),
747 Some(&config),
748 );
749 assert_eq!(api, "https://vault.example.com/api");
750 assert_eq!(identity, "https://vault.example.com/identity");
751 assert!(trusted);
752 }
753
754 #[test]
757 fn explicit_configured_api_and_identity_win_over_the_base() {
758 let config = ConfigFile {
759 web_vault: Some("https://vault.example.com".to_string()),
760 api: Some("https://api.example.com".to_string()),
761 identity: Some("https://identity.example.com/".to_string()),
762 ..Default::default()
763 };
764 let (api, identity, trusted) = resolve_urls(
765 &parse("https://vault.example.com/#/send/id/key"),
766 Some(&config),
767 );
768 assert_eq!(api, "https://api.example.com");
769 assert_eq!(identity, "https://identity.example.com");
770 assert!(trusted);
771 }
772
773 #[test]
776 fn a_config_for_a_different_origin_is_ignored() {
777 let config = ConfigFile {
778 server: Some("https://bw.example.com".to_string()),
779 api: Some("https://api.example.com".to_string()),
780 ..Default::default()
781 };
782 let (api, identity, trusted) =
783 resolve_urls(&parse("https://send.bitwarden.com/#id/key"), Some(&config));
784 assert_eq!(api, "https://api.bitwarden.com");
785 assert_eq!(identity, "https://identity.bitwarden.com");
786 assert!(trusted);
787
788 let (api, identity, trusted) = resolve_urls(
789 &parse("https://other.example.com/#/send/id/key"),
790 Some(&config),
791 );
792 assert_eq!(api, "https://other.example.com/api");
793 assert_eq!(identity, "https://other.example.com/identity");
794 assert!(
795 !trusted,
796 "an origin that matches neither the cloud table nor the configured deployment \
797 must not be trusted"
798 );
799 }
800
801 #[test]
804 fn password_flag_wins() {
805 let password = password_from_args(Some("hunter2"), None, None).unwrap();
806 assert_eq!(password.as_deref(), Some("hunter2"));
807 }
808
809 #[test]
810 fn empty_password_flag_falls_through() {
811 assert_eq!(password_from_args(Some(""), None, None).unwrap(), None);
813 }
814
815 #[test]
816 fn password_file_supplies_its_first_line_only() {
817 let path = std::env::temp_dir().join(format!(
818 "bw-receive-passwordfile-{}",
819 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
820 ));
821 std::fs::write(&path, "first-line\nsecond-line\n").unwrap();
822
823 let password = password_from_args(None, Some(&path.to_string_lossy()), None).unwrap();
824
825 assert_eq!(password.as_deref(), Some("first-line"));
826 std::fs::remove_file(&path).ok();
827 }
828
829 #[test]
830 fn password_file_strips_a_windows_line_terminator() {
831 let path = std::env::temp_dir().join(format!(
832 "bw-receive-passwordfile-crlf-{}",
833 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
834 ));
835 std::fs::write(&path, "first-line\r\nsecond\r\n").unwrap();
836
837 let password = password_from_args(None, Some(&path.to_string_lossy()), None).unwrap();
838
839 assert_eq!(password.as_deref(), Some("first-line"));
840 std::fs::remove_file(&path).ok();
841 }
842
843 #[test]
844 fn password_file_rejects_traversal() {
845 let err = password_from_args(None, Some("../secrets/pw.txt"), None).unwrap_err();
846 assert!(
847 err.to_string().contains("--passwordfile"),
848 "expected a --passwordfile traversal error, got: {err}"
849 );
850 }
851
852 #[test]
853 fn missing_password_file_is_an_error_not_a_fallthrough() {
854 assert!(password_from_args(None, Some("/nonexistent/bw-receive-pw.txt"), None).is_err());
857 }
858
859 #[test]
862 fn password_file_short_circuits_password_env() {
863 let path = std::env::temp_dir().join(format!(
864 "bw-receive-passwordfile-empty-{}",
865 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
866 ));
867 std::fs::write(&path, "").unwrap();
868
869 let password = password_from_args(
870 None,
871 Some(&path.to_string_lossy()),
872 Some("BW_RECEIVE_TEST_UNSET_VAR"),
876 )
877 .unwrap();
878
879 assert_eq!(password, None);
880 std::fs::remove_file(&path).ok();
881 }
882
883 #[test]
884 fn unset_password_env_yields_no_password() {
885 assert_eq!(
886 password_from_args(None, None, Some("BW_RECEIVE_TEST_DEFINITELY_UNSET")).unwrap(),
887 None
888 );
889 }
890
891 #[test]
892 fn no_inputs_yields_no_password() {
893 assert_eq!(password_from_args(None, None, None).unwrap(), None);
894 }
895}