1use core::fmt;
56use core::num::NonZeroUsize;
57use core::str::FromStr;
58
59use serde::{Deserialize, Serialize};
60use thiserror::Error;
61
62pub const SEPARATOR: char = '.';
64
65pub const WILDCARD_ONE: &str = "*";
67
68pub const WILDCARD_TAIL: &str = ">";
71
72pub const SEGMENT_MAX_LEN: usize = 64;
75
76#[derive(Clone, Debug, PartialEq, Eq, Error)]
82pub enum SegmentError {
83 #[error("segment is empty")]
85 Empty,
86 #[error("segment `{0}` contains the `{SEPARATOR}` separator")]
88 ContainsSeparator(String),
89 #[error("segment `{0}` contains whitespace")]
92 ContainsWhitespace(String),
93 #[error("`{0}` is a pattern wildcard, not a label")]
96 WildcardToken(String),
97 #[error("segment `{segment}`: illegal character `{ch}` at byte {index}; allowed: a-z A-Z 0-9 `-` `_`")]
99 IllegalChar {
100 segment: String,
102 ch: char,
104 index: usize,
106 },
107 #[error("segment `{segment}` is {len} bytes, over the {} byte maximum", SEGMENT_MAX_LEN)]
109 TooLong {
110 segment: String,
112 len: usize,
114 },
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
122#[serde(into = "String", try_from = "String")]
123pub struct Segment(String);
124
125impl Segment {
126 pub fn parse(s: &str) -> Result<Self, SegmentError> {
131 if s.is_empty() {
132 return Err(SegmentError::Empty);
133 }
134 if s == WILDCARD_ONE || s == WILDCARD_TAIL {
135 return Err(SegmentError::WildcardToken(s.to_string()));
136 }
137 if s.contains(SEPARATOR) {
138 return Err(SegmentError::ContainsSeparator(s.to_string()));
139 }
140 if s.chars().any(char::is_whitespace) {
141 return Err(SegmentError::ContainsWhitespace(s.to_string()));
142 }
143 if s.len() > SEGMENT_MAX_LEN {
144 return Err(SegmentError::TooLong {
145 segment: s.to_string(),
146 len: s.len(),
147 });
148 }
149 if let Some((index, ch)) = s.char_indices().find(|(_, c)| !Self::is_legal(*c)) {
150 return Err(SegmentError::IllegalChar {
151 segment: s.to_string(),
152 ch,
153 index,
154 });
155 }
156 Ok(Self(s.to_string()))
157 }
158
159 #[must_use]
161 pub fn as_str(&self) -> &str {
162 &self.0
163 }
164
165 fn is_legal(c: char) -> bool {
166 c.is_ascii_alphanumeric() || c == '-' || c == '_'
167 }
168}
169
170impl fmt::Display for Segment {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 f.write_str(&self.0)
173 }
174}
175
176impl FromStr for Segment {
177 type Err = SegmentError;
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 Self::parse(s)
180 }
181}
182
183impl TryFrom<String> for Segment {
184 type Error = SegmentError;
185 fn try_from(s: String) -> Result<Self, Self::Error> {
186 Self::parse(&s)
187 }
188}
189
190impl From<Segment> for String {
191 fn from(v: Segment) -> Self {
192 v.0
193 }
194}
195
196#[derive(Clone, Debug, PartialEq, Eq, Error)]
198pub enum AddressError {
199 #[error("address is empty")]
201 Empty,
202 #[error("address segment {index}: {source}")]
205 Segment {
206 index: usize,
208 #[source]
210 source: SegmentError,
211 },
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
222#[serde(into = "String", try_from = "String")]
223pub struct Address(Vec<Segment>);
224
225impl Address {
226 pub fn parse(s: &str) -> Result<Self, AddressError> {
230 if s.is_empty() {
231 return Err(AddressError::Empty);
232 }
233 let mut segments = Vec::new();
234 for (index, part) in s.split(SEPARATOR).enumerate() {
235 let seg = Segment::parse(part)
236 .map_err(|source| AddressError::Segment { index, source })?;
237 segments.push(seg);
238 }
239 Ok(Self(segments))
240 }
241
242 pub fn from_segments<I>(segments: I) -> Result<Self, AddressError>
245 where
246 I: IntoIterator<Item = Segment>,
247 {
248 let segments: Vec<Segment> = segments.into_iter().collect();
249 if segments.is_empty() {
250 return Err(AddressError::Empty);
251 }
252 Ok(Self(segments))
253 }
254
255 #[must_use]
258 pub fn root(segment: Segment) -> Self {
259 Self(vec![segment])
260 }
261
262 #[must_use]
264 pub fn segments(&self) -> &[Segment] {
265 &self.0
266 }
267
268 #[must_use]
270 pub fn leaf(&self) -> &Segment {
271 self.0.last().expect("Address is non-empty by construction")
272 }
273
274 #[must_use]
277 pub fn depth(&self) -> NonZeroUsize {
278 NonZeroUsize::new(self.0.len()).expect("Address is non-empty by construction")
279 }
280
281 #[must_use]
284 pub fn parent(&self) -> Option<Self> {
285 if self.0.len() == 1 {
286 return None;
287 }
288 Some(Self(self.0[..self.0.len() - 1].to_vec()))
289 }
290
291 #[must_use]
293 pub fn child(&self, segment: Segment) -> Self {
294 let mut segments = self.0.clone();
295 segments.push(segment);
296 Self(segments)
297 }
298
299 #[must_use]
303 pub fn starts_with(&self, prefix: &Self) -> bool {
304 self.0.len() >= prefix.0.len() && self.0[..prefix.0.len()] == prefix.0[..]
305 }
306}
307
308impl fmt::Display for Address {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 let mut first = true;
311 for seg in &self.0 {
312 if !first {
313 f.write_str(".")?;
314 }
315 f.write_str(seg.as_str())?;
316 first = false;
317 }
318 Ok(())
319 }
320}
321
322impl FromStr for Address {
323 type Err = AddressError;
324 fn from_str(s: &str) -> Result<Self, Self::Err> {
325 Self::parse(s)
326 }
327}
328
329impl TryFrom<String> for Address {
330 type Error = AddressError;
331 fn try_from(s: String) -> Result<Self, Self::Error> {
332 Self::parse(&s)
333 }
334}
335
336impl From<Address> for String {
337 fn from(v: Address) -> Self {
338 v.to_string()
339 }
340}
341
342#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
344pub enum PatternToken {
345 Literal(Segment),
347 One,
349 Tail,
353}
354
355impl fmt::Display for PatternToken {
356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357 match self {
358 PatternToken::Literal(s) => f.write_str(s.as_str()),
359 PatternToken::One => f.write_str(WILDCARD_ONE),
360 PatternToken::Tail => f.write_str(WILDCARD_TAIL),
361 }
362 }
363}
364
365#[derive(Clone, Debug, PartialEq, Eq, Error)]
367pub enum PatternError {
368 #[error("pattern is empty")]
370 Empty,
371 #[error("`{WILDCARD_TAIL}` at token {index} is not final; the tail wildcard is legal only last")]
375 TailNotFinal {
376 index: usize,
378 },
379 #[error("pattern token {index}: {source}")]
381 Token {
382 index: usize,
384 #[source]
386 source: SegmentError,
387 },
388}
389
390#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
400#[serde(into = "String", try_from = "String")]
401pub struct Pattern(Vec<PatternToken>);
402
403impl Pattern {
404 pub fn parse(s: &str) -> Result<Self, PatternError> {
406 if s.is_empty() {
407 return Err(PatternError::Empty);
408 }
409 let parts: Vec<&str> = s.split(SEPARATOR).collect();
410 let last = parts.len() - 1;
411 let mut tokens = Vec::with_capacity(parts.len());
412 for (index, part) in parts.into_iter().enumerate() {
413 let token = match part {
414 WILDCARD_ONE => PatternToken::One,
415 WILDCARD_TAIL => {
416 if index != last {
417 return Err(PatternError::TailNotFinal { index });
418 }
419 PatternToken::Tail
420 }
421 literal => PatternToken::Literal(
422 Segment::parse(literal)
423 .map_err(|source| PatternError::Token { index, source })?,
424 ),
425 };
426 tokens.push(token);
427 }
428 Ok(Self(tokens))
429 }
430
431 #[must_use]
434 pub fn exact(address: &Address) -> Self {
435 Self(
436 address
437 .segments()
438 .iter()
439 .cloned()
440 .map(PatternToken::Literal)
441 .collect(),
442 )
443 }
444
445 #[must_use]
447 pub fn tokens(&self) -> &[PatternToken] {
448 &self.0
449 }
450
451 #[must_use]
456 pub fn matches(&self, address: &Address) -> bool {
457 let segments = address.segments();
458 for (i, token) in self.0.iter().enumerate() {
459 match token {
460 PatternToken::Tail => return segments.len() > i,
463 PatternToken::One => {
464 if segments.len() <= i {
465 return false;
466 }
467 }
468 PatternToken::Literal(want) => match segments.get(i) {
469 Some(have) if have == want => {}
470 _ => return false,
471 },
472 }
473 }
474 segments.len() == self.0.len()
475 }
476}
477
478impl fmt::Display for Pattern {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 let mut first = true;
481 for token in &self.0 {
482 if !first {
483 f.write_str(".")?;
484 }
485 write!(f, "{token}")?;
486 first = false;
487 }
488 Ok(())
489 }
490}
491
492impl FromStr for Pattern {
493 type Err = PatternError;
494 fn from_str(s: &str) -> Result<Self, Self::Err> {
495 Self::parse(s)
496 }
497}
498
499impl TryFrom<String> for Pattern {
500 type Error = PatternError;
501 fn try_from(s: String) -> Result<Self, Self::Error> {
502 Self::parse(&s)
503 }
504}
505
506impl From<Pattern> for String {
507 fn from(v: Pattern) -> Self {
508 v.to_string()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 fn seg(s: &str) -> Segment {
517 Segment::parse(s).expect("test fixture must be a legal segment")
518 }
519
520 fn addr(s: &str) -> Address {
521 Address::parse(s).expect("test fixture must be a legal address")
522 }
523
524 fn pat(s: &str) -> Pattern {
525 Pattern::parse(s).expect("test fixture must be a legal pattern")
526 }
527
528 #[test]
531 fn segment_rejects_empty() {
532 assert_eq!(Segment::parse("").unwrap_err(), SegmentError::Empty);
533 }
534
535 #[test]
536 fn segment_rejects_the_separator() {
537 let err = Segment::parse("work.build").unwrap_err();
538 assert_eq!(err, SegmentError::ContainsSeparator("work.build".into()));
539 assert!(matches!(
541 Segment::parse(".x"),
542 Err(SegmentError::ContainsSeparator(_))
543 ));
544 assert!(matches!(
545 Segment::parse("x."),
546 Err(SegmentError::ContainsSeparator(_))
547 ));
548 }
549
550 #[test]
551 fn segment_rejects_whitespace() {
552 for bad in ["a b", " a", "a ", "a\tb", "a\nb", "\u{a0}a"] {
553 assert!(
554 matches!(Segment::parse(bad), Err(SegmentError::ContainsWhitespace(_))),
555 "expected whitespace rejection for {bad:?}"
556 );
557 }
558 }
559
560 #[test]
561 fn segment_rejects_the_wildcard_tokens() {
562 assert_eq!(
563 Segment::parse("*").unwrap_err(),
564 SegmentError::WildcardToken("*".into())
565 );
566 assert_eq!(
567 Segment::parse(">").unwrap_err(),
568 SegmentError::WildcardToken(">".into())
569 );
570 }
571
572 #[test]
573 fn segment_rejects_wildcard_characters_inside_a_label() {
574 match Segment::parse("a*b").unwrap_err() {
577 SegmentError::IllegalChar { ch, index, .. } => {
578 assert_eq!(ch, '*');
579 assert_eq!(index, 1);
580 }
581 other => panic!("expected IllegalChar, got {other:?}"),
582 }
583 assert!(matches!(
584 Segment::parse("a>b"),
585 Err(SegmentError::IllegalChar { ch: '>', .. })
586 ));
587 }
588
589 #[test]
590 fn segment_rejects_characters_outside_the_charset() {
591 for (bad, ch, index) in [
592 ("work/build", '/', 4),
593 ("caf\u{e9}", '\u{e9}', 3),
594 ("a:b", ':', 1),
595 ("a+b", '+', 1),
596 ("a$b", '$', 1),
597 ] {
598 match Segment::parse(bad).unwrap_err() {
599 SegmentError::IllegalChar {
600 ch: got_ch,
601 index: got_index,
602 ..
603 } => {
604 assert_eq!(got_ch, ch, "wrong char for {bad:?}");
605 assert_eq!(got_index, index, "wrong index for {bad:?}");
606 }
607 other => panic!("expected IllegalChar for {bad:?}, got {other:?}"),
608 }
609 }
610 }
611
612 #[test]
613 fn segment_rejects_over_the_length_maximum() {
614 let ok = "a".repeat(SEGMENT_MAX_LEN);
615 assert!(Segment::parse(&ok).is_ok());
616 let too_long = "a".repeat(SEGMENT_MAX_LEN + 1);
617 match Segment::parse(&too_long).unwrap_err() {
618 SegmentError::TooLong { len, .. } => assert_eq!(len, SEGMENT_MAX_LEN + 1),
619 other => panic!("expected TooLong, got {other:?}"),
620 }
621 }
622
623 #[test]
624 fn segment_accepts_the_documented_charset() {
625 for good in ["a", "Z", "0", "helm-charts", "build_2", "AkeyLess", "x-_-x"] {
626 assert_eq!(seg(good).as_str(), good);
627 }
628 }
629
630 #[test]
631 fn segment_is_case_sensitive_and_never_normalises() {
632 assert_ne!(seg("Build"), seg("build"));
633 assert_eq!(seg("Build").as_str(), "Build");
634 }
635
636 #[test]
637 fn segment_display_round_trips_through_from_str() {
638 let s = seg("helm-charts");
639 let back: Segment = s.to_string().parse().unwrap();
640 assert_eq!(s, back);
641 }
642
643 #[test]
644 fn segment_serde_is_a_plain_string_and_validates_on_the_way_in() {
645 let s = seg("build");
646 assert_eq!(serde_json::to_string(&s).unwrap(), "\"build\"");
647 let back: Segment = serde_json::from_str("\"build\"").unwrap();
648 assert_eq!(s, back);
649 assert!(serde_json::from_str::<Segment>("\"*\"").is_err());
652 assert!(serde_json::from_str::<Segment>("\"a.b\"").is_err());
653 }
654
655 #[test]
658 fn address_parses_and_displays_dot_joined() {
659 let a = addr("work.akeyless.helm-charts.build");
660 assert_eq!(a.to_string(), "work.akeyless.helm-charts.build");
661 assert_eq!(a.depth().get(), 4);
662 assert_eq!(a.leaf().as_str(), "build");
663 }
664
665 #[test]
666 fn address_rejects_the_empty_string() {
667 assert_eq!(Address::parse("").unwrap_err(), AddressError::Empty);
668 }
669
670 #[test]
671 fn address_reports_the_failing_segment_and_its_index() {
672 match Address::parse("work..build").unwrap_err() {
673 AddressError::Segment { index, source } => {
674 assert_eq!(index, 1);
675 assert_eq!(source, SegmentError::Empty);
676 }
677 other => panic!("expected Segment error, got {other:?}"),
678 }
679 match Address::parse("work.a b.c").unwrap_err() {
680 AddressError::Segment { index, source } => {
681 assert_eq!(index, 1);
682 assert!(matches!(source, SegmentError::ContainsWhitespace(_)));
683 }
684 other => panic!("expected Segment error, got {other:?}"),
685 }
686 match Address::parse("work.*.build").unwrap_err() {
688 AddressError::Segment { index, source } => {
689 assert_eq!(index, 1);
690 assert!(matches!(source, SegmentError::WildcardToken(_)));
691 }
692 other => panic!("expected Segment error, got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn address_from_segments_rejects_an_empty_sequence() {
698 assert_eq!(
699 Address::from_segments(Vec::new()).unwrap_err(),
700 AddressError::Empty
701 );
702 let a = Address::from_segments(vec![seg("work"), seg("build")]).unwrap();
703 assert_eq!(a.to_string(), "work.build");
704 }
705
706 #[test]
707 fn address_parent_child_and_depth_compose() {
708 let a = addr("work.akeyless");
709 let child = a.child(seg("build"));
710 assert_eq!(child.to_string(), "work.akeyless.build");
711 assert_eq!(child.depth().get(), 3);
712 assert_eq!(child.parent().unwrap(), a);
713 assert_eq!(a.parent().unwrap(), Address::root(seg("work")));
714 }
715
716 #[test]
717 fn address_root_has_no_parent() {
718 let root = Address::root(seg("work"));
719 assert_eq!(root.depth().get(), 1);
720 assert!(root.parent().is_none());
721 }
722
723 #[test]
724 fn address_starts_with_is_segment_wise_not_string_wise() {
725 let a = addr("work.helm-charts.build");
726 assert!(a.starts_with(&addr("work")));
727 assert!(a.starts_with(&addr("work.helm-charts")));
728 assert!(a.starts_with(&a));
729 assert!(!a.starts_with(&addr("work.helm")));
731 assert!(!addr("work").starts_with(&a));
732 }
733
734 #[test]
735 fn address_serde_is_a_plain_string_and_validates_on_the_way_in() {
736 let a = addr("work.build");
737 assert_eq!(serde_json::to_string(&a).unwrap(), "\"work.build\"");
738 let back: Address = serde_json::from_str("\"work.build\"").unwrap();
739 assert_eq!(a, back);
740 assert!(serde_json::from_str::<Address>("\"\"").is_err());
741 assert!(serde_json::from_str::<Address>("\"work..build\"").is_err());
742 }
743
744 #[test]
747 fn pattern_rejects_empty() {
748 assert_eq!(Pattern::parse("").unwrap_err(), PatternError::Empty);
749 }
750
751 #[test]
752 fn pattern_tail_wildcard_must_be_final() {
753 assert_eq!(
754 Pattern::parse("work.>.build").unwrap_err(),
755 PatternError::TailNotFinal { index: 1 }
756 );
757 assert_eq!(
758 Pattern::parse(">.work").unwrap_err(),
759 PatternError::TailNotFinal { index: 0 }
760 );
761 assert_eq!(
762 Pattern::parse("a.>.>").unwrap_err(),
763 PatternError::TailNotFinal { index: 1 }
764 );
765 assert!(Pattern::parse("work.>").is_ok());
767 assert!(Pattern::parse(">").is_ok());
768 }
769
770 #[test]
771 fn pattern_reports_the_failing_token_and_its_index() {
772 match Pattern::parse("work.a b.>").unwrap_err() {
773 PatternError::Token { index, source } => {
774 assert_eq!(index, 1);
775 assert!(matches!(source, SegmentError::ContainsWhitespace(_)));
776 }
777 other => panic!("expected Token error, got {other:?}"),
778 }
779 match Pattern::parse("work..build").unwrap_err() {
780 PatternError::Token { index, source } => {
781 assert_eq!(index, 1);
782 assert_eq!(source, SegmentError::Empty);
783 }
784 other => panic!("expected Token error, got {other:?}"),
785 }
786 }
787
788 #[test]
789 fn pattern_literal_matches_itself_and_nothing_else() {
790 let p = pat("work.build");
791 assert!(p.matches(&addr("work.build")));
792 assert!(!p.matches(&addr("work.Build")));
793 assert!(!p.matches(&addr("work")));
794 assert!(!p.matches(&addr("work.build.extra")));
795 assert!(!p.matches(&addr("other.build")));
796 }
797
798 #[test]
799 fn pattern_star_matches_exactly_one_segment() {
800 let p = pat("work.*.build");
801 assert!(p.matches(&addr("work.akeyless.build")));
802 assert!(p.matches(&addr("work.x.build")));
803 assert!(!p.matches(&addr("work.build")));
805 assert!(!p.matches(&addr("work.a.b.build")));
807
808 let trailing = pat("work.*");
809 assert!(trailing.matches(&addr("work.build")));
810 assert!(!trailing.matches(&addr("work")));
811 assert!(!trailing.matches(&addr("work.build.deep")));
812 }
813
814 #[test]
815 fn pattern_tail_matches_one_or_more_but_never_zero() {
816 let p = pat("work.>");
817 assert!(p.matches(&addr("work.build")));
818 assert!(p.matches(&addr("work.akeyless.helm-charts.build")));
819 assert!(!p.matches(&addr("work")));
821 assert!(!p.matches(&addr("other.build")));
822
823 let everything = pat(">");
824 assert!(everything.matches(&addr("work")));
825 assert!(everything.matches(&addr("work.a.b.c")));
826 }
827
828 #[test]
829 fn pattern_mixes_literals_stars_and_a_tail() {
830 let p = pat("work.*.helm-charts.>");
831 assert!(p.matches(&addr("work.akeyless.helm-charts.build")));
832 assert!(p.matches(&addr("work.pleme.helm-charts.a.b")));
833 assert!(!p.matches(&addr("work.akeyless.helm-charts")));
834 assert!(!p.matches(&addr("work.akeyless.other.build")));
835 assert!(!p.matches(&addr("work.helm-charts.build")));
836 }
837
838 #[test]
839 fn pattern_display_round_trips_through_from_str() {
840 for src in ["work.build", "work.*.build", "work.>", ">", "*"] {
841 let p = pat(src);
842 assert_eq!(p.to_string(), src);
843 let back: Pattern = p.to_string().parse().unwrap();
844 assert_eq!(p, back);
845 }
846 }
847
848 #[test]
849 fn pattern_exact_matches_only_its_own_address() {
850 let a = addr("work.akeyless.build");
851 let p = Pattern::exact(&a);
852 assert_eq!(p.to_string(), a.to_string());
853 assert!(p.matches(&a));
854 assert!(!p.matches(&addr("work.akeyless")));
855 assert!(!p.matches(&addr("work.akeyless.build.x")));
856 assert_eq!(p.tokens().len(), 3);
857 }
858
859 #[test]
860 fn pattern_serde_is_a_plain_string_and_validates_on_the_way_in() {
861 let p = pat("work.*.>");
862 assert_eq!(serde_json::to_string(&p).unwrap(), "\"work.*.>\"");
863 let back: Pattern = serde_json::from_str("\"work.*.>\"").unwrap();
864 assert_eq!(p, back);
865 assert!(serde_json::from_str::<Pattern>("\"work.>.build\"").is_err());
866 }
867
868 #[test]
875 fn the_only_way_in_is_a_fallible_parse() {
876 let src = include_str!("address.rs");
877 let code: String = src
878 .lines()
879 .map(str::trim_start)
880 .filter(|l| !l.starts_with("//"))
881 .collect::<Vec<_>>()
882 .join("\n");
883 let code = code.split("mod tests").next().unwrap_or(&code);
884
885 for decl in [
888 "pub struct Segment(String);",
889 "pub struct Address(Vec<Segment>);",
890 "pub struct Pattern(Vec<PatternToken>);",
891 ] {
892 assert!(code.contains(decl), "inner state must stay private: {decl}");
893 }
894 assert!(
895 !code.contains("pub fn new("),
896 "an infallible `new` would be a second, unchecked way in"
897 );
898 assert!(
899 !code.contains("impl From<String> for"),
900 "an infallible `From<String>` would bypass parsing"
901 );
902
903 assert!(code.contains("pub fn parse(s: &str) -> Result<Self, SegmentError>"));
905 assert!(code.contains("try_from = \"String\""));
906 }
907}