1use std::path::Path;
30
31use crate::config::{parse_bool, ConfigFile, ConfigScope, ConfigSet};
32use crate::error::{Error, Result};
33use crate::partial_clone::ObjectFilterSpec;
34use crate::rev_list::ObjectFilter;
35
36#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
41#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
42pub enum PromisorField {
43 PartialCloneFilter,
46 Token,
48}
49
50impl PromisorField {
51 pub const ALL: [Self; 2] = [Self::PartialCloneFilter, Self::Token];
53
54 #[must_use]
60 pub const fn wire_name(self) -> &'static str {
61 match self {
62 Self::PartialCloneFilter => "partialCloneFilter",
63 Self::Token => "token",
64 }
65 }
66
67 #[must_use]
73 pub const fn config_key(self) -> &'static str {
74 match self {
75 Self::PartialCloneFilter => "partialclonefilter",
76 Self::Token => "token",
77 }
78 }
79
80 #[must_use]
92 pub fn parse_list(raw: &str) -> (Vec<Self>, Vec<String>) {
93 let mut known = Vec::new();
94 let mut unknown = Vec::new();
95 for name in raw
96 .split([',', ' ', '\t'])
97 .map(str::trim)
98 .filter(|s| !s.is_empty())
99 {
100 match Self::ALL
101 .into_iter()
102 .find(|f| f.wire_name().eq_ignore_ascii_case(name))
103 {
104 Some(field) if !known.contains(&field) => known.push(field),
105 Some(_) => {}
106 None => unknown.push(name.to_owned()),
107 }
108 }
109 (known, unknown)
110 }
111
112 fn value_of(self, info: &PromisorInfo) -> Option<&str> {
113 match self {
114 Self::PartialCloneFilter => info.filter.as_deref(),
115 Self::Token => info.token.as_deref(),
116 }
117 }
118
119 fn set_on(self, info: &mut PromisorInfo, value: String) {
120 match self {
121 Self::PartialCloneFilter => info.filter = Some(value),
122 Self::Token => info.token = Some(value),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
130#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
131pub enum AcceptFromServer {
132 #[default]
134 None,
135 KnownUrl,
137 KnownName,
139 All,
141}
142
143impl AcceptFromServer {
144 #[must_use]
150 pub fn parse(value: &str) -> Option<Self> {
151 let value = value.trim();
152 [
153 ("none", Self::None),
154 ("knownurl", Self::KnownUrl),
155 ("knownname", Self::KnownName),
156 ("all", Self::All),
157 ]
158 .into_iter()
159 .find_map(|(name, policy)| value.eq_ignore_ascii_case(name).then_some(policy))
160 }
161}
162
163#[non_exhaustive]
165#[derive(Debug, Clone, PartialEq, Eq)]
166#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
167#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
168pub enum PromisorConfigWarning {
169 UnknownAcceptValue(String),
171 UnknownField {
173 key: &'static str,
175 field: String,
177 },
178 InvalidAdvertisedValue {
181 remote: String,
183 field: PromisorField,
185 },
186 UnusableAutoFilter(String),
189}
190
191#[derive(Debug, Clone, Default, PartialEq, Eq)]
193#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
194pub struct PromisorAcceptPolicy {
195 pub accept: AcceptFromServer,
197 pub check_fields: Vec<PromisorField>,
199 pub store_fields: Vec<PromisorField>,
201}
202
203impl PromisorAcceptPolicy {
204 #[must_use]
215 pub fn from_config(cfg: &ConfigSet) -> (Self, Vec<PromisorConfigWarning>) {
216 let mut warnings = Vec::new();
217 let accept = match cfg.get("promisor.acceptFromServer") {
218 None => AcceptFromServer::None,
219 Some(raw) => AcceptFromServer::parse(&raw).unwrap_or_else(|| {
220 warnings.push(PromisorConfigWarning::UnknownAcceptValue(raw));
221 AcceptFromServer::None
222 }),
223 };
224 let mut fields = |key: &'static str| {
225 let (known, unknown) = PromisorField::parse_list(&cfg.get(key).unwrap_or_default());
226 warnings.extend(
227 unknown
228 .into_iter()
229 .map(|field| PromisorConfigWarning::UnknownField { key, field }),
230 );
231 known
232 };
233 let check_fields = fields("promisor.checkFields");
234 let store_fields = fields("promisor.storeFields");
235 (
236 Self {
237 accept,
238 check_fields,
239 store_fields,
240 },
241 warnings,
242 )
243 }
244}
245
246#[derive(Clone, Default, PartialEq, Eq)]
251#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
252pub struct PromisorInfo {
253 pub name: String,
255 #[cfg_attr(
258 feature = "serde-models",
259 serde(serialize_with = "serialize_redacted_url")
260 )]
261 pub url: Option<String>,
262 pub filter: Option<String>,
264 #[cfg_attr(feature = "serde-models", serde(skip))]
266 pub token: Option<String>,
267}
268
269impl PromisorInfo {
270 #[must_use]
276 pub fn display_url(&self) -> String {
277 crate::transport_path::redact_url_for_display(self.url.as_deref().unwrap_or_default())
278 }
279}
280
281#[cfg(feature = "serde-models")]
282fn serialize_redacted_url<S: serde::Serializer>(
283 url: &Option<String>,
284 serializer: S,
285) -> std::result::Result<S::Ok, S::Error> {
286 match url {
287 Some(url) => serializer.serialize_some(&crate::transport_path::redact_url_for_display(url)),
288 None => serializer.serialize_none(),
289 }
290}
291
292impl std::fmt::Debug for PromisorInfo {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("PromisorInfo")
295 .field("name", &self.name)
296 .field("url", &self.url.as_ref().map(|_| self.display_url()))
297 .field("filter", &self.filter)
298 .field("token", &self.token.as_ref().map(|_| "<redacted>"))
299 .finish()
300 }
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Eq)]
305#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
306pub struct PromisorAdvertisement {
307 pub remotes: Vec<PromisorInfo>,
309}
310
311impl PromisorAdvertisement {
312 #[must_use]
325 pub fn from_server_config(cfg: &ConfigSet) -> Option<Self> {
326 let advertise = cfg
327 .get_bool("promisor.advertise")
328 .and_then(|r| r.ok())
329 .unwrap_or(false);
330 if !advertise {
331 return None;
332 }
333 let (fields, _unknown) =
334 PromisorField::parse_list(&cfg.get("promisor.sendFields").unwrap_or_default());
335 let remotes = configured_promisor_remotes(cfg, &fields);
336 (!remotes.is_empty()).then_some(Self { remotes })
337 }
338
339 #[must_use]
349 pub fn parse(value: &str) -> Self {
350 Self {
351 remotes: value.split(';').filter_map(parse_one_advertised).collect(),
352 }
353 }
354
355 #[must_use]
361 pub fn to_capability_value(&self) -> String {
362 let mut out = String::new();
363 for (i, p) in self.remotes.iter().enumerate() {
364 if i != 0 {
365 out.push(';');
366 }
367 out.push_str("name=");
368 out.push_str(&urlencode(&p.name));
369 out.push_str(",url=");
370 out.push_str(&urlencode(p.url.as_deref().unwrap_or("")));
371 for field in PromisorField::ALL {
372 if let Some(v) = field.value_of(p) {
373 out.push(',');
374 out.push_str(field.wire_name());
375 out.push('=');
376 out.push_str(&urlencode(v));
377 }
378 }
379 }
380 out
381 }
382
383 pub fn names(&self) -> impl Iterator<Item = &str> {
385 self.remotes.iter().map(|r| r.name.as_str())
386 }
387}
388
389fn urlencode(s: &str) -> String {
392 use std::fmt::Write as _;
393 let mut out = String::with_capacity(s.len());
394 for &b in s.as_bytes() {
395 if b != b',' && b != b';' && b != b'%' && (33..=126).contains(&b) {
396 out.push(char::from(b));
397 } else {
398 let _ = write!(out, "%{b:02X}");
399 }
400 }
401 out
402}
403
404fn urldecode(s: &str) -> String {
406 let bytes = s.as_bytes();
407 let mut out = Vec::with_capacity(bytes.len());
408 let mut i = 0;
409 while i < bytes.len() {
410 if bytes[i] == b'%' && i + 2 < bytes.len() {
411 let hex = |b: u8| char::from(b).to_digit(16);
412 if let (Some(hi), Some(lo)) = (hex(bytes[i + 1]), hex(bytes[i + 2])) {
413 out.push((hi * 16 + lo) as u8);
414 i += 3;
415 continue;
416 }
417 }
418 out.push(bytes[i]);
419 i += 1;
420 }
421 String::from_utf8_lossy(&out).into_owned()
422}
423
424fn parse_one_advertised(remote_info: &str) -> Option<PromisorInfo> {
426 let mut info = PromisorInfo::default();
427 for elem in remote_info.split(',') {
428 let Some((field, value)) = elem.split_once('=') else {
429 continue;
430 };
431 let value = urldecode(value);
432 match field {
433 "name" => info.name = value,
434 "url" => info.url = Some(value),
435 other => {
436 if let Some(f) = PromisorField::ALL
437 .into_iter()
438 .find(|f| f.wire_name() == other)
439 {
440 f.set_on(&mut info, value);
441 }
442 }
443 }
444 }
445 if info.name.is_empty() || info.url.as_deref().is_none_or(str::is_empty) {
446 return None;
447 }
448 Some(info)
449}
450
451fn promisor_remote_names(cfg: &ConfigSet) -> Vec<String> {
457 let mut names: Vec<String> = Vec::new();
458 for e in cfg.entries() {
459 let Some(rest) = e.key.strip_prefix("remote.") else {
460 continue;
461 };
462 let name = if let Some(name) = rest.strip_suffix(".promisor") {
463 let on = e.value.as_deref().map_or(Ok(true), parse_bool);
464 on.unwrap_or(false).then_some(name)
465 } else {
466 rest.strip_suffix(".partialclonefilter")
467 };
468 if let Some(name) = name.filter(|n| !n.is_empty()) {
469 if !names.iter().any(|n| n == name) {
470 names.push(name.to_owned());
471 }
472 }
473 }
474 if let Some(pc) = cfg.get("extensions.partialclone").filter(|s| !s.is_empty()) {
475 names.retain(|n| *n != pc);
476 names.push(pc);
477 }
478 names
479}
480
481pub(crate) fn request_filter_omits_promised_blobs(cfg: &ConfigSet, request: &ObjectFilter) -> bool {
492 let names = promisor_remote_names(cfg);
493 if names.is_empty() {
494 return false;
495 }
496 let mut floor = u64::MAX;
498 for name in names {
499 let filter = cfg
500 .get(&format!("remote.{name}.partialclonefilter"))
501 .and_then(|spec| ObjectFilter::parse(&spec).ok());
502 match filter {
503 Some(ObjectFilter::BlobNone) => floor = 0,
504 Some(ObjectFilter::BlobLimit(n)) => floor = floor.min(n),
505 _ => return false,
506 }
507 }
508 omits_blobs_from(request, floor)
509}
510
511fn omits_blobs_from(filter: &ObjectFilter, floor: u64) -> bool {
513 match filter {
514 ObjectFilter::BlobNone => true,
515 ObjectFilter::BlobLimit(limit) => *limit <= floor,
516 ObjectFilter::Combine(parts) => parts.iter().any(|part| omits_blobs_from(part, floor)),
517 _ => false,
518 }
519}
520
521pub(crate) fn is_promisor_remote(cfg: &ConfigSet, name: &str) -> bool {
524 promisor_remote_names(cfg).iter().any(|n| n == name)
525}
526
527#[must_use]
538pub fn configured_promisor_remotes(cfg: &ConfigSet, fields: &[PromisorField]) -> Vec<PromisorInfo> {
539 promisor_remote_names(cfg)
540 .into_iter()
541 .filter_map(|name| {
542 let url = cfg
543 .get(&format!("remote.{name}.url"))
544 .filter(|u| !u.is_empty())?;
545 let mut info = PromisorInfo {
546 name,
547 url: Some(url),
548 ..Default::default()
549 };
550 for &field in fields {
551 if let Some(v) = cfg
552 .get(&format!("remote.{}.{}", info.name, field.config_key()))
553 .filter(|v| !v.is_empty())
554 {
555 field.set_on(&mut info, v);
556 }
557 }
558 Some(info)
559 })
560 .collect()
561}
562
563#[must_use]
573pub fn lazy_fetch_order(cfg: &ConfigSet, preferred: &[String]) -> Vec<String> {
574 let all: Vec<String> = configured_promisor_remotes(cfg, &[])
575 .into_iter()
576 .map(|p| p.name)
577 .collect();
578 let mut order: Vec<String> =
579 preferred
580 .iter()
581 .filter(|p| all.contains(p))
582 .fold(Vec::new(), |mut acc, p| {
583 if !acc.contains(p) {
584 acc.push(p.clone());
585 }
586 acc
587 });
588 for name in all {
589 if !order.contains(&name) {
590 order.push(name);
591 }
592 }
593 order
594}
595
596#[non_exhaustive]
598#[derive(Debug, Clone, PartialEq, Eq)]
599#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
600#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
601pub enum PromisorRejection {
602 PolicyNone,
604 UnknownRemote,
606 UrlMismatch {
608 configured: String,
610 },
611 FieldMissing(PromisorField),
613 FieldMismatch(PromisorField),
615 NotPartialFetch,
618}
619
620#[derive(Debug, Clone, PartialEq, Eq)]
622#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
623pub struct PromisorCandidate {
624 pub advertised: PromisorInfo,
626 pub rejection: Option<PromisorRejection>,
628}
629
630impl PromisorCandidate {
631 #[must_use]
633 pub fn is_accepted(&self) -> bool {
634 self.rejection.is_none()
635 }
636}
637
638#[derive(Debug, Clone, Default, PartialEq, Eq)]
640#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
641pub struct PromisorDecision {
642 pub candidates: Vec<PromisorCandidate>,
644}
645
646impl PromisorDecision {
647 #[must_use]
663 pub fn decide(
664 policy: &PromisorAcceptPolicy,
665 local: &[PromisorInfo],
666 adv: &PromisorAdvertisement,
667 ) -> Self {
668 let candidates = adv
669 .remotes
670 .iter()
671 .map(|advertised| PromisorCandidate {
672 advertised: advertised.clone(),
673 rejection: rejection_for(policy, local, advertised),
674 })
675 .collect();
676 Self { candidates }
677 }
678
679 pub fn accepted(&self) -> impl Iterator<Item = &PromisorInfo> {
681 self.candidates
682 .iter()
683 .filter(|c| c.is_accepted())
684 .map(|c| &c.advertised)
685 }
686
687 #[must_use]
689 pub fn accepted_names(&self) -> Vec<String> {
690 self.accepted().map(|p| p.name.clone()).collect()
691 }
692
693 #[must_use]
695 pub fn any_accepted(&self) -> bool {
696 self.accepted().next().is_some()
697 }
698
699 #[must_use]
706 pub fn reply_value(&self) -> Option<String> {
707 let names: Vec<String> = self.accepted().map(|p| urlencode(&p.name)).collect();
708 (!names.is_empty()).then(|| names.join(";"))
709 }
710
711 pub fn decline_all(&mut self, why: PromisorRejection) {
713 for c in &mut self.candidates {
714 if c.rejection.is_none() {
715 c.rejection = Some(why.clone());
716 }
717 }
718 }
719
720 #[must_use]
732 pub fn combined_filter(&self) -> (Option<ObjectFilterSpec>, Vec<String>) {
733 let mut best: Option<ObjectFilterSpec> = None;
734 let mut unsupported = Vec::new();
735 for spec in self.accepted().filter_map(|p| p.filter.as_deref()) {
736 let Ok(parsed) = ObjectFilterSpec::parse(spec) else {
737 unsupported.push(spec.to_owned());
738 continue;
739 };
740 best = Some(match (best, parsed) {
741 (None, p) => p,
742 (Some(ObjectFilterSpec::BlobNone), _) | (_, ObjectFilterSpec::BlobNone) => {
743 ObjectFilterSpec::BlobNone
744 }
745 (Some(ObjectFilterSpec::BlobLimit(a)), ObjectFilterSpec::BlobLimit(b)) => {
746 ObjectFilterSpec::BlobLimit(a.min(b))
747 }
748 });
749 }
750 (best, unsupported)
751 }
752}
753
754fn rejection_for(
755 policy: &PromisorAcceptPolicy,
756 local: &[PromisorInfo],
757 advertised: &PromisorInfo,
758) -> Option<PromisorRejection> {
759 let known = local.iter().find(|p| p.name == advertised.name);
760 match (policy.accept, known) {
761 (AcceptFromServer::None, _) => Some(PromisorRejection::PolicyNone),
762 (AcceptFromServer::All, _) => fields_rejection(policy, local, advertised, None),
763 (_, None) => Some(PromisorRejection::UnknownRemote),
764 (AcceptFromServer::KnownName, Some(p)) => {
765 fields_rejection(policy, local, advertised, Some(p))
766 }
767 (AcceptFromServer::KnownUrl, Some(p)) => {
768 if p.url != advertised.url {
769 return Some(PromisorRejection::UrlMismatch {
770 configured: p.display_url(),
771 });
772 }
773 fields_rejection(policy, local, advertised, Some(p))
774 }
775 }
776}
777
778fn fields_rejection(
781 policy: &PromisorAcceptPolicy,
782 local: &[PromisorInfo],
783 advertised: &PromisorInfo,
784 entry: Option<&PromisorInfo>,
785) -> Option<PromisorRejection> {
786 for &field in &policy.check_fields {
787 let Some(value) = field.value_of(advertised) else {
788 return Some(PromisorRejection::FieldMissing(field));
789 };
790 let matches = match entry {
791 Some(p) => match field.value_of(p) {
792 None => return Some(PromisorRejection::FieldMissing(field)),
793 Some(local_value) => local_value == value,
794 },
795 None => local.iter().any(|p| field.value_of(p) == Some(value)),
796 };
797 if !matches {
798 return Some(PromisorRejection::FieldMismatch(field));
799 }
800 }
801 None
802}
803
804#[derive(Debug, Clone, PartialEq, Eq)]
806#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
807pub struct StoredPromisorField {
808 pub remote: String,
810 pub field: PromisorField,
812 pub replaced: bool,
814}
815
816pub fn store_advertised_fields(
837 git_dir: &Path,
838 policy: &PromisorAcceptPolicy,
839 cfg: &ConfigSet,
840 decision: &PromisorDecision,
841) -> Result<(Vec<StoredPromisorField>, Vec<PromisorConfigWarning>)> {
842 let mut stored = Vec::new();
843 let mut warnings = Vec::new();
844 if policy.store_fields.is_empty() {
845 return Ok((stored, warnings));
846 }
847 let local = configured_promisor_remotes(cfg, &policy.store_fields);
848 let mut writes: Vec<(String, String)> = Vec::new();
849 for advertised in decision.accepted() {
850 let Some(known) = local.iter().find(|p| p.name == advertised.name) else {
851 continue;
852 };
853 for &field in &policy.store_fields {
854 let Some(value) = field.value_of(advertised) else {
855 continue;
856 };
857 let current = field.value_of(known);
858 if current == Some(value) {
859 continue;
860 }
861 let valid = match field {
862 PromisorField::PartialCloneFilter => {
863 crate::rev_list::ObjectFilter::parse(value).is_ok()
864 }
865 PromisorField::Token => !value.chars().any(char::is_control),
866 };
867 if !valid {
868 warnings.push(PromisorConfigWarning::InvalidAdvertisedValue {
869 remote: advertised.name.clone(),
870 field,
871 });
872 continue;
873 }
874 writes.push((
875 format!("remote.{}.{}", advertised.name, field.config_key()),
876 value.to_owned(),
877 ));
878 stored.push(StoredPromisorField {
879 remote: advertised.name.clone(),
880 field,
881 replaced: current.is_some(),
882 });
883 }
884 }
885 if writes.is_empty() {
886 return Ok((stored, warnings));
887 }
888 let path = git_dir.join("config");
889 let content = std::fs::read_to_string(&path).map_err(Error::Io)?;
890 let mut file = ConfigFile::parse(&path, &content, ConfigScope::Local)?;
891 for (key, value) in &writes {
892 file.set(key, value)?;
893 }
894 file.write()?;
895 Ok((stored, warnings))
896}
897
898#[derive(Debug, Clone, Default, PartialEq, Eq)]
903#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
904pub struct PromisorHandshake {
905 pub decision: PromisorDecision,
907 pub stored: Vec<StoredPromisorField>,
909 pub resolved_filter: Option<ObjectFilterSpec>,
912 pub warnings: Vec<PromisorConfigWarning>,
915}
916
917impl PromisorHandshake {
918 #[must_use]
934 pub fn evaluate(
935 cfg: &ConfigSet,
936 advertisement: &PromisorAdvertisement,
937 partial_fetch: bool,
938 auto_filter: bool,
939 ) -> Self {
940 let (policy, mut warnings) = PromisorAcceptPolicy::from_config(cfg);
941 let local = configured_promisor_remotes(cfg, &policy.check_fields);
942 let mut decision = PromisorDecision::decide(&policy, &local, advertisement);
943 if !partial_fetch {
944 decision.decline_all(PromisorRejection::NotPartialFetch);
945 }
946 let resolved_filter = if auto_filter {
947 let (filter, unsupported) = decision.combined_filter();
948 warnings.extend(
949 unsupported
950 .into_iter()
951 .map(PromisorConfigWarning::UnusableAutoFilter),
952 );
953 filter
954 } else {
955 None
956 };
957 Self {
958 decision,
959 stored: Vec::new(),
960 resolved_filter,
961 warnings,
962 }
963 }
964
965 pub fn store_fields(&mut self, git_dir: &Path, cfg: &ConfigSet) -> Result<()> {
977 let (policy, _) = PromisorAcceptPolicy::from_config(cfg);
978 let (stored, warnings) = store_advertised_fields(git_dir, &policy, cfg, &self.decision)?;
979 self.stored.extend(stored);
980 self.warnings.extend(warnings);
981 Ok(())
982 }
983}
984
985#[non_exhaustive]
987#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
988pub enum PromisorReplyError {
989 #[error("empty promisor-remote reply")]
991 Empty,
992}
993
994pub fn parse_accepted_reply(value: &str) -> std::result::Result<Vec<String>, PromisorReplyError> {
1004 let mut names: Vec<String> = Vec::new();
1005 for name in value.split(';').map(urldecode).filter(|n| !n.is_empty()) {
1006 if !names.contains(&name) {
1007 names.push(name);
1008 }
1009 }
1010 if names.is_empty() {
1011 return Err(PromisorReplyError::Empty);
1012 }
1013 Ok(names)
1014}
1015
1016#[must_use]
1025pub fn promisor_remote_info(cfg: &ConfigSet) -> Option<String> {
1026 PromisorAdvertisement::from_server_config(cfg).map(|a| a.to_capability_value())
1027}
1028
1029pub struct PromisorReply {
1031 pub reply: Option<String>,
1033 pub accepted: Vec<String>,
1035 pub accepted_filters: Vec<(String, String)>,
1037}
1038
1039#[must_use]
1044pub fn parse_advertisement(info: &str) -> Vec<PromisorInfo> {
1045 PromisorAdvertisement::parse(info).remotes
1046}
1047
1048#[must_use]
1058pub fn promisor_remote_reply(cfg: &ConfigSet, info: &str) -> PromisorReply {
1059 let (policy, _warnings) = PromisorAcceptPolicy::from_config(cfg);
1060 let local = configured_promisor_remotes(cfg, &policy.check_fields);
1061 let decision = PromisorDecision::decide(&policy, &local, &PromisorAdvertisement::parse(info));
1062 PromisorReply {
1063 reply: decision.reply_value(),
1064 accepted: decision.accepted_names(),
1065 accepted_filters: decision
1066 .accepted()
1067 .filter_map(|p| p.filter.clone().map(|f| (p.name.clone(), f)))
1068 .collect(),
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 fn cfg(pairs: &[(&str, &str)]) -> ConfigSet {
1077 let mut c = ConfigSet::default();
1078 for (k, v) in pairs {
1079 c.add_command_override(k, v).unwrap();
1080 }
1081 c
1082 }
1083
1084 #[test]
1085 fn request_filters_that_leave_out_promised_blobs() {
1086 let parse = |spec: &str| ObjectFilter::parse(spec).unwrap();
1087 let lop = cfg(&[
1088 ("remote.lop.url", "https://lop.example.com/r"),
1089 ("remote.lop.promisor", "true"),
1090 ("remote.lop.partialCloneFilter", "blob:limit=1m"),
1091 ]);
1092 for (spec, omits) in [
1093 ("blob:none", true),
1094 ("blob:limit=1m", true),
1095 ("blob:limit=4k", true),
1096 ("blob:limit=2m", false),
1097 ("tree:0", false),
1098 ("combine:tree:3+blob:limit=512k", true),
1099 ("combine:tree:3+blob:limit=2m", false),
1100 ] {
1101 assert_eq!(
1102 request_filter_omits_promised_blobs(&lop, &parse(spec)),
1103 omits,
1104 "{spec}"
1105 );
1106 }
1107 let two = cfg(&[
1109 ("remote.lop.promisor", "true"),
1110 ("remote.lop.partialCloneFilter", "blob:limit=1m"),
1111 ("remote.small.partialCloneFilter", "blob:limit=64k"),
1112 ]);
1113 assert!(request_filter_omits_promised_blobs(
1114 &two,
1115 &parse("blob:limit=64k")
1116 ));
1117 assert!(!request_filter_omits_promised_blobs(
1118 &two,
1119 &parse("blob:limit=1m")
1120 ));
1121 let none = cfg(&[("remote.origin.partialCloneFilter", "blob:none")]);
1122 assert!(request_filter_omits_promised_blobs(
1123 &none,
1124 &parse("blob:none")
1125 ));
1126 assert!(!request_filter_omits_promised_blobs(
1127 &none,
1128 &parse("blob:limit=1")
1129 ));
1130 let unfiltered = cfg(&[("remote.lop.promisor", "true")]);
1132 assert!(!request_filter_omits_promised_blobs(
1133 &unfiltered,
1134 &parse("blob:none")
1135 ));
1136 assert!(!request_filter_omits_promised_blobs(
1137 &ConfigSet::default(),
1138 &parse("blob:none")
1139 ));
1140 }
1141
1142 fn adv(value: &str) -> PromisorAdvertisement {
1143 PromisorAdvertisement::parse(value)
1144 }
1145
1146 fn decide(client: &[(&str, &str)], value: &str) -> PromisorDecision {
1147 let c = cfg(client);
1148 let (policy, _) = PromisorAcceptPolicy::from_config(&c);
1149 let local = configured_promisor_remotes(&c, &policy.check_fields);
1150 PromisorDecision::decide(&policy, &local, &adv(value))
1151 }
1152
1153 fn rejection(d: &PromisorDecision) -> Option<PromisorRejection> {
1154 d.candidates[0].rejection.clone()
1155 }
1156
1157 #[test]
1158 fn no_advertise_when_disabled() {
1159 let c = cfg(&[
1160 ("remote.lop.promisor", "true"),
1161 ("remote.lop.url", "file:///lop"),
1162 ]);
1163 assert_eq!(promisor_remote_info(&c), None);
1164 let c = cfg(&[
1165 ("promisor.advertise", "false"),
1166 ("remote.lop.promisor", "true"),
1167 ("remote.lop.url", "file:///lop"),
1168 ]);
1169 assert_eq!(PromisorAdvertisement::from_server_config(&c), None);
1170 }
1171
1172 #[test]
1173 fn no_advertise_without_promisor_remotes() {
1174 let c = cfg(&[
1175 ("promisor.advertise", "true"),
1176 ("remote.origin.url", "file:///o"),
1177 ]);
1178 assert_eq!(promisor_remote_info(&c), None);
1179 }
1180
1181 #[test]
1182 fn advertise_name_and_url() {
1183 let c = cfg(&[
1184 ("promisor.advertise", "true"),
1185 ("remote.lop.promisor", "true"),
1186 ("remote.lop.url", "file:///lop"),
1187 ]);
1188 assert_eq!(
1189 promisor_remote_info(&c).as_deref(),
1190 Some("name=lop,url=file:///lop")
1191 );
1192 }
1193
1194 #[test]
1195 fn advertise_escapes_separators_percent_and_space() {
1196 let c = cfg(&[
1197 ("promisor.advertise", "true"),
1198 ("remote.lop.promisor", "true"),
1199 ("remote.lop.url", "file:///a b,c;d%e"),
1200 ]);
1201 assert_eq!(
1202 promisor_remote_info(&c).as_deref(),
1203 Some("name=lop,url=file:///a%20b%2Cc%3Bd%25e")
1204 );
1205 }
1206
1207 #[test]
1208 fn advertise_send_fields() {
1209 let c = cfg(&[
1210 ("promisor.advertise", "true"),
1211 ("promisor.sendFields", "partialCloneFilter, token"),
1212 ("remote.lop.promisor", "true"),
1213 ("remote.lop.url", "file:///lop"),
1214 ("remote.lop.partialCloneFilter", "blob:none"),
1215 ("remote.lop.token", "fooBar"),
1216 ]);
1217 assert_eq!(
1218 promisor_remote_info(&c).as_deref(),
1219 Some("name=lop,url=file:///lop,partialCloneFilter=blob:none,token=fooBar")
1220 );
1221 }
1222
1223 #[test]
1224 fn advertise_skips_empty_url_and_lists_partial_clone_remote_last() {
1225 let c = cfg(&[
1226 ("promisor.advertise", "true"),
1227 ("extensions.partialClone", "origin"),
1228 ("remote.origin.promisor", "true"),
1229 ("remote.origin.url", "file:///origin"),
1230 ("remote.empty.promisor", "true"),
1231 ("remote.empty.url", ""),
1232 ("remote.lop.promisor", "true"),
1233 ("remote.lop.url", "file:///lop"),
1234 ]);
1235 assert_eq!(
1236 promisor_remote_info(&c).as_deref(),
1237 Some("name=lop,url=file:///lop;name=origin,url=file:///origin")
1238 );
1239 }
1240
1241 #[test]
1242 fn partial_clone_extension_alone_makes_a_promisor_remote() {
1243 let c = cfg(&[
1244 ("extensions.partialClone", "origin"),
1245 ("remote.origin.url", "file:///origin"),
1246 ("remote.other.promisor", "false"),
1247 ("remote.other.url", "file:///other"),
1248 ]);
1249 let names: Vec<String> = configured_promisor_remotes(&c, &[])
1250 .into_iter()
1251 .map(|p| p.name)
1252 .collect();
1253 assert_eq!(names, ["origin"]);
1254 }
1255
1256 #[test]
1257 fn parse_ignores_unknown_fields_and_drops_bad_entries() {
1258 let a = adv(
1259 "name=lop,url=file:///l%20p,foo=bar,partialCloneFilter=blob:limit%3D5k;\
1260 name=nourl;name=empty,url=;url=file:///noname",
1261 );
1262 assert_eq!(a.remotes.len(), 1);
1263 assert_eq!(a.remotes[0].name, "lop");
1264 assert_eq!(a.remotes[0].url.as_deref(), Some("file:///l p"));
1265 assert_eq!(a.remotes[0].filter.as_deref(), Some("blob:limit=5k"));
1266 assert_eq!(parse_advertisement("name=x").len(), 0);
1267 }
1268
1269 #[test]
1270 fn parse_round_trips_capability_value() {
1271 let original = PromisorAdvertisement {
1272 remotes: vec![
1273 PromisorInfo {
1274 name: "a;b".into(),
1275 url: Some("https://h/p,q".into()),
1276 filter: Some("blob:none".into()),
1277 token: Some("t%k".into()),
1278 },
1279 PromisorInfo {
1280 name: "c".into(),
1281 url: Some("file:///c".into()),
1282 ..Default::default()
1283 },
1284 ],
1285 };
1286 let value = original.to_capability_value();
1287 assert_eq!(PromisorAdvertisement::parse(&value), original);
1288 assert_eq!(original.names().collect::<Vec<_>>(), ["a;b", "c"]);
1289 }
1290
1291 #[test]
1292 fn field_list_parsing() {
1293 let (known, unknown) = PromisorField::parse_list("PartialCloneFilter token,foo token");
1294 assert_eq!(
1295 known,
1296 [PromisorField::PartialCloneFilter, PromisorField::Token]
1297 );
1298 assert_eq!(unknown, ["foo"]);
1299 assert_eq!(PromisorField::Token.config_key(), "token");
1300 assert_eq!(
1301 PromisorField::PartialCloneFilter.config_key(),
1302 "partialclonefilter"
1303 );
1304 }
1305
1306 #[test]
1307 fn policy_values_are_case_insensitive_and_unknown_warns() {
1308 for (raw, want) in [
1309 ("ALL", AcceptFromServer::All),
1310 ("knownurl", AcceptFromServer::KnownUrl),
1311 ("KnownName", AcceptFromServer::KnownName),
1312 ("None", AcceptFromServer::None),
1313 ] {
1314 let (p, w) =
1315 PromisorAcceptPolicy::from_config(&cfg(&[("promisor.acceptFromServer", raw)]));
1316 assert_eq!(p.accept, want);
1317 assert!(w.is_empty());
1318 }
1319 let (p, w) = PromisorAcceptPolicy::from_config(&cfg(&[
1320 ("promisor.acceptFromServer", "sometimes"),
1321 ("promisor.checkFields", "bogus"),
1322 ]));
1323 assert_eq!(p.accept, AcceptFromServer::None);
1324 assert_eq!(
1325 w,
1326 [
1327 PromisorConfigWarning::UnknownAcceptValue("sometimes".into()),
1328 PromisorConfigWarning::UnknownField {
1329 key: "promisor.checkFields",
1330 field: "bogus".into()
1331 }
1332 ]
1333 );
1334 }
1335
1336 const LOP: [(&str, &str); 2] = [
1337 ("remote.lop.promisor", "true"),
1338 ("remote.lop.url", "file:///lop"),
1339 ];
1340
1341 #[test]
1342 fn accept_none_by_default() {
1343 let d = decide(&LOP, "name=lop,url=file:///lop");
1344 assert_eq!(rejection(&d), Some(PromisorRejection::PolicyNone));
1345 assert_eq!(d.reply_value(), None);
1346 assert!(!d.any_accepted());
1347 }
1348
1349 #[test]
1350 fn known_url_needs_matching_url() {
1351 let mut c = LOP.to_vec();
1352 c.push(("promisor.acceptFromServer", "KnownUrl"));
1353 let d = decide(&c, "name=lop,url=file:///lop");
1354 assert_eq!(d.reply_value().as_deref(), Some("lop"));
1355 let d = decide(&c, "name=lop,url=file:///elsewhere");
1356 assert_eq!(
1357 rejection(&d),
1358 Some(PromisorRejection::UrlMismatch {
1359 configured: "file:///lop".into()
1360 })
1361 );
1362 let d = decide(&c, "name=other,url=file:///lop");
1363 assert_eq!(rejection(&d), Some(PromisorRejection::UnknownRemote));
1364 }
1365
1366 #[test]
1367 fn known_name_ignores_url() {
1368 let mut c = LOP.to_vec();
1369 c.push(("promisor.acceptFromServer", "knownName"));
1370 let d = decide(&c, "name=lop,url=file:///elsewhere");
1371 assert!(d.any_accepted());
1372 let d = decide(&c, "name=other,url=file:///lop");
1373 assert_eq!(rejection(&d), Some(PromisorRejection::UnknownRemote));
1374 }
1375
1376 #[test]
1377 fn all_accepts_unknown_remotes() {
1378 let d = decide(
1379 &[("promisor.acceptFromServer", "all")],
1380 "name=lop,url=file:///lop;name=x,url=file:///x",
1381 );
1382 assert_eq!(d.accepted_names(), ["lop", "x"]);
1383 assert_eq!(d.reply_value().as_deref(), Some("lop;x"));
1384 }
1385
1386 #[test]
1387 fn check_fields_conditions() {
1388 let mut c = LOP.to_vec();
1389 c.push(("promisor.acceptFromServer", "knownName"));
1390 c.push(("promisor.checkFields", "partialCloneFilter"));
1391 let d = decide(&c, "name=lop,url=file:///lop,partialCloneFilter=blob:none");
1393 assert_eq!(
1394 rejection(&d),
1395 Some(PromisorRejection::FieldMissing(
1396 PromisorField::PartialCloneFilter
1397 ))
1398 );
1399 c.push(("remote.lop.partialCloneFilter", "blob:none"));
1400 let d = decide(&c, "name=lop,url=file:///lop");
1402 assert_eq!(
1403 rejection(&d),
1404 Some(PromisorRejection::FieldMissing(
1405 PromisorField::PartialCloneFilter
1406 ))
1407 );
1408 let d = decide(&c, "name=lop,url=file:///lop,partialCloneFilter=BLOB:none");
1410 assert_eq!(
1411 rejection(&d),
1412 Some(PromisorRejection::FieldMismatch(
1413 PromisorField::PartialCloneFilter
1414 ))
1415 );
1416 let d = decide(&c, "name=lop,url=file:///lop,partialCloneFilter=blob:none");
1417 assert!(d.any_accepted());
1418 }
1419
1420 #[test]
1421 fn check_fields_under_all_match_any_local_remote() {
1422 let c = [
1423 ("promisor.acceptFromServer", "all"),
1424 ("promisor.checkFields", "token"),
1425 ("remote.lop.promisor", "true"),
1426 ("remote.lop.url", "file:///lop"),
1427 ("remote.lop.token", "secret"),
1428 ];
1429 assert!(decide(&c, "name=new,url=file:///n,token=secret").any_accepted());
1430 assert_eq!(
1431 rejection(&decide(&c, "name=new,url=file:///n,token=other")),
1432 Some(PromisorRejection::FieldMismatch(PromisorField::Token))
1433 );
1434 }
1435
1436 #[test]
1437 fn reply_value_encodes_names() {
1438 let d = decide(
1439 &[("promisor.acceptFromServer", "all")],
1440 "name=a%3Bb,url=file:///x",
1441 );
1442 assert_eq!(d.reply_value().as_deref(), Some("a%3Bb"));
1443 assert_eq!(parse_accepted_reply("a%3Bb").unwrap(), ["a;b"]);
1444 }
1445
1446 #[test]
1447 fn decline_all_rejects_accepted_only() {
1448 let mut c = LOP.to_vec();
1449 c.push(("promisor.acceptFromServer", "knownName"));
1450 let mut d = decide(&c, "name=lop,url=file:///lop;name=x,url=file:///x");
1451 d.decline_all(PromisorRejection::NotPartialFetch);
1452 assert_eq!(
1453 d.candidates[0].rejection,
1454 Some(PromisorRejection::NotPartialFetch)
1455 );
1456 assert_eq!(
1457 d.candidates[1].rejection,
1458 Some(PromisorRejection::UnknownRemote)
1459 );
1460 }
1461
1462 #[test]
1463 fn combined_filter_rules() {
1464 let all = [("promisor.acceptFromServer", "all")];
1465 let d = decide(
1466 &all,
1467 "name=a,url=u,partialCloneFilter=blob:limit=5k;name=b,url=v,partialCloneFilter=blob:none",
1468 );
1469 assert_eq!(
1470 d.combined_filter(),
1471 (Some(ObjectFilterSpec::BlobNone), vec![])
1472 );
1473 let d = decide(
1474 &all,
1475 "name=a,url=u,partialCloneFilter=blob:limit=5k;name=b,url=v,partialCloneFilter=blob:limit=100",
1476 );
1477 assert_eq!(
1478 d.combined_filter(),
1479 (Some(ObjectFilterSpec::BlobLimit(100)), vec![])
1480 );
1481 let d = decide(&all, "name=a,url=u,partialCloneFilter=tree:0;name=b,url=v");
1482 assert_eq!(d.combined_filter(), (None, vec!["tree:0".to_owned()]));
1483 }
1484
1485 #[test]
1486 fn accepted_reply_parsing() {
1487 assert_eq!(parse_accepted_reply(""), Err(PromisorReplyError::Empty));
1488 assert_eq!(parse_accepted_reply(";"), Err(PromisorReplyError::Empty));
1489 assert_eq!(parse_accepted_reply("a;b;a").unwrap(), ["a", "b"]);
1490 }
1491
1492 #[test]
1493 fn lazy_fetch_order_rules() {
1494 let c = cfg(&[
1495 ("extensions.partialClone", "origin"),
1496 ("remote.origin.promisor", "true"),
1497 ("remote.origin.url", "file:///o"),
1498 ("remote.a.promisor", "true"),
1499 ("remote.a.url", "file:///a"),
1500 ("remote.b.partialCloneFilter", "blob:none"),
1501 ("remote.b.url", "file:///b"),
1502 ]);
1503 assert_eq!(lazy_fetch_order(&c, &[]), ["a", "b", "origin"]);
1504 assert_eq!(
1505 lazy_fetch_order(&c, &["b".into(), "gone".into(), "b".into()]),
1506 ["b", "a", "origin"]
1507 );
1508 }
1509
1510 #[test]
1511 fn debug_and_serialization_drop_url_credentials() {
1512 let p = PromisorInfo {
1513 name: "lop".into(),
1514 url: Some("https://bob:pw-secret@lop.example.com/r.git".into()),
1515 filter: None,
1516 token: Some("hunter2".into()),
1517 };
1518 let mut outputs = vec![
1519 format!("{p:?}"),
1520 format!(
1521 "{:?}",
1522 PromisorAdvertisement {
1523 remotes: vec![p.clone()]
1524 }
1525 ),
1526 ];
1527 #[cfg(feature = "serde-models")]
1528 outputs.push(serde_json::to_string(&p).unwrap());
1529 for shown in outputs {
1530 assert!(!shown.contains("pw-secret"), "{shown}");
1531 assert!(!shown.contains("hunter2"), "{shown}");
1532 assert!(shown.contains("lop.example.com/r.git"), "{shown}");
1533 }
1534
1535 let d = decide(
1536 &[
1537 ("remote.lop.promisor", "true"),
1538 (
1539 "remote.lop.url",
1540 "https://bob:pw-secret@lop.example.com/r.git",
1541 ),
1542 ("promisor.acceptFromServer", "knownUrl"),
1543 ],
1544 "name=lop,url=https://lop.example.com/other.git",
1545 );
1546 let Some(PromisorRejection::UrlMismatch { configured }) = rejection(&d) else {
1547 panic!("expected a URL mismatch");
1548 };
1549 assert!(!configured.contains("pw-secret"), "{configured}");
1550 assert!(configured.contains("lop.example.com/r.git"), "{configured}");
1551 let shown = format!("{d:?}");
1552 assert!(!shown.contains("pw-secret"), "{shown}");
1553 #[cfg(feature = "serde-models")]
1554 {
1555 let json = serde_json::to_string(&d).unwrap();
1556 assert!(!json.contains("pw-secret"), "{json}");
1557 }
1558 }
1559
1560 #[test]
1561 fn debug_redacts_token() {
1562 let p = PromisorInfo {
1563 name: "lop".into(),
1564 url: Some("u".into()),
1565 filter: None,
1566 token: Some("hunter2".into()),
1567 };
1568 let shown = format!("{p:?}");
1569 assert!(!shown.contains("hunter2"), "{shown}");
1570 assert!(shown.contains("<redacted>"));
1571 }
1572
1573 #[test]
1574 fn display_url_drops_credentials() {
1575 let p = PromisorInfo {
1576 name: "lop".into(),
1577 url: Some("https://bob:pw@lop.example.com/r.git".into()),
1578 ..Default::default()
1579 };
1580 let shown = p.display_url();
1581 assert!(!shown.contains("pw"), "{shown}");
1582 assert!(shown.contains("lop.example.com/r.git"), "{shown}");
1583 assert_eq!(PromisorInfo::default().display_url(), "");
1584 }
1585
1586 #[cfg(feature = "serde-models")]
1587 #[test]
1588 fn serialized_info_skips_token() {
1589 let p = PromisorInfo {
1590 name: "lop".into(),
1591 url: Some("u".into()),
1592 filter: None,
1593 token: Some("hunter2".into()),
1594 };
1595 let json = serde_json::to_string(&p).unwrap();
1596 assert!(!json.contains("hunter2"), "{json}");
1597 assert!(json.contains("\"name\":\"lop\""), "{json}");
1598 }
1599
1600 #[test]
1601 fn legacy_reply_wrapper() {
1602 let server = cfg(&[
1603 ("promisor.advertise", "true"),
1604 ("promisor.sendFields", "partialCloneFilter"),
1605 ("remote.lop.promisor", "true"),
1606 ("remote.lop.url", "file:///lop"),
1607 ("remote.lop.partialCloneFilter", "blob:limit=5k"),
1608 ]);
1609 let info = promisor_remote_info(&server).unwrap();
1610 let mut client = LOP.to_vec();
1611 client.push(("promisor.acceptFromServer", "All"));
1612 let r = promisor_remote_reply(&cfg(&client), &info);
1613 assert_eq!(r.reply.as_deref(), Some("lop"));
1614 assert_eq!(r.accepted, ["lop"]);
1615 assert_eq!(
1616 r.accepted_filters,
1617 [("lop".to_owned(), "blob:limit=5k".to_owned())]
1618 );
1619 }
1620
1621 #[test]
1622 fn handshake_declines_unless_partial_and_resolves_auto_filter() {
1623 let mut client = LOP.to_vec();
1624 client.push(("promisor.acceptFromServer", "knownUrl"));
1625 let c = cfg(&client);
1626 let a = adv("name=lop,url=file:///lop,partialCloneFilter=blob:limit=5k");
1627
1628 let h = PromisorHandshake::evaluate(&c, &a, false, true);
1629 assert_eq!(
1630 rejection(&h.decision),
1631 Some(PromisorRejection::NotPartialFetch)
1632 );
1633 assert_eq!(h.resolved_filter, None);
1634
1635 let h = PromisorHandshake::evaluate(&c, &a, true, true);
1636 assert!(h.decision.any_accepted());
1637 assert_eq!(
1638 h.resolved_filter,
1639 Some(ObjectFilterSpec::BlobLimit(5 * 1024))
1640 );
1641
1642 let h = PromisorHandshake::evaluate(&c, &a, true, false);
1643 assert_eq!(h.resolved_filter, None);
1644
1645 let a = adv("name=lop,url=file:///lop,partialCloneFilter=tree:0");
1646 let h = PromisorHandshake::evaluate(&c, &a, true, true);
1647 assert_eq!(
1648 h.warnings,
1649 [PromisorConfigWarning::UnusableAutoFilter("tree:0".into())]
1650 );
1651 }
1652
1653 #[test]
1654 fn handshake_store_fields() {
1655 let dir = repo_with_config(
1656 "[remote \"lop\"]\n\turl = file:///lop\n\tpromisor = true\n\
1657 [promisor]\n\tacceptFromServer = knownUrl\n\tstoreFields = partialCloneFilter\n",
1658 );
1659 let c = load(dir.path());
1660 let a = adv("name=lop,url=file:///lop,partialCloneFilter=blob:none");
1661 let mut h = PromisorHandshake::evaluate(&c, &a, true, false);
1662 h.store_fields(dir.path(), &c).unwrap();
1663 assert_eq!(h.stored.len(), 1);
1664 assert_eq!(
1665 load(dir.path())
1666 .get("remote.lop.partialclonefilter")
1667 .as_deref(),
1668 Some("blob:none")
1669 );
1670 }
1671
1672 fn repo_with_config(body: &str) -> tempfile::TempDir {
1673 let dir = tempfile::tempdir().unwrap();
1674 std::fs::write(dir.path().join("config"), body).unwrap();
1675 dir
1676 }
1677
1678 fn load(dir: &Path) -> ConfigSet {
1679 ConfigSet::load_repo_local_only(dir).unwrap()
1680 }
1681
1682 #[test]
1683 fn store_fields_writes_known_remotes_only() {
1684 let dir = repo_with_config(
1685 "[remote \"lop\"]\n\turl = file:///lop\n\tpromisor = true\n\ttoken = old\n",
1686 );
1687 let c = load(dir.path());
1688 let policy = PromisorAcceptPolicy {
1689 accept: AcceptFromServer::All,
1690 check_fields: vec![],
1691 store_fields: vec![PromisorField::PartialCloneFilter, PromisorField::Token],
1692 };
1693 let d = PromisorDecision::decide(
1694 &policy,
1695 &configured_promisor_remotes(&c, &[]),
1696 &adv(
1697 "name=lop,url=file:///evil,partialCloneFilter=blob:limit=5k,token=new;\
1698 name=unknown,url=file:///u,token=t",
1699 ),
1700 );
1701 let (stored, warnings) = store_advertised_fields(dir.path(), &policy, &c, &d).unwrap();
1702 assert!(warnings.is_empty());
1703 assert_eq!(
1704 stored,
1705 [
1706 StoredPromisorField {
1707 remote: "lop".into(),
1708 field: PromisorField::PartialCloneFilter,
1709 replaced: false
1710 },
1711 StoredPromisorField {
1712 remote: "lop".into(),
1713 field: PromisorField::Token,
1714 replaced: true
1715 }
1716 ]
1717 );
1718 let after = load(dir.path());
1719 assert_eq!(
1720 after.get("remote.lop.partialclonefilter").as_deref(),
1721 Some("blob:limit=5k")
1722 );
1723 assert_eq!(after.get("remote.lop.token").as_deref(), Some("new"));
1724 assert_eq!(after.get("remote.lop.url").as_deref(), Some("file:///lop"));
1725 assert_eq!(after.get("remote.unknown.url"), None);
1726
1727 let (stored, _) = store_advertised_fields(dir.path(), &policy, &after, &d).unwrap();
1729 assert!(stored.is_empty());
1730 }
1731
1732 #[test]
1733 fn store_fields_rejects_invalid_values() {
1734 let dir = repo_with_config("[remote \"lop\"]\n\turl = file:///lop\n\tpromisor = true\n");
1735 let c = load(dir.path());
1736 let policy = PromisorAcceptPolicy {
1737 accept: AcceptFromServer::KnownName,
1738 check_fields: vec![],
1739 store_fields: vec![PromisorField::PartialCloneFilter, PromisorField::Token],
1740 };
1741 let d = PromisorDecision::decide(
1742 &policy,
1743 &configured_promisor_remotes(&c, &[]),
1744 &adv("name=lop,url=file:///lop,partialCloneFilter=bogus:filter,token=a%0Ab"),
1745 );
1746 let (stored, warnings) = store_advertised_fields(dir.path(), &policy, &c, &d).unwrap();
1747 assert!(stored.is_empty());
1748 assert_eq!(warnings.len(), 2);
1749 let after = load(dir.path());
1750 assert_eq!(after.get("remote.lop.token"), None);
1751 assert_eq!(after.get("remote.lop.partialclonefilter"), None);
1752 }
1753}