1use std::collections::BTreeSet;
4
5pub const MAX_TOOL_NAME: usize = 64;
6
7pub fn sanitize(name: &str) -> String {
9 let s: String = name.chars().map(|c| if c.is_ascii_alphanumeric() || c == '_' || c == '-' { c } else { '_' }).collect();
10 if s.is_empty() { "tool".into() } else { s }
11}
12
13fn fnv1a(s: &str) -> u32 {
15 s.bytes().fold(0x811c_9dc5u32, |h, b| (h ^ u32::from(b)).wrapping_mul(0x0100_0193))
16}
17
18pub fn fit(name: &str) -> String {
21 if name.len() <= MAX_TOOL_NAME {
22 return name.to_string();
23 }
24 let hash = format!("_{:08x}", fnv1a(name));
25 let keep = MAX_TOOL_NAME - hash.len();
26 format!("{}{hash}", &name[..keep])
27}
28
29pub fn namespaced(plugin_id: &str, tool: &str) -> String {
31 fit(&format!("{}__{}", sanitize(plugin_id), sanitize(tool)))
32}
33
34#[derive(Debug, Clone, Default)]
36pub struct ToolNames {
37 taken: BTreeSet<String>,
38}
39
40impl ToolNames {
41 pub fn new<I: IntoIterator<Item = S>, S: Into<String>>(reserved: I) -> Self {
42 Self { taken: reserved.into_iter().map(Into::into).collect() }
43 }
44
45 pub fn reserve(&mut self, name: &str) {
46 self.taken.insert(name.to_string());
47 }
48
49 pub fn is_taken(&self, name: &str) -> bool {
50 self.taken.contains(name)
51 }
52
53 pub fn claim(&mut self, preferred: &str, fallback: &str) -> String {
55 let preferred = fit(&sanitize(preferred));
56 let name = if self.taken.contains(&preferred) {
57 let base = fit(&sanitize(fallback));
58 let mut name = base.clone();
59 let mut i = 2u32;
60 while self.taken.contains(&name) {
61 let suffix = format!("_{i}");
62 let keep = base.len().min(MAX_TOOL_NAME - suffix.len());
63 name = format!("{}{suffix}", &base[..keep]);
64 i = i.saturating_add(1);
65 }
66 name
67 } else {
68 preferred
69 };
70 self.taken.insert(name.clone());
71 name
72 }
73}
74
75pub fn valid(name: &str) -> bool {
77 !name.is_empty() && name.len() <= MAX_TOOL_NAME && name.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'-')
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn names_are_valid_and_unique() {
86 assert_eq!(namespaced("linear", "create.issue"), "linear__create_issue");
87 assert_eq!(sanitize("ünï"), "_n_");
88 assert_eq!(sanitize(""), "tool");
89 let long_a = namespaced("some-plugin", &"a".repeat(80));
90 let long_b = namespaced("some-plugin", &format!("{}b", "a".repeat(79)));
91 assert!(valid(&long_a) && valid(&long_b) && long_a.len() == 64);
92 assert_ne!(long_a, long_b);
93
94 let mut names = ToolNames::new(["read_file"]);
95 assert_eq!(names.claim("count", "p__count"), "count");
96 assert_eq!(names.claim("count", "p__count"), "p__count");
97 assert_eq!(names.claim("count", "p__count"), "p__count_2");
98 assert_eq!(names.claim("read_file", "p__read_file"), "p__read_file");
99 let long = "x".repeat(64);
100 assert_eq!(names.claim(&long, &long), long);
101 let next = names.claim(&long, &long);
102 assert!(valid(&next) && next.ends_with("_2"));
103 }
104}