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 hosts
50 .iter()
51 .any(|host| after_ascii_prefix(&text, host).is_some())
52 })
53}
54
55pub fn hugging_face_repo(raw: &str) -> Option<String> {
59 let text = cleaned(raw)?;
60 let text = stripped(&text, &HUGGING_FACE_HOSTS);
61 if text.contains("://") || text.contains(':') {
62 return None;
63 }
64 let components: Vec<&str> = text.split('/').collect();
65 if components.len() < 2 {
66 return None;
67 }
68 let org = components[0];
69 let name = components[1];
70 if org.is_empty()
71 || name.is_empty()
72 || HUGGING_FACE_RESERVED_ROOTS.contains(&org.to_lowercase().as_str())
73 {
74 return None;
75 }
76 if components.len() > 2 {
77 let raw_lower = raw.to_lowercase();
78 let from_hf_host = raw_lower.contains("hf.co") || raw_lower.contains("huggingface.co");
79 let next = components[2];
80 let next_ok =
81 next.is_empty() || HUGGING_FACE_SUBPATHS.contains(&next.to_lowercase().as_str());
82 if !(from_hf_host && next_ok) {
83 return None;
84 }
85 }
86 Some(format!("{org}/{name}"))
87}
88
89pub fn ollama_tag(raw: &str) -> Option<String> {
92 tag(raw, true)
93}
94
95pub fn ollama_install_tag(raw: &str) -> Option<String> {
98 tag(raw, false)
99}
100
101pub fn ollama_direct_tag(query: &str) -> Option<String> {
105 if hugging_face_repo(query).is_some() {
106 return None;
107 }
108 let tag = ollama_tag(query)?;
109 if query.contains(':') || is_ollama_link(query) {
110 Some(tag)
111 } else {
112 None
113 }
114}
115
116fn tag(raw: &str, require_explicit_tag_for_namespaced: bool) -> Option<String> {
117 let text = cleaned(raw)?;
118 let is_link = OLLAMA_HOSTS
119 .iter()
120 .any(|host| after_ascii_prefix(&text, host).is_some());
121 let mut text = stripped(&text, &OLLAMA_HOSTS);
122 if text.contains("://") {
123 return None;
124 }
125 if is_link {
126 let components: Vec<&str> = text.split('/').collect();
127 let selected: Vec<&str> = if components
128 .first()
129 .is_some_and(|first| first.eq_ignore_ascii_case("library"))
130 {
131 components.iter().skip(1).take(1).copied().collect()
132 } else {
133 components.iter().take(2).copied().collect()
134 };
135 if selected.is_empty() {
136 return None;
137 }
138 let joined = selected.join("/");
139 return shaped(&joined, false).then_some(joined);
140 }
141 if let Some(rest) = after_ascii_prefix(&text, "library/") {
142 text = rest.to_owned();
143 }
144 shaped(&text, require_explicit_tag_for_namespaced).then_some(text)
145}
146
147pub fn normalized_tag(reference: &str) -> String {
149 let with_tag = if reference.contains(':') {
150 reference.to_owned()
151 } else {
152 format!("{reference}:latest")
153 };
154 with_tag.to_lowercase()
155}
156
157pub fn normalized(provider: &InstallProviderId, reference: &str) -> String {
160 if provider.as_str() == "ollama" {
161 normalized_tag(reference)
162 } else {
163 reference.to_lowercase()
164 }
165}
166
167pub fn is_ollama_tag_shaped(reference: &str) -> bool {
171 shaped(reference, true)
172}
173
174fn shaped(reference: &str, require_explicit_tag_for_namespaced: bool) -> bool {
175 if reference.is_empty()
176 || reference.chars().any(char::is_whitespace)
177 || reference.contains("://")
178 {
179 return false;
180 }
181 let components: Vec<&str> = reference.split('/').collect();
182 if components.len() > 2 || components.iter().any(|component| component.is_empty()) {
183 return false;
184 }
185 let Some(name) = components.last() else {
186 return false;
187 };
188 let name_parts: Vec<&str> = name.split(':').collect();
189 if name_parts.len() > 2 || name_parts.iter().any(|part| part.is_empty()) {
190 return false;
191 }
192 if components.len() == 2 {
193 return !require_explicit_tag_for_namespaced || name.contains(':');
194 }
195 true
196}
197
198fn cleaned(raw: &str) -> Option<String> {
202 let mut text = raw.trim().to_owned();
203 if text.is_empty() || text.chars().any(char::is_whitespace) {
204 return None;
205 }
206 for scheme in ["https://", "http://"] {
209 if let Some(rest) = after_ascii_prefix(&text, scheme) {
210 text = rest.to_owned();
211 }
212 }
213 if let Some(stop) = text.find(['?', '#']) {
214 text.truncate(stop);
215 }
216 while text.ends_with('/') {
217 text.pop();
218 }
219 (!text.is_empty()).then_some(text)
220}
221
222fn stripped(text: &str, hosts: &[&str]) -> String {
224 hosts
225 .iter()
226 .find_map(|host| after_ascii_prefix(text, host))
227 .unwrap_or(text)
228 .to_owned()
229}
230
231fn after_ascii_prefix<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
235 text.split_at_checked(prefix.len())
236 .filter(|(head, _)| head.eq_ignore_ascii_case(prefix))
237 .map(|(_, rest)| rest)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn a_prefix_is_matched_over_its_own_bytes() {
246 assert_eq!(after_ascii_prefix("HTTPS://a", "https://"), Some("a"));
247 assert_eq!(after_ascii_prefix("https://", "https://"), Some(""));
248 assert_eq!(after_ascii_prefix("http://a", "https://"), None);
249 assert_eq!(after_ascii_prefix("\u{212A}x", "kx"), None);
252 assert_eq!(after_ascii_prefix("\u{212A}", "k"), None);
253 }
254
255 #[test]
256 fn tag_shape_requires_a_version_when_namespaced() {
257 assert!(is_ollama_tag_shaped("llama3"));
258 assert!(is_ollama_tag_shaped("llama3:8b"));
259 assert!(is_ollama_tag_shaped("org/model:tag"));
260 assert!(!is_ollama_tag_shaped("org/model"));
262 assert!(!is_ollama_tag_shaped("a b"));
264 assert!(!is_ollama_tag_shaped("a/b/c"));
265 assert!(!is_ollama_tag_shaped("a:b:c"));
266 assert!(!is_ollama_tag_shaped("model:"));
267 assert!(!is_ollama_tag_shaped(""));
268 }
269
270 #[test]
271 fn cleaned_strips_stacked_schemes() {
272 assert_eq!(cleaned("https://http://foo").as_deref(), Some("foo"));
274 }
275}