kernel/install/
reference.rs1use crate::install::provider::InstallProviderId;
7
8const HUGGING_FACE_HOSTS: [&str; 3] = ["huggingface.co/", "www.huggingface.co/", "hf.co/"];
9const OLLAMA_HOSTS: [&str; 3] = ["ollama.com/", "www.ollama.com/", "registry.ollama.ai/"];
10const HUGGING_FACE_SUBPATHS: [&str; 8] = [
11 "tree",
12 "blob",
13 "resolve",
14 "commit",
15 "commits",
16 "discussions",
17 "blame",
18 "raw",
19];
20const HUGGING_FACE_RESERVED_ROOTS: [&str; 14] = [
21 "datasets",
22 "spaces",
23 "collections",
24 "models",
25 "blog",
26 "docs",
27 "papers",
28 "tasks",
29 "posts",
30 "pricing",
31 "settings",
32 "organizations",
33 "learn",
34 "chat",
35];
36
37pub fn is_hugging_face_link(raw: &str) -> bool {
39 matches_host(raw, &HUGGING_FACE_HOSTS)
40}
41
42pub fn is_ollama_link(raw: &str) -> bool {
44 matches_host(raw, &OLLAMA_HOSTS)
45}
46
47fn matches_host(raw: &str, hosts: &[&str]) -> bool {
48 cleaned(raw).is_some_and(|text| {
49 let lower = text.to_lowercase();
50 hosts.iter().any(|host| lower.starts_with(host))
51 })
52}
53
54pub fn hugging_face_repo(raw: &str) -> Option<String> {
58 let text = cleaned(raw)?;
59 let text = stripped(&text, &HUGGING_FACE_HOSTS);
60 if text.contains("://") || text.contains(':') {
61 return None;
62 }
63 let components: Vec<&str> = text.split('/').collect();
64 if components.len() < 2 {
65 return None;
66 }
67 let org = components[0];
68 let name = components[1];
69 if org.is_empty()
70 || name.is_empty()
71 || HUGGING_FACE_RESERVED_ROOTS.contains(&org.to_lowercase().as_str())
72 {
73 return None;
74 }
75 if components.len() > 2 {
76 let raw_lower = raw.to_lowercase();
77 let from_hf_host = raw_lower.contains("hf.co") || raw_lower.contains("huggingface.co");
78 let next = components[2];
79 let next_ok =
80 next.is_empty() || HUGGING_FACE_SUBPATHS.contains(&next.to_lowercase().as_str());
81 if !(from_hf_host && next_ok) {
82 return None;
83 }
84 }
85 Some(format!("{org}/{name}"))
86}
87
88pub fn ollama_tag(raw: &str) -> Option<String> {
91 tag(raw, true)
92}
93
94pub fn ollama_install_tag(raw: &str) -> Option<String> {
97 tag(raw, false)
98}
99
100pub fn ollama_direct_tag(query: &str) -> Option<String> {
104 if hugging_face_repo(query).is_some() {
105 return None;
106 }
107 let tag = ollama_tag(query)?;
108 if query.contains(':') || is_ollama_link(query) {
109 Some(tag)
110 } else {
111 None
112 }
113}
114
115fn tag(raw: &str, require_explicit_tag_for_namespaced: bool) -> Option<String> {
116 let text = cleaned(raw)?;
117 let lower = text.to_lowercase();
118 let is_link = OLLAMA_HOSTS.iter().any(|host| lower.starts_with(host));
119 let mut text = stripped(&text, &OLLAMA_HOSTS);
120 if text.contains("://") {
121 return None;
122 }
123 if is_link {
124 let components: Vec<&str> = text.split('/').collect();
125 let selected: Vec<&str> = if components
126 .first()
127 .is_some_and(|first| first.eq_ignore_ascii_case("library"))
128 {
129 components.iter().skip(1).take(1).copied().collect()
130 } else {
131 components.iter().take(2).copied().collect()
132 };
133 if selected.is_empty() {
134 return None;
135 }
136 let joined = selected.join("/");
137 return shaped(&joined, false).then_some(joined);
138 }
139 if lower.starts_with("library/") {
140 text = text["library/".len()..].to_string();
141 }
142 shaped(&text, require_explicit_tag_for_namespaced).then_some(text)
143}
144
145pub fn normalized_tag(reference: &str) -> String {
147 let with_tag = if reference.contains(':') {
148 reference.to_owned()
149 } else {
150 format!("{reference}:latest")
151 };
152 with_tag.to_lowercase()
153}
154
155pub fn normalized(provider: &InstallProviderId, reference: &str) -> String {
158 if provider.as_str() == "ollama" {
159 normalized_tag(reference)
160 } else {
161 reference.to_lowercase()
162 }
163}
164
165pub fn is_ollama_tag_shaped(reference: &str) -> bool {
169 shaped(reference, true)
170}
171
172fn shaped(reference: &str, require_explicit_tag_for_namespaced: bool) -> bool {
173 if reference.is_empty()
174 || reference.chars().any(char::is_whitespace)
175 || reference.contains("://")
176 {
177 return false;
178 }
179 let components: Vec<&str> = reference.split('/').collect();
180 if components.len() > 2 || components.iter().any(|component| component.is_empty()) {
181 return false;
182 }
183 let Some(name) = components.last() else {
184 return false;
185 };
186 let name_parts: Vec<&str> = name.split(':').collect();
187 if name_parts.len() > 2 || name_parts.iter().any(|part| part.is_empty()) {
188 return false;
189 }
190 if components.len() == 2 {
191 return !require_explicit_tag_for_namespaced || name.contains(':');
192 }
193 true
194}
195
196fn cleaned(raw: &str) -> Option<String> {
200 let mut text = raw.trim().to_owned();
201 if text.is_empty() || text.chars().any(char::is_whitespace) {
202 return None;
203 }
204 for scheme in ["https://", "http://"] {
207 if text.to_lowercase().starts_with(scheme) {
208 text = text[scheme.len()..].to_owned();
209 }
210 }
211 if let Some(stop) = text.find(['?', '#']) {
212 text.truncate(stop);
213 }
214 while text.ends_with('/') {
215 text.pop();
216 }
217 (!text.is_empty()).then_some(text)
218}
219
220fn stripped(text: &str, hosts: &[&str]) -> String {
222 let lower = text.to_lowercase();
223 for host in hosts {
224 if lower.starts_with(host) {
225 return text[host.len()..].to_owned();
226 }
227 }
228 text.to_owned()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn tag_shape_requires_a_version_when_namespaced() {
237 assert!(is_ollama_tag_shaped("llama3"));
238 assert!(is_ollama_tag_shaped("llama3:8b"));
239 assert!(is_ollama_tag_shaped("org/model:tag"));
240 assert!(!is_ollama_tag_shaped("org/model"));
242 assert!(!is_ollama_tag_shaped("a b"));
244 assert!(!is_ollama_tag_shaped("a/b/c"));
245 assert!(!is_ollama_tag_shaped("a:b:c"));
246 assert!(!is_ollama_tag_shaped("model:"));
247 assert!(!is_ollama_tag_shaped(""));
248 }
249
250 #[test]
251 fn cleaned_strips_stacked_schemes() {
252 assert_eq!(cleaned("https://http://foo").as_deref(), Some("foo"));
254 }
255}