1use std::borrow::Cow;
2use std::fmt;
3use std::io::Read;
4use std::io::Write;
5use std::str::FromStr;
6
7use base64::prelude::BASE64_STANDARD;
8use base64::read::DecoderReader;
9use base64::write::EncoderWriter;
10use http::Uri;
11use reqsign::aws::DefaultSigner as AwsDefaultSigner;
12use reqsign::azure::DefaultSigner as AzureDefaultSigner;
13use reqsign::google::DefaultSigner as GcsDefaultSigner;
14use reqwest::Request;
15use reqwest::header::{HeaderName, HeaderValue};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use url::Url;
19
20use uv_netrc::Netrc;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24const AZURE_STORAGE_VERSION: &str = "2023-11-03";
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Credentials {
28 Basic {
30 username: Username,
32 password: Option<Password>,
34 },
35 Bearer {
37 token: Token,
39 },
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct Username(Option<String>);
45
46impl Username {
47 pub(crate) fn new(value: Option<String>) -> Self {
51 Self(value.filter(|s| !s.is_empty()))
53 }
54
55 pub(crate) fn none() -> Self {
56 Self::new(None)
57 }
58
59 pub(crate) fn is_none(&self) -> bool {
60 self.0.is_none()
61 }
62
63 pub(crate) fn is_some(&self) -> bool {
64 self.0.is_some()
65 }
66
67 pub(crate) fn as_deref(&self) -> Option<&str> {
68 self.0.as_deref()
69 }
70}
71
72impl From<String> for Username {
73 fn from(value: String) -> Self {
74 Self::new(Some(value))
75 }
76}
77
78impl From<Option<String>> for Username {
79 fn from(value: Option<String>) -> Self {
80 Self::new(value)
81 }
82}
83
84#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct Password(String);
87
88impl Password {
89 pub fn new(password: String) -> Self {
90 Self(password)
91 }
92
93 pub fn as_str(&self) -> &str {
95 self.0.as_str()
96 }
97
98 pub fn into_string(self) -> String {
100 self.0
101 }
102}
103
104impl fmt::Debug for Password {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "****")
107 }
108}
109
110#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
111#[serde(transparent)]
112pub struct Token(Vec<u8>);
113
114impl Token {
115 pub fn new(token: Vec<u8>) -> Self {
116 Self(token)
117 }
118
119 pub fn as_slice(&self) -> &[u8] {
121 self.0.as_slice()
122 }
123
124 pub fn into_bytes(self) -> Vec<u8> {
126 self.0
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.0.is_empty()
132 }
133}
134
135impl fmt::Debug for Token {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "****")
138 }
139}
140impl Credentials {
141 #[allow(dead_code)]
143 pub fn basic(username: Option<String>, password: Option<String>) -> Self {
144 Self::Basic {
145 username: Username::new(username),
146 password: password.map(Password),
147 }
148 }
149
150 #[allow(dead_code)]
152 pub fn bearer(token: Vec<u8>) -> Self {
153 Self::Bearer {
154 token: Token::new(token),
155 }
156 }
157
158 pub fn username(&self) -> Option<&str> {
159 match self {
160 Self::Basic { username, .. } => username.as_deref(),
161 Self::Bearer { .. } => None,
162 }
163 }
164
165 pub(crate) fn to_username(&self) -> Username {
166 match self {
167 Self::Basic { username, .. } => username.clone(),
168 Self::Bearer { .. } => Username::none(),
169 }
170 }
171
172 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
173 match self {
174 Self::Basic { username, .. } => Cow::Borrowed(username),
175 Self::Bearer { .. } => Cow::Owned(Username::none()),
176 }
177 }
178
179 pub fn password(&self) -> Option<&str> {
180 match self {
181 Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
182 Self::Bearer { .. } => None,
183 }
184 }
185
186 pub fn is_authenticated(&self) -> bool {
187 match self {
188 Self::Basic {
189 username: _,
190 password,
191 } => password.is_some(),
192 Self::Bearer { token } => !token.is_empty(),
193 }
194 }
195
196 pub(crate) fn is_empty(&self) -> bool {
197 match self {
198 Self::Basic { username, password } => username.is_none() && password.is_none(),
199 Self::Bearer { token } => token.is_empty(),
200 }
201 }
202
203 pub(crate) fn from_netrc(
207 netrc: &Netrc,
208 url: &DisplaySafeUrl,
209 username: Option<&str>,
210 ) -> Option<Self> {
211 let host = url.host_str()?;
212 let entry = netrc
213 .hosts
214 .get(host)
215 .or_else(|| netrc.hosts.get("default"))?;
216
217 if username.is_some_and(|username| username != entry.login) {
219 return None;
220 }
221
222 Some(Self::Basic {
223 username: Username::new(Some(entry.login.clone())),
224 password: Some(Password(entry.password.clone())),
225 })
226 }
227
228 pub fn from_url(url: &Url) -> Option<Self> {
232 if url.username().is_empty() && url.password().is_none() {
233 return None;
234 }
235 Some(Self::Basic {
236 username: if url.username().is_empty() {
239 None
240 } else {
241 Some(
242 percent_encoding::percent_decode_str(url.username())
243 .decode_utf8()
244 .expect("An encoded username should always decode")
245 .into_owned(),
246 )
247 }
248 .into(),
249 password: url.password().map(|password| {
250 Password(
251 percent_encoding::percent_decode_str(password)
252 .decode_utf8()
253 .expect("An encoded password should always decode")
254 .into_owned(),
255 )
256 }),
257 })
258 }
259
260 pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
265 let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
266 let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
267 if username.is_none() && password.is_none() {
268 None
269 } else {
270 Some(Self::basic(username, password))
271 }
272 }
273
274 pub(crate) fn from_request(request: &Request) -> Option<Self> {
278 Self::from_url(request.url()).or(
280 request
282 .headers()
283 .get(reqwest::header::AUTHORIZATION)
284 .map(Self::from_header_value)?,
285 )
286 }
287
288 pub(crate) fn from_header_value(header: &HeaderValue) -> Option<Self> {
297 if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
299 let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
300 let mut buf = String::new();
301 decoder
302 .read_to_string(&mut buf)
303 .expect("HTTP Basic Authentication should be base64 encoded");
304 let (username, password) = buf
305 .split_once(':')
306 .expect("HTTP Basic Authentication should include a `:` separator");
307 let username = if username.is_empty() {
308 None
309 } else {
310 Some(username.to_string())
311 };
312 let password = if password.is_empty() {
313 None
314 } else {
315 Some(password.to_string())
316 };
317 return Some(Self::Basic {
318 username: Username::new(username),
319 password: password.map(Password),
320 });
321 }
322
323 if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
325 return Some(Self::Bearer {
326 token: Token::new(token.to_vec()),
327 });
328 }
329
330 None
331 }
332
333 pub fn to_header_value(&self) -> HeaderValue {
337 match self {
338 Self::Basic { .. } => {
339 let mut buf = b"Basic ".to_vec();
341 {
342 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
343 write!(encoder, "{}:", self.username().unwrap_or_default())
344 .expect("Write to base64 encoder should succeed");
345 if let Some(password) = self.password() {
346 write!(encoder, "{password}")
347 .expect("Write to base64 encoder should succeed");
348 }
349 }
350 let mut header =
351 HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
352 header.set_sensitive(true);
353 header
354 }
355 Self::Bearer { token } => {
356 let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
357 .expect("Bearer token is always valid HeaderValue");
358 header.set_sensitive(true);
359 header
360 }
361 }
362 }
363
364 #[must_use]
368 pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
369 if let Some(username) = self.username() {
370 let _ = url.set_username(username);
371 }
372 if let Some(password) = self.password() {
373 let _ = url.set_password(Some(password));
374 }
375 url
376 }
377
378 #[must_use]
382 pub fn authenticate(&self, mut request: Request) -> Request {
383 request
384 .headers_mut()
385 .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
386 request
387 }
388}
389
390#[derive(Clone, Debug)]
391pub(crate) enum Authentication {
392 Credentials(Credentials),
394
395 AwsSigner(AwsDefaultSigner),
397
398 GcsSigner(GcsDefaultSigner),
400
401 AzureSigner(AzureDefaultSigner),
403}
404
405#[derive(Debug, Error)]
406pub(crate) enum AuthenticationError {
407 #[error("Failed to convert request URL to URI")]
408 InvalidUri(#[from] http::uri::InvalidUri),
409
410 #[error("Failed to build request for {provider} signing")]
411 BuildRequest {
412 provider: &'static str,
413 #[source]
414 source: http::Error,
415 },
416
417 #[error("Failed to sign request with {provider} credentials")]
418 Sign {
419 provider: &'static str,
420 #[source]
421 source: reqsign::Error,
422 },
423}
424
425impl PartialEq for Authentication {
426 fn eq(&self, other: &Self) -> bool {
427 match (self, other) {
428 (Self::Credentials(a), Self::Credentials(b)) => a == b,
429 (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
430 (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
431 (Self::AzureSigner(..), Self::AzureSigner(..)) => true,
432 _ => false,
433 }
434 }
435}
436
437impl Eq for Authentication {}
438
439impl From<Credentials> for Authentication {
440 fn from(credentials: Credentials) -> Self {
441 Self::Credentials(credentials)
442 }
443}
444
445impl From<AwsDefaultSigner> for Authentication {
446 fn from(signer: AwsDefaultSigner) -> Self {
447 Self::AwsSigner(signer)
448 }
449}
450
451impl From<GcsDefaultSigner> for Authentication {
452 fn from(signer: GcsDefaultSigner) -> Self {
453 Self::GcsSigner(signer)
454 }
455}
456
457impl From<AzureDefaultSigner> for Authentication {
458 fn from(signer: AzureDefaultSigner) -> Self {
459 Self::AzureSigner(signer)
460 }
461}
462
463impl Authentication {
464 pub(crate) fn password(&self) -> Option<&str> {
466 match self {
467 Self::Credentials(credentials) => credentials.password(),
468 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
469 }
470 }
471
472 pub(crate) fn username(&self) -> Option<&str> {
474 match self {
475 Self::Credentials(credentials) => credentials.username(),
476 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
477 }
478 }
479
480 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
482 match self {
483 Self::Credentials(credentials) => credentials.as_username(),
484 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => {
485 Cow::Owned(Username::none())
486 }
487 }
488 }
489
490 pub(crate) fn to_username(&self) -> Username {
492 match self {
493 Self::Credentials(credentials) => credentials.to_username(),
494 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(),
495 }
496 }
497
498 pub(crate) fn is_authenticated(&self) -> bool {
500 match self {
501 Self::Credentials(credentials) => credentials.is_authenticated(),
502 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true,
503 }
504 }
505
506 pub(crate) fn is_empty(&self) -> bool {
508 match self {
509 Self::Credentials(credentials) => credentials.is_empty(),
510 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false,
511 }
512 }
513
514 pub(crate) async fn authenticate(
518 &self,
519 mut request: Request,
520 ) -> Result<Request, AuthenticationError> {
521 match self {
522 Self::Credentials(credentials) => Ok(credentials.authenticate(request)),
523 Self::AwsSigner(signer) => {
524 let uri = Uri::from_str(request.url().as_str())?;
526 let mut http_req = http::Request::builder()
527 .method(request.method().clone())
528 .uri(uri)
529 .body(())
530 .map_err(|source| AuthenticationError::BuildRequest {
531 provider: "AWS",
532 source,
533 })?;
534 *http_req.headers_mut() = request.headers().clone();
535
536 let (mut parts, ()) = http_req.into_parts();
538 signer.sign(&mut parts, None).await.map_err(|source| {
539 AuthenticationError::Sign {
540 provider: "AWS",
541 source,
542 }
543 })?;
544
545 request.headers_mut().extend(parts.headers);
547
548 if let Some(path_and_query) = parts.uri.path_and_query() {
550 request.url_mut().set_path(path_and_query.path());
551 request.url_mut().set_query(path_and_query.query());
552 }
553 Ok(request)
554 }
555 Self::GcsSigner(signer) => {
556 let uri = Uri::from_str(request.url().as_str())?;
558 let mut http_req = http::Request::builder()
559 .method(request.method().clone())
560 .uri(uri)
561 .body(())
562 .map_err(|source| AuthenticationError::BuildRequest {
563 provider: "GCS",
564 source,
565 })?;
566 *http_req.headers_mut() = request.headers().clone();
567
568 let (mut parts, ()) = http_req.into_parts();
570 signer.sign(&mut parts, None).await.map_err(|source| {
571 AuthenticationError::Sign {
572 provider: "GCS",
573 source,
574 }
575 })?;
576
577 request.headers_mut().extend(parts.headers);
579
580 if let Some(path_and_query) = parts.uri.path_and_query() {
582 request.url_mut().set_path(path_and_query.path());
583 request.url_mut().set_query(path_and_query.query());
584 }
585 Ok(request)
586 }
587 Self::AzureSigner(signer) => {
588 let uri = Uri::from_str(request.url().as_str())?;
590 let mut http_req = http::Request::builder()
591 .method(request.method().clone())
592 .uri(uri)
593 .body(())
594 .map_err(|source| AuthenticationError::BuildRequest {
595 provider: "Azure",
596 source,
597 })?;
598 *http_req.headers_mut() = request.headers().clone();
599 http_req
600 .headers_mut()
601 .entry(HeaderName::from_static("x-ms-version"))
602 .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION));
603
604 let (mut parts, ()) = http_req.into_parts();
606 signer.sign(&mut parts, None).await.map_err(|source| {
607 AuthenticationError::Sign {
608 provider: "Azure",
609 source,
610 }
611 })?;
612
613 request.headers_mut().extend(parts.headers);
615
616 if let Some(path_and_query) = parts.uri.path_and_query() {
618 request.url_mut().set_path(path_and_query.path());
619 request.url_mut().set_query(path_and_query.query());
620 }
621 Ok(request)
622 }
623 }
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use insta::assert_debug_snapshot;
630 use reqsign::aws::Credential as AwsCredential;
631 use reqsign::azure::Credential as AzureCredential;
632 use reqsign::{Context, ProvideCredential};
633
634 use super::*;
635
636 #[derive(Debug)]
637 struct EmptyAwsCredentialProvider;
638
639 impl ProvideCredential for EmptyAwsCredentialProvider {
640 type Credential = AwsCredential;
641
642 async fn provide_credential(
643 &self,
644 _ctx: &Context,
645 ) -> reqsign::Result<Option<Self::Credential>> {
646 Ok(None)
647 }
648 }
649
650 #[derive(Debug)]
651 struct EmptyAzureCredentialProvider;
652
653 impl ProvideCredential for EmptyAzureCredentialProvider {
654 type Credential = AzureCredential;
655
656 async fn provide_credential(
657 &self,
658 _ctx: &Context,
659 ) -> reqsign::Result<Option<Self::Credential>> {
660 Ok(None)
661 }
662 }
663
664 #[test]
665 fn from_url_no_credentials() {
666 let url = &Url::parse("https://example.com/simple/first/").unwrap();
667 assert_eq!(Credentials::from_url(url), None);
668 }
669
670 #[test]
671 fn from_url_username_and_password() {
672 let url = &Url::parse("https://example.com/simple/first/").unwrap();
673 let mut auth_url = url.clone();
674 auth_url.set_username("user").unwrap();
675 auth_url.set_password(Some("password")).unwrap();
676 let credentials = Credentials::from_url(&auth_url).unwrap();
677 assert_eq!(credentials.username(), Some("user"));
678 assert_eq!(credentials.password(), Some("password"));
679 }
680
681 #[test]
682 fn from_url_no_username() {
683 let url = &Url::parse("https://example.com/simple/first/").unwrap();
684 let mut auth_url = url.clone();
685 auth_url.set_password(Some("password")).unwrap();
686 let credentials = Credentials::from_url(&auth_url).unwrap();
687 assert_eq!(credentials.username(), None);
688 assert_eq!(credentials.password(), Some("password"));
689 }
690
691 #[test]
696 fn from_url_empty_username_with_password() {
697 let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
699 let credentials = Credentials::from_url(&url).unwrap();
700 assert_eq!(credentials.username(), None);
701 assert_eq!(credentials.password(), Some("token"));
702 assert!(
703 credentials.is_authenticated(),
704 "URL with empty username but password should be considered authenticated"
705 );
706 }
707
708 #[test]
709 fn from_url_no_password() {
710 let url = &Url::parse("https://example.com/simple/first/").unwrap();
711 let mut auth_url = url.clone();
712 auth_url.set_username("user").unwrap();
713 let credentials = Credentials::from_url(&auth_url).unwrap();
714 assert_eq!(credentials.username(), Some("user"));
715 assert_eq!(credentials.password(), None);
716 }
717
718 #[test]
719 fn authenticated_request_from_url() {
720 let url = Url::parse("https://example.com/simple/first/").unwrap();
721 let mut auth_url = url.clone();
722 auth_url.set_username("user").unwrap();
723 auth_url.set_password(Some("password")).unwrap();
724 let credentials = Credentials::from_url(&auth_url).unwrap();
725
726 let mut request = Request::new(reqwest::Method::GET, url);
727 request = credentials.authenticate(request);
728
729 let mut header = request
730 .headers()
731 .get(reqwest::header::AUTHORIZATION)
732 .expect("Authorization header should be set")
733 .clone();
734 header.set_sensitive(false);
735
736 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
737 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
738 }
739
740 #[test]
741 fn authenticated_request_from_url_with_percent_encoded_user() {
742 let url = Url::parse("https://example.com/simple/first/").unwrap();
743 let mut auth_url = url.clone();
744 auth_url.set_username("user@domain").unwrap();
745 auth_url.set_password(Some("password")).unwrap();
746 let credentials = Credentials::from_url(&auth_url).unwrap();
747
748 let mut request = Request::new(reqwest::Method::GET, url);
749 request = credentials.authenticate(request);
750
751 let mut header = request
752 .headers()
753 .get(reqwest::header::AUTHORIZATION)
754 .expect("Authorization header should be set")
755 .clone();
756 header.set_sensitive(false);
757
758 assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
759 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
760 }
761
762 #[test]
763 fn authenticated_request_from_url_with_percent_encoded_password() {
764 let url = Url::parse("https://example.com/simple/first/").unwrap();
765 let mut auth_url = url.clone();
766 auth_url.set_username("user").unwrap();
767 auth_url.set_password(Some("password==")).unwrap();
768 let credentials = Credentials::from_url(&auth_url).unwrap();
769
770 let mut request = Request::new(reqwest::Method::GET, url);
771 request = credentials.authenticate(request);
772
773 let mut header = request
774 .headers()
775 .get(reqwest::header::AUTHORIZATION)
776 .expect("Authorization header should be set")
777 .clone();
778 header.set_sensitive(false);
779
780 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
781 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
782 }
783
784 #[tokio::test]
785 async fn authenticated_request_with_azure_signer() {
786 let signer = reqsign::azure::default_signer().with_credential_provider(
787 reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
788 );
789 let authentication = Authentication::from(signer);
790
791 let request = Request::new(
792 reqwest::Method::GET,
793 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
794 );
795 let request = authentication.authenticate(request).await.unwrap();
796
797 let authorization = request
798 .headers()
799 .get(reqwest::header::AUTHORIZATION)
800 .expect("Authorization header should be set");
801 assert_eq!(authorization.to_str().unwrap(), "Bearer token");
802 assert!(request.headers().contains_key("x-ms-date"));
803 assert_eq!(
804 request
805 .headers()
806 .get("x-ms-version")
807 .expect("x-ms-version header should be set")
808 .to_str()
809 .unwrap(),
810 AZURE_STORAGE_VERSION
811 );
812 }
813
814 #[tokio::test]
815 async fn authenticated_request_with_aws_signer_missing_credentials() {
816 let signer = reqsign::aws::default_signer("s3", "us-east-1")
817 .with_credential_provider(EmptyAwsCredentialProvider);
818 let authentication = Authentication::from(signer);
819
820 let request = Request::new(
821 reqwest::Method::GET,
822 Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
823 );
824 let err = authentication.authenticate(request).await.unwrap_err();
825
826 insta::assert_snapshot!(
827 err.to_string(),
828 @"Failed to sign request with AWS credentials"
829 );
830 }
831
832 #[tokio::test]
833 async fn authenticated_request_with_azure_signer_missing_credentials() {
834 let signer =
835 reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
836 let authentication = Authentication::from(signer);
837
838 let request = Request::new(
839 reqwest::Method::GET,
840 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
841 );
842 let err = authentication.authenticate(request).await.unwrap_err();
843
844 insta::assert_snapshot!(
845 err.to_string(),
846 @"Failed to sign request with Azure credentials"
847 );
848 }
849
850 #[test]
852 fn test_password_redaction() {
853 let credentials =
854 Credentials::basic(Some(String::from("user")), Some(String::from("password")));
855 insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
856 }
857
858 #[test]
860 fn test_bearer_token_redaction() {
861 let token = "super_secret_token";
862 let credentials = Credentials::bearer(token.into());
863 insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
864 }
865}