1use crate::{Certainty, UpstreamDatum, UpstreamDatumWithMetadata};
2use crate::{ProviderError, UpstreamMetadata};
3use log::warn;
4
5const DEFAULT_ITERATION_LIMIT: usize = 10;
6
7type ExtrapolationCallback = fn(
8 UpstreamMetadata,
9 bool,
10) -> std::pin::Pin<
11 Box<
12 dyn std::future::Future<Output = Result<Vec<UpstreamDatumWithMetadata>, ProviderError>>
13 + Send,
14 >,
15>;
16
17struct Extrapolation {
18 from_fields: &'static [&'static str],
19 to_fields: &'static [&'static str],
20 cb: ExtrapolationCallback,
21}
22
23async fn extrapolate_repository_from_homepage(
24 upstream_metadata: &UpstreamMetadata,
25 net_access: bool,
26) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
27 let mut ret = vec![];
28
29 let homepage = upstream_metadata.get("Homepage").unwrap();
30
31 let url = match homepage.datum.to_url() {
32 Some(url) => url,
33 None => {
34 return {
35 warn!("Homepage field is not a URL");
36 Ok(vec![])
37 }
38 }
39 };
40
41 if let Some(repo) = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await {
42 ret.push(UpstreamDatumWithMetadata {
43 datum: UpstreamDatum::Repository(repo),
44 certainty: Some(
45 std::cmp::min(homepage.certainty, Some(Certainty::Likely))
46 .unwrap_or(Certainty::Likely),
47 ),
48 origin: homepage.origin.clone(),
49 });
50 }
51 Ok(ret)
52}
53
54async fn extrapolate_homepage_from_repository_browse(
55 upstream_metadata: &UpstreamMetadata,
56 net_access: bool,
57) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
58 let mut ret = vec![];
59 let browse_url = upstream_metadata.get("Repository-Browse").unwrap();
60
61 let url = match browse_url.datum.to_url() {
62 Some(url) => url,
63 None => {
64 return {
65 warn!("Repository-Browse field is not a URL");
66 Ok(vec![])
67 }
68 }
69 };
70
71 let forge = crate::find_forge(&url, Some(net_access)).await;
75 if forge.is_some() && forge.unwrap().repository_browse_can_be_homepage() {
76 ret.push(UpstreamDatumWithMetadata {
77 datum: UpstreamDatum::Homepage(browse_url.datum.as_str().unwrap().to_string()),
78 certainty: Some(
79 std::cmp::min(browse_url.certainty, Some(Certainty::Possible))
80 .unwrap_or(Certainty::Possible),
81 ),
82 origin: browse_url.origin.clone(),
83 });
84 }
85 Ok(ret)
86}
87
88async fn copy_bug_db_field(
89 upstream_metadata: &UpstreamMetadata,
90 _net_access: bool,
91) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
92 let mut ret = vec![];
93
94 let old_bug_db = upstream_metadata.get("Bugs-Database").unwrap();
95
96 ret.push(UpstreamDatumWithMetadata {
97 datum: UpstreamDatum::BugDatabase(old_bug_db.datum.as_str().unwrap().to_string()),
98 certainty: old_bug_db.certainty,
99 origin: old_bug_db.origin.clone(),
100 });
101
102 Ok(ret)
103}
104
105async fn extrapolate_repository_from_bug_db(
106 upstream_metadata: &UpstreamMetadata,
107 net_access: bool,
108) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
109 let old_value = upstream_metadata.get("Bug-Database").unwrap();
110 let url = match old_value.datum.to_url() {
111 Some(url) => url,
112 None => {
113 return {
114 warn!("Bug-Database field is not a URL");
115 Ok(vec![])
116 }
117 }
118 };
119 let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
120
121 Ok(if let Some(repo) = repo {
122 vec![UpstreamDatumWithMetadata {
123 datum: UpstreamDatum::Repository(repo),
124 certainty: Some(
125 std::cmp::min(old_value.certainty, Some(Certainty::Likely))
126 .unwrap_or(Certainty::Likely),
127 ),
128 origin: old_value.origin.clone(),
129 }]
130 } else {
131 vec![]
132 })
133}
134
135async fn extrapolate_repository_browse_from_repository(
136 upstream_metadata: &UpstreamMetadata,
137 net_access: bool,
138) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
139 let old_value = upstream_metadata.get("Repository").unwrap();
140 let url = match old_value.datum.to_url() {
141 Some(url) => url,
142 None => {
143 return {
144 warn!("Repository field is not a URL");
145 Ok(vec![])
146 }
147 }
148 };
149 let browse_url = crate::vcs::browse_url_from_repo_url(
150 &crate::vcs::VcsLocation {
151 url,
152 branch: None,
153 subpath: None,
154 },
155 Some(net_access),
156 )
157 .await;
158 Ok(if let Some(browse_url) = browse_url {
159 vec![UpstreamDatumWithMetadata {
160 datum: UpstreamDatum::RepositoryBrowse(browse_url.to_string()),
161 certainty: old_value.certainty,
162 origin: old_value.origin.clone(),
163 }]
164 } else {
165 vec![]
166 })
167}
168
169async fn extrapolate_repository_from_repository_browse(
170 upstream_metadata: &UpstreamMetadata,
171 net_access: bool,
172) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
173 let old_value = upstream_metadata.get("Repository-Browse").unwrap();
174 let url = match old_value.datum.to_url() {
175 Some(url) => url,
176 None => {
177 return {
178 warn!("Repository-Browse field is not a URL");
179 Ok(vec![])
180 }
181 }
182 };
183 let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
184 Ok(if let Some(repo) = repo {
185 vec![UpstreamDatumWithMetadata {
186 datum: UpstreamDatum::Repository(repo),
187 certainty: old_value.certainty,
188 origin: old_value.origin.clone(),
189 }]
190 } else {
191 vec![]
192 })
193}
194
195async fn extrapolate_bug_database_from_repository(
196 upstream_metadata: &UpstreamMetadata,
197 net_access: bool,
198) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
199 let old_value = upstream_metadata.get("Repository").unwrap();
200
201 let url = match old_value.datum.to_url() {
202 Some(url) => url,
203 None => {
204 return {
205 warn!("Repository field is not a URL");
206 Ok(vec![])
207 }
208 }
209 };
210
211 Ok(
212 if let Some(bug_db_url) =
213 crate::guess_bug_database_url_from_repo_url(&url, Some(net_access)).await
214 {
215 vec![UpstreamDatumWithMetadata {
216 datum: UpstreamDatum::BugDatabase(bug_db_url.to_string()),
217 certainty: Some(
218 std::cmp::min(old_value.certainty, Some(Certainty::Likely))
219 .unwrap_or(Certainty::Likely),
220 ),
221 origin: old_value.origin.clone(),
222 }]
223 } else {
224 vec![]
225 },
226 )
227}
228
229async fn extrapolate_bug_submit_from_bug_db(
230 upstream_metadata: &UpstreamMetadata,
231 net_access: bool,
232) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
233 let old_value = upstream_metadata.get("Bug-Database").unwrap();
234
235 let url = match old_value.datum.to_url() {
236 Some(url) => url,
237 None => {
238 return {
239 warn!("Bug-Database field is not a URL");
240 Ok(vec![])
241 }
242 }
243 };
244
245 let bug_submit_url = crate::bug_submit_url_from_bug_database_url(&url, Some(net_access)).await;
246
247 Ok(if let Some(bug_submit_url) = bug_submit_url {
248 vec![UpstreamDatumWithMetadata {
249 datum: UpstreamDatum::BugSubmit(bug_submit_url.to_string()),
250 certainty: old_value.certainty,
251 origin: old_value.origin.clone(),
252 }]
253 } else {
254 vec![]
255 })
256}
257
258async fn extrapolate_bug_db_from_bug_submit(
259 upstream_metadata: &UpstreamMetadata,
260 net_access: bool,
261) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
262 let old_value = upstream_metadata.get("Bug-Submit").unwrap();
263
264 let old_value_url = match old_value.datum.to_url() {
265 Some(url) => url,
266 None => return Ok(vec![]),
267 };
268
269 let bug_db_url =
270 crate::bug_database_url_from_bug_submit_url(&old_value_url, Some(net_access)).await;
271
272 Ok(if let Some(bug_db_url) = bug_db_url {
273 vec![UpstreamDatumWithMetadata {
274 datum: UpstreamDatum::BugDatabase(bug_db_url.to_string()),
275 certainty: old_value.certainty,
276 origin: old_value.origin.clone(),
277 }]
278 } else {
279 vec![]
280 })
281}
282
283async fn extrapolate_repository_from_download(
284 upstream_metadata: &UpstreamMetadata,
285 net_access: bool,
286) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
287 let old_value = upstream_metadata.get("Download").unwrap();
288
289 let url = match old_value.datum.to_url() {
290 Some(url) => url,
291 None => {
292 return {
293 warn!("Download field is not a URL");
294 Ok(vec![])
295 }
296 }
297 };
298
299 let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
300 Ok(if let Some(repo) = repo {
301 vec![UpstreamDatumWithMetadata {
302 datum: UpstreamDatum::Repository(repo),
303 certainty: Some(
304 std::cmp::min(old_value.certainty, Some(Certainty::Likely))
305 .unwrap_or(Certainty::Likely),
306 ),
307 origin: old_value.origin.clone(),
308 }]
309 } else {
310 vec![]
311 })
312}
313
314async fn extrapolate_name_from_repository(
315 upstream_metadata: &UpstreamMetadata,
316 net_access: bool,
317) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
318 let mut ret = vec![];
319 let old_value = upstream_metadata.get("Repository").unwrap();
320 let url = match old_value.datum.to_url() {
321 Some(url) => url,
322 None => {
323 return {
324 warn!("Repository field is not a URL");
325 Ok(vec![])
326 }
327 }
328 };
329 let repo = crate::vcs::guess_repo_from_url(&url, Some(net_access)).await;
330 if let Some(repo) = repo {
331 let parsed: url::Url = repo.parse().unwrap();
332 let name = parsed.path_segments().unwrap().next_back().unwrap();
333 let name = name.strip_suffix(".git").unwrap_or(name);
334 if !name.is_empty() {
335 ret.push(UpstreamDatumWithMetadata {
336 datum: UpstreamDatum::Name(name.to_string()),
337 certainty: Some(
338 std::cmp::min(old_value.certainty, Some(Certainty::Likely))
339 .unwrap_or(Certainty::Likely),
340 ),
341 origin: old_value.origin.clone(),
342 });
343 }
344 }
345 Ok(ret)
346}
347
348async fn extrapolate_security_contact_from_security_md(
349 upstream_metadata: &UpstreamMetadata,
350 net_access: bool,
351) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
352 let repository_url = upstream_metadata.get("Repository").unwrap();
353 let security_md_path = upstream_metadata.get("Security-MD").unwrap();
354
355 let url = match repository_url.datum.to_url() {
356 Some(url) => url,
357 None => {
358 return {
359 warn!("Repository field is not a URL");
360 Ok(vec![])
361 }
362 }
363 };
364
365 let security_url = crate::vcs::browse_url_from_repo_url(
366 &crate::vcs::VcsLocation {
367 url,
368 branch: None,
369 subpath: security_md_path.datum.as_str().map(|x| x.to_string()),
370 },
371 Some(net_access),
372 )
373 .await;
374
375 Ok(if let Some(security_url) = security_url {
376 vec![UpstreamDatumWithMetadata {
377 datum: UpstreamDatum::SecurityContact(security_url.to_string()),
378 certainty: std::cmp::min(repository_url.certainty, security_md_path.certainty),
379 origin: repository_url.origin.clone(),
380 }]
381 } else {
382 vec![]
383 })
384}
385
386async fn extrapolate_contact_from_maintainer(
387 upstream_metadata: &UpstreamMetadata,
388 _net_access: bool,
389) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
390 let maintainer = upstream_metadata.get("Maintainer").unwrap();
391
392 Ok(vec![UpstreamDatumWithMetadata {
393 datum: UpstreamDatum::Contact(maintainer.datum.as_person().unwrap().to_string()),
394 certainty: maintainer.certainty,
395 origin: maintainer.origin.clone(),
396 }])
397}
398
399async fn consult_homepage(
400 upstream_metadata: &UpstreamMetadata,
401 net_access: bool,
402) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
403 if !net_access {
404 return Ok(vec![]);
405 }
406 let homepage = upstream_metadata.get("Homepage").unwrap();
407
408 let url = match homepage.datum.to_url() {
409 Some(url) => url,
410 None => {
411 return {
412 warn!("Homepage field is not a URL");
413 Ok(vec![])
414 }
415 }
416 };
417
418 let mut ret = vec![];
419
420 for mut entry in crate::homepage::guess_from_homepage(&url).await? {
421 entry.certainty = std::cmp::min(homepage.certainty, entry.certainty);
422 ret.push(entry);
423 }
424 Ok(ret)
425}
426
427const EXTRAPOLATIONS: &[Extrapolation] = &[
428 Extrapolation {
429 from_fields: &["Homepage"],
430 to_fields: &["Repository"],
431 cb: |us, na| Box::pin(async move { extrapolate_repository_from_homepage(&us, na).await }),
432 },
433 Extrapolation {
434 from_fields: &["Repository-Browse"],
435 to_fields: &["Homepage"],
436 cb: |us, na| {
437 Box::pin(async move { extrapolate_homepage_from_repository_browse(&us, na).await })
438 },
439 },
440 Extrapolation {
441 from_fields: &["Bugs-Database"],
442 to_fields: &["Bug-Database"],
443 cb: |us, na| Box::pin(async move { copy_bug_db_field(&us, na).await }),
444 },
445 Extrapolation {
446 from_fields: &["Bug-Database"],
447 to_fields: &["Repository"],
448 cb: |us, na| Box::pin(async move { extrapolate_repository_from_bug_db(&us, na).await }),
449 },
450 Extrapolation {
451 from_fields: &["Repository"],
452 to_fields: &["Repository-Browse"],
453 cb: |us, na| {
454 Box::pin(async move { extrapolate_repository_browse_from_repository(&us, na).await })
455 },
456 },
457 Extrapolation {
458 from_fields: &["Repository-Browse"],
459 to_fields: &["Repository"],
460 cb: |us, na| {
461 Box::pin(async move { extrapolate_repository_from_repository_browse(&us, na).await })
462 },
463 },
464 Extrapolation {
465 from_fields: &["Repository"],
466 to_fields: &["Bug-Database"],
467 cb: |us, na| {
468 Box::pin(async move { extrapolate_bug_database_from_repository(&us, na).await })
469 },
470 },
471 Extrapolation {
472 from_fields: &["Bug-Database"],
473 to_fields: &["Bug-Submit"],
474 cb: |us, na| Box::pin(async move { extrapolate_bug_submit_from_bug_db(&us, na).await }),
475 },
476 Extrapolation {
477 from_fields: &["Bug-Submit"],
478 to_fields: &["Bug-Database"],
479 cb: |us, na| Box::pin(async move { extrapolate_bug_db_from_bug_submit(&us, na).await }),
480 },
481 Extrapolation {
482 from_fields: &["Download"],
483 to_fields: &["Repository"],
484 cb: |us, na| Box::pin(async move { extrapolate_repository_from_download(&us, na).await }),
485 },
486 Extrapolation {
487 from_fields: &["Repository"],
488 to_fields: &["Name"],
489 cb: |us, na| Box::pin(async move { extrapolate_name_from_repository(&us, na).await }),
490 },
491 Extrapolation {
492 from_fields: &["Repository", "Security-MD"],
493 to_fields: &["Security-Contact"],
494 cb: |us, na| {
495 Box::pin(async move { extrapolate_security_contact_from_security_md(&us, na).await })
496 },
497 },
498 Extrapolation {
499 from_fields: &["Maintainer"],
500 to_fields: &["Contact"],
501 cb: |us, na| Box::pin(async move { extrapolate_contact_from_maintainer(&us, na).await }),
502 },
503 Extrapolation {
504 from_fields: &["Homepage"],
505 to_fields: &["Bug-Database", "Repository"],
506 cb: |us, na| Box::pin(async move { consult_homepage(&us, na).await }),
507 },
508];
509
510pub async fn extrapolate_fields(
512 upstream_metadata: &mut UpstreamMetadata,
513 net_access: bool,
514 iteration_limit: Option<usize>,
515) -> Result<(), ProviderError> {
516 let iteration_limit = iteration_limit.unwrap_or(DEFAULT_ITERATION_LIMIT);
517
518 let mut changed = true;
519 let mut iterations = 0;
520
521 while changed {
522 changed = false;
523
524 iterations += 1;
525
526 if iterations > iteration_limit {
527 return Err(ProviderError::ExtrapolationLimitExceeded(iteration_limit));
528 }
529
530 for extrapolation in EXTRAPOLATIONS {
531 let from_fields = extrapolation.from_fields;
532 let to_fields = extrapolation.to_fields;
533 let cb = extrapolation.cb;
534 let from_values = from_fields
535 .iter()
536 .map(|f| upstream_metadata.get(f))
537 .collect::<Vec<_>>();
538 if !from_values.iter().all(|v| v.is_some()) {
539 log::trace!(
540 "Not enough values for extrapolation from {:?} to {:?}",
541 from_fields,
542 to_fields
543 );
544 continue;
545 }
546
547 let from_values = from_values
548 .iter()
549 .map(|v| v.unwrap().clone())
550 .collect::<Vec<_>>();
551
552 let from_certainties = from_fields
553 .iter()
554 .map(|f| upstream_metadata.get(f).unwrap().certainty)
555 .collect::<Vec<_>>();
556
557 let from_certainty = *from_certainties.iter().min().unwrap();
558
559 let old_to_values: std::collections::HashMap<_, _> = to_fields
560 .iter()
561 .filter_map(|f| upstream_metadata.get(f).map(|v| (f, v.clone())))
562 .collect();
563
564 assert!(old_to_values.values().all(|v| v.certainty.is_some()));
565
566 if to_fields.iter().all(|f| {
569 old_to_values
570 .get(f)
571 .map(|v| v.certainty >= from_certainty)
572 .unwrap_or(false)
573 }) {
574 log::trace!(
575 "Not extrapolating from {:?} to {:?} because of certainty ({:?} >= {:?})",
576 from_fields,
577 to_fields,
578 old_to_values
579 .values()
580 .map(|v| v.certainty)
581 .collect::<Vec<_>>(),
582 from_certainty
583 );
584 continue;
585 }
586
587 let extra_upstream_metadata = cb(upstream_metadata.clone(), net_access).await?;
588 let changes = upstream_metadata.update(extra_upstream_metadata.into_iter());
589
590 if !changes.is_empty() {
591 log::debug!(
592 "Extrapolating ({:?} ⇒ {:?}) from ({:?})",
593 old_to_values
594 .iter()
595 .map(|(k, v)| format!("{}: {}", k, v.datum))
596 .collect::<Vec<_>>(),
597 changes
598 .iter()
599 .map(|d| format!("{}: {}", d.datum.field(), d.datum))
600 .collect::<Vec<_>>(),
601 from_values
602 .iter()
603 .map(|v| format!(
604 "{}: {} ({})",
605 v.datum.field(),
606 v.datum,
607 v.certainty
608 .map_or_else(|| "unknown".to_string(), |c| c.to_string())
609 ))
610 .collect::<Vec<_>>()
611 );
612 changed = true;
613 }
614 }
615 }
616
617 Ok(())
618}