Skip to main content

upstream_ontologist/
extrapolate.rs

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    // Some hosting sites are commonly used as Homepage
72    // TODO(jelmer): Maybe check that there is a README file that
73    // can serve as index?
74    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
510/// Extrapolates missing fields from existing upstream metadata
511pub 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 any of the to_fields already exist in old_to_values with a better or same
567            // certainty, then we don't need to extrapolate.
568            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}