1#![allow(unexpected_cfgs)]
3#![deny(missing_docs)]
4#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
5
6use futures::stream::StreamExt;
7use futures::Stream;
8use lazy_regex::regex;
9use log::{debug, warn};
10use percent_encoding::utf8_percent_encode;
11#[cfg(feature = "pyo3")]
12use pyo3::{
13 exceptions::{PyRuntimeError, PyTypeError, PyValueError},
14 prelude::*,
15 types::PyDict,
16};
17use reqwest::header::HeaderMap;
18use serde::ser::SerializeSeq;
19use std::cmp::Ordering;
20use std::fs::File;
21use std::io::Read;
22use std::pin::Pin;
23use std::str::FromStr;
24
25use std::path::{Path, PathBuf};
26use url::Url;
27
28static USER_AGENT: &str = concat!("upstream-ontologist/", env!("CARGO_PKG_VERSION"));
29
30pub mod extrapolate;
32pub mod forges;
34pub mod github;
36pub mod homepage;
38pub mod http;
40pub mod providers;
42pub mod readme;
44pub mod repology;
46pub mod vcs;
48pub mod vcs_command;
50
51#[cfg(test)]
52mod upstream_tests {
53 include!(concat!(env!("OUT_DIR"), "/upstream_tests.rs"));
54}
55
56#[cfg(test)]
57mod readme_tests {
58 include!(concat!(env!("OUT_DIR"), "/readme_tests.rs"));
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
62pub enum Certainty {
64 Possible,
66
67 Likely,
69
70 Confident,
72
73 Certain,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub enum Origin {
80 Path(PathBuf),
82
83 Url(url::Url),
85
86 Other(String),
88}
89
90impl std::fmt::Display for Origin {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Origin::Path(path) => write!(f, "{}", path.display()),
94 Origin::Url(url) => write!(f, "{}", url),
95 Origin::Other(s) => write!(f, "{}", s),
96 }
97 }
98}
99
100impl From<&std::path::Path> for Origin {
101 fn from(path: &std::path::Path) -> Self {
102 Origin::Path(path.to_path_buf())
103 }
104}
105
106impl From<std::path::PathBuf> for Origin {
107 fn from(path: std::path::PathBuf) -> Self {
108 Origin::Path(path)
109 }
110}
111
112impl From<url::Url> for Origin {
113 fn from(url: url::Url) -> Self {
114 Origin::Url(url)
115 }
116}
117
118#[cfg(feature = "pyo3")]
119impl<'py> IntoPyObject<'py> for &Origin {
120 type Target = PyAny;
121 type Output = Bound<'py, Self::Target>;
122 type Error = PyErr;
123
124 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
125 match self {
126 Origin::Path(path) => Ok(path.to_str().unwrap().into_pyobject(py)?.into_any()),
127 Origin::Url(url) => Ok(url.to_string().into_pyobject(py)?.into_any()),
128 Origin::Other(s) => Ok(s.into_pyobject(py)?.into_any()),
129 }
130 }
131}
132
133#[cfg(feature = "pyo3")]
134impl<'py> IntoPyObject<'py> for Origin {
135 type Target = PyAny;
136 type Output = Bound<'py, Self::Target>;
137 type Error = PyErr;
138
139 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
140 match self {
141 Origin::Path(path) => Ok(path.to_str().unwrap().into_pyobject(py)?.into_any()),
142 Origin::Url(url) => Ok(url.to_string().into_pyobject(py)?.into_any()),
143 Origin::Other(s) => Ok(s.into_pyobject(py)?.into_any()),
144 }
145 }
146}
147
148#[cfg(feature = "pyo3")]
149impl<'py> FromPyObject<'_, 'py> for Origin {
150 type Error = PyErr;
151
152 fn extract(ob: pyo3::Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
153 if let Ok(path) = ob.extract::<PathBuf>() {
154 Ok(Origin::Path(path))
155 } else if let Ok(s) = ob.extract::<String>() {
156 Ok(Origin::Other(s))
157 } else {
158 Err(PyTypeError::new_err("expected str or Path"))
159 }
160 }
161}
162
163impl FromStr for Certainty {
164 type Err = String;
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 match s {
167 "certain" => Ok(Certainty::Certain),
168 "confident" => Ok(Certainty::Confident),
169 "likely" => Ok(Certainty::Likely),
170 "possible" => Ok(Certainty::Possible),
171 _ => Err(format!("unknown certainty: {}", s)),
172 }
173 }
174}
175
176impl std::fmt::Display for Certainty {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 Certainty::Certain => write!(f, "certain"),
180 Certainty::Confident => write!(f, "confident"),
181 Certainty::Likely => write!(f, "likely"),
182 Certainty::Possible => write!(f, "possible"),
183 }
184 }
185}
186
187#[cfg(feature = "pyo3")]
188impl<'py> FromPyObject<'_, 'py> for Certainty {
189 type Error = PyErr;
190
191 fn extract(ob: pyo3::Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
192 let o: String = ob.extract::<String>()?;
193 o.parse().map_err(PyValueError::new_err)
194 }
195}
196
197#[derive(Default, Clone, Debug, PartialEq, Eq)]
199pub struct Person {
200 pub name: Option<String>,
202 pub email: Option<String>,
204 pub url: Option<String>,
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub enum BugTracker {
211 GitHub {
213 owner: String,
215 repo: String,
217 },
218 GitLab {
220 base_url: String,
222 path: String,
224 },
225 Launchpad {
227 project: String,
229 },
230 SourceForge {
232 project: String,
234 },
235 Other {
237 database_url: String,
239 submit_url: Option<String>,
241 },
242}
243
244impl serde::ser::Serialize for Person {
245 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
246 where
247 S: serde::ser::Serializer,
248 {
249 let mut map = serde_norway::Mapping::new();
250 if let Some(name) = &self.name {
251 map.insert(
252 serde_norway::Value::String("name".to_string()),
253 serde_norway::Value::String(name.to_string()),
254 );
255 }
256 if let Some(email) = &self.email {
257 map.insert(
258 serde_norway::Value::String("email".to_string()),
259 serde_norway::Value::String(email.to_string()),
260 );
261 }
262 if let Some(url) = &self.url {
263 map.insert(
264 serde_norway::Value::String("url".to_string()),
265 serde_norway::Value::String(url.to_string()),
266 );
267 }
268 let tag = serde_norway::value::TaggedValue {
269 tag: serde_norway::value::Tag::new("!Person"),
270 value: serde_norway::Value::Mapping(map),
271 };
272 tag.serialize(serializer)
273 }
274}
275
276impl<'a> serde::de::Deserialize<'a> for Person {
277 fn deserialize<D>(deserializer: D) -> Result<Person, D::Error>
278 where
279 D: serde::de::Deserializer<'a>,
280 {
281 let value = serde_norway::Value::deserialize(deserializer)?;
282 if let serde_norway::Value::Mapping(map) = value {
283 let mut name = None;
284 let mut email = None;
285 let mut url = None;
286 for (k, v) in map {
287 match k {
288 serde_norway::Value::String(k) => match k.as_str() {
289 "name" => {
290 if let serde_norway::Value::String(s) = v {
291 name = Some(s);
292 }
293 }
294 "email" => {
295 if let serde_norway::Value::String(s) = v {
296 email = Some(s);
297 }
298 }
299 "url" => {
300 if let serde_norway::Value::String(s) = v {
301 url = Some(s);
302 }
303 }
304 n => {
305 return Err(serde::de::Error::custom(format!("unknown key: {}", n)));
306 }
307 },
308 n => {
309 return Err(serde::de::Error::custom(format!(
310 "expected string key, got {:?}",
311 n
312 )));
313 }
314 }
315 }
316 Ok(Person { name, email, url })
317 } else {
318 Err(serde::de::Error::custom("expected mapping"))
319 }
320 }
321}
322
323impl std::fmt::Display for Person {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 write!(f, "{}", self.name.as_ref().unwrap_or(&"".to_string()))?;
326 if let Some(email) = &self.email {
327 write!(f, " <{}>", email)?;
328 }
329 if let Some(url) = &self.url {
330 write!(f, " ({})", url)?;
331 }
332 Ok(())
333 }
334}
335
336impl From<&str> for Person {
337 fn from(text: &str) -> Self {
338 let mut text = text.replace(" at ", "@");
339 text = text.replace(" -at- ", "@");
340 text = text.replace(" -dot- ", ".");
341 text = text.replace("[AT]", "@");
342
343 if text.contains('(') && text.ends_with(')') {
344 if let Some((p1, p2)) = text[..text.len() - 1].split_once('(') {
345 if p2.starts_with("https://") || p2.starts_with("http://") {
346 let url = p2.to_string();
347 if let Some((name, email)) = parseaddr(p1) {
348 Person {
349 name: Some(name),
350 email: Some(email),
351 url: Some(url),
352 }
353 } else {
354 Person {
355 name: Some(p1.to_string()),
356 url: Some(url),
357 ..Default::default()
358 }
359 }
360 } else if p2.contains('@') {
361 Person {
362 name: Some(p1.to_string()),
363 email: Some(p2.to_string()),
364 ..Default::default()
365 }
366 } else {
367 Person {
368 name: Some(text.to_string()),
369 ..Default::default()
370 }
371 }
372 } else {
373 Person {
374 name: Some(text.to_string()),
375 ..Default::default()
376 }
377 }
378 } else if text.contains('<') {
379 if let Some((name, email)) = parseaddr(text.as_str()) {
380 return Person {
381 name: Some(name),
382 email: Some(email),
383 ..Default::default()
384 };
385 } else {
386 Person {
387 name: Some(text.to_string()),
388 ..Default::default()
389 }
390 }
391 } else if text.contains('@') && !text.contains(' ') {
392 return Person {
393 email: Some(text),
394 ..Default::default()
395 };
396 } else {
397 Person {
398 name: Some(text),
399 ..Default::default()
400 }
401 }
402 }
403}
404
405#[cfg(feature = "pyo3")]
406impl<'py> IntoPyObject<'py> for &Person {
407 type Target = PyAny;
408 type Output = Bound<'py, Self::Target>;
409 type Error = PyErr;
410
411 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
412 let m = PyModule::import(py, "upstream_ontologist")?;
413 let person_cls = m.getattr("Person")?;
414 person_cls.call1((self.name.as_ref(), self.email.as_ref(), self.url.as_ref()))
415 }
416}
417
418fn parseaddr(text: &str) -> Option<(String, String)> {
419 let re = regex!(r"(.*?)\s*<([^<>]+)>");
420 if let Some(captures) = re.captures(text) {
421 let name = captures.get(1).map(|m| m.as_str().trim().to_string());
422 let email = captures.get(2).map(|m| m.as_str().trim().to_string());
423 if let (Some(name), Some(email)) = (name, email) {
424 return Some((name, email));
425 }
426 }
427 None
428}
429
430#[cfg(feature = "pyo3")]
431impl<'py> FromPyObject<'_, 'py> for Person {
432 type Error = PyErr;
433
434 fn extract(ob: pyo3::Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
435 let name = ob.getattr("name")?.extract::<Option<String>>()?;
436 let email = ob.getattr("email")?.extract::<Option<String>>()?;
437 let url = ob.getattr("url")?.extract::<Option<String>>()?;
438 Ok(Person { name, email, url })
439 }
440}
441
442#[derive(Clone, Debug, PartialEq, Eq)]
444pub enum UpstreamDatum {
445 Name(String),
451
452 Homepage(String),
458
459 Repository(String),
465
466 RepositoryBrowse(String),
472
473 Description(String),
479
480 Summary(String),
486
487 License(String),
494
495 Author(Vec<Person>),
500
501 Maintainer(Person),
506
507 BugDatabase(String),
513
514 BugSubmit(String),
522
523 Contact(String),
529
530 CargoCrate(String),
536
537 SecurityMD(String),
543
544 SecurityContact(String),
553
554 Version(String),
563
564 Keywords(Vec<String>),
569
570 Copyright(String),
575
576 Documentation(String),
582
583 APIDocumentation(String),
589
590 GoImportPath(String),
595
596 Download(String),
602
603 Wiki(String),
607
608 MailingList(String),
617
618 SourceForgeProject(String),
623
624 Archive(String),
628
629 Demo(String),
635
636 PeclPackage(String),
641
642 Funding(String),
651
652 Changelog(String),
658
659 HaskellPackage(String),
664
665 DebianITP(i32),
670
671 Screenshots(Vec<String>),
676
677 Registry(Vec<(String, String)>),
679
680 CiteAs(String),
685
686 Donation(String),
692
693 Webservice(String),
700
701 BuildSystem(String),
706
707 FAQ(String),
711}
712
713#[derive(PartialEq, Eq, Debug, Clone)]
715pub struct UpstreamDatumWithMetadata {
716 pub datum: UpstreamDatum,
718 pub origin: Option<Origin>,
720 pub certainty: Option<Certainty>,
722}
723
724fn known_bad_url(value: &str) -> bool {
725 if value.contains("${") {
726 return true;
727 }
728 false
729}
730
731impl UpstreamDatum {
732 pub fn field(&self) -> &'static str {
734 match self {
735 UpstreamDatum::Summary(..) => "Summary",
736 UpstreamDatum::Description(..) => "Description",
737 UpstreamDatum::Name(..) => "Name",
738 UpstreamDatum::Homepage(..) => "Homepage",
739 UpstreamDatum::Repository(..) => "Repository",
740 UpstreamDatum::RepositoryBrowse(..) => "Repository-Browse",
741 UpstreamDatum::License(..) => "License",
742 UpstreamDatum::Author(..) => "Author",
743 UpstreamDatum::BugDatabase(..) => "Bug-Database",
744 UpstreamDatum::BugSubmit(..) => "Bug-Submit",
745 UpstreamDatum::Contact(..) => "Contact",
746 UpstreamDatum::CargoCrate(..) => "Cargo-Crate",
747 UpstreamDatum::SecurityMD(..) => "Security-MD",
748 UpstreamDatum::SecurityContact(..) => "Security-Contact",
749 UpstreamDatum::Version(..) => "Version",
750 UpstreamDatum::Keywords(..) => "Keywords",
751 UpstreamDatum::Maintainer(..) => "Maintainer",
752 UpstreamDatum::Copyright(..) => "Copyright",
753 UpstreamDatum::Documentation(..) => "Documentation",
754 UpstreamDatum::APIDocumentation(..) => "API-Documentation",
755 UpstreamDatum::GoImportPath(..) => "Go-Import-Path",
756 UpstreamDatum::Download(..) => "Download",
757 UpstreamDatum::Wiki(..) => "Wiki",
758 UpstreamDatum::MailingList(..) => "MailingList",
759 UpstreamDatum::SourceForgeProject(..) => "SourceForge-Project",
760 UpstreamDatum::Archive(..) => "Archive",
761 UpstreamDatum::Demo(..) => "Demo",
762 UpstreamDatum::PeclPackage(..) => "Pecl-Package",
763 UpstreamDatum::HaskellPackage(..) => "Haskell-Package",
764 UpstreamDatum::Funding(..) => "Funding",
765 UpstreamDatum::Changelog(..) => "Changelog",
766 UpstreamDatum::DebianITP(..) => "Debian-ITP",
767 UpstreamDatum::Screenshots(..) => "Screenshots",
768 UpstreamDatum::Registry(..) => "Registry",
769 UpstreamDatum::CiteAs(..) => "Cite-As",
770 UpstreamDatum::Donation(..) => "Donation",
771 UpstreamDatum::Webservice(..) => "Webservice",
772 UpstreamDatum::BuildSystem(..) => "BuildSystem",
773 UpstreamDatum::FAQ(..) => "FAQ",
774 }
775 }
776
777 pub fn as_str(&self) -> Option<&str> {
779 match self {
780 UpstreamDatum::Name(s) => Some(s),
781 UpstreamDatum::Homepage(s) => Some(s),
782 UpstreamDatum::Repository(s) => Some(s),
783 UpstreamDatum::RepositoryBrowse(s) => Some(s),
784 UpstreamDatum::Description(s) => Some(s),
785 UpstreamDatum::Summary(s) => Some(s),
786 UpstreamDatum::License(s) => Some(s),
787 UpstreamDatum::BugDatabase(s) => Some(s),
788 UpstreamDatum::BugSubmit(s) => Some(s),
789 UpstreamDatum::Contact(s) => Some(s),
790 UpstreamDatum::CargoCrate(s) => Some(s),
791 UpstreamDatum::SecurityMD(s) => Some(s),
792 UpstreamDatum::SecurityContact(s) => Some(s),
793 UpstreamDatum::Version(s) => Some(s),
794 UpstreamDatum::Documentation(s) => Some(s),
795 UpstreamDatum::APIDocumentation(s) => Some(s),
796 UpstreamDatum::GoImportPath(s) => Some(s),
797 UpstreamDatum::Download(s) => Some(s),
798 UpstreamDatum::Wiki(s) => Some(s),
799 UpstreamDatum::MailingList(s) => Some(s),
800 UpstreamDatum::SourceForgeProject(s) => Some(s),
801 UpstreamDatum::Archive(s) => Some(s),
802 UpstreamDatum::Demo(s) => Some(s),
803 UpstreamDatum::PeclPackage(s) => Some(s),
804 UpstreamDatum::HaskellPackage(s) => Some(s),
805 UpstreamDatum::Author(..) => None,
806 UpstreamDatum::Maintainer(..) => None,
807 UpstreamDatum::Keywords(..) => None,
808 UpstreamDatum::Copyright(c) => Some(c),
809 UpstreamDatum::Funding(f) => Some(f),
810 UpstreamDatum::Changelog(c) => Some(c),
811 UpstreamDatum::Screenshots(..) => None,
812 UpstreamDatum::DebianITP(_c) => None,
813 UpstreamDatum::CiteAs(c) => Some(c),
814 UpstreamDatum::Registry(_) => None,
815 UpstreamDatum::Donation(d) => Some(d),
816 UpstreamDatum::Webservice(w) => Some(w),
817 UpstreamDatum::BuildSystem(b) => Some(b),
818 UpstreamDatum::FAQ(f) => Some(f),
819 }
820 }
821
822 pub fn to_url(&self) -> Option<url::Url> {
824 match self {
825 UpstreamDatum::Name(..) => None,
826 UpstreamDatum::Homepage(s) => Some(s.parse().ok()?),
827 UpstreamDatum::Repository(s) => Some(s.parse().ok()?),
828 UpstreamDatum::RepositoryBrowse(s) => Some(s.parse().ok()?),
829 UpstreamDatum::Description(..) => None,
830 UpstreamDatum::Summary(..) => None,
831 UpstreamDatum::License(..) => None,
832 UpstreamDatum::BugDatabase(s) => Some(s.parse().ok()?),
833 UpstreamDatum::BugSubmit(s) => Some(s.parse().ok()?),
834 UpstreamDatum::Contact(..) => None,
835 UpstreamDatum::CargoCrate(s) => Some(s.parse().ok()?),
836 UpstreamDatum::SecurityMD(..) => None,
837 UpstreamDatum::SecurityContact(..) => None,
838 UpstreamDatum::Version(..) => None,
839 UpstreamDatum::Documentation(s) => Some(s.parse().ok()?),
840 UpstreamDatum::APIDocumentation(s) => Some(s.parse().ok()?),
841 UpstreamDatum::GoImportPath(_s) => None,
842 UpstreamDatum::Download(s) => Some(s.parse().ok()?),
843 UpstreamDatum::Wiki(s) => Some(s.parse().ok()?),
844 UpstreamDatum::MailingList(s) => Some(s.parse().ok()?),
845 UpstreamDatum::SourceForgeProject(s) => Some(s.parse().ok()?),
846 UpstreamDatum::Archive(s) => Some(s.parse().ok()?),
847 UpstreamDatum::Demo(s) => Some(s.parse().ok()?),
848 UpstreamDatum::PeclPackage(_s) => None,
849 UpstreamDatum::HaskellPackage(_s) => None,
850 UpstreamDatum::Author(..) => None,
851 UpstreamDatum::Maintainer(..) => None,
852 UpstreamDatum::Keywords(..) => None,
853 UpstreamDatum::Copyright(..) => None,
854 UpstreamDatum::Funding(s) => Some(s.parse().ok()?),
855 UpstreamDatum::Changelog(s) => Some(s.parse().ok()?),
856 UpstreamDatum::Screenshots(..) => None,
857 UpstreamDatum::DebianITP(_c) => None,
858 UpstreamDatum::Registry(_r) => None,
859 UpstreamDatum::CiteAs(_c) => None,
860 UpstreamDatum::Donation(_d) => None,
861 UpstreamDatum::Webservice(w) => Some(w.parse().ok()?),
862 UpstreamDatum::BuildSystem(_) => None,
863 UpstreamDatum::FAQ(f) => Some(f.parse().ok()?),
864 }
865 }
866
867 pub fn as_person(&self) -> Option<&Person> {
869 match self {
870 UpstreamDatum::Maintainer(p) => Some(p),
871 _ => None,
872 }
873 }
874
875 pub fn known_bad_guess(&self) -> bool {
877 match self {
878 UpstreamDatum::BugDatabase(s) | UpstreamDatum::BugSubmit(s) => {
879 if known_bad_url(s) {
880 return true;
881 }
882 let url = match Url::parse(s) {
883 Ok(url) => url,
884 Err(_) => return false,
885 };
886 if url.host_str() == Some("bugzilla.gnome.org") {
887 return true;
888 }
889 if url.host_str() == Some("bugs.freedesktop.org") {
890 return true;
891 }
892 if url.path().ends_with("/sign_in") {
893 return true;
894 }
895 }
896 UpstreamDatum::Repository(s) => {
897 if known_bad_url(s) {
898 return true;
899 }
900 let url = match Url::parse(s) {
901 Ok(url) => url,
902 Err(_) => return false,
903 };
904 if url.host_str() == Some("anongit.kde.org") {
905 return true;
906 }
907 if url.host_str() == Some("git.gitorious.org") {
908 return true;
909 }
910 if url.path().ends_with("/sign_in") {
911 return true;
912 }
913 }
914 UpstreamDatum::Homepage(s) => {
915 let url = match Url::parse(s) {
916 Ok(url) => url,
917 Err(_) => return false,
918 };
919
920 if url.host_str() == Some("pypi.org") {
921 return true;
922 }
923 if url.host_str() == Some("rubygems.org") {
924 return true;
925 }
926 }
927 UpstreamDatum::RepositoryBrowse(s) => {
928 if known_bad_url(s) {
929 return true;
930 }
931 let url = match Url::parse(s) {
932 Ok(url) => url,
933 Err(_) => return false,
934 };
935 if url.host_str() == Some("cgit.kde.org") {
936 return true;
937 }
938 if url.path().ends_with("/sign_in") {
939 return true;
940 }
941 }
942 UpstreamDatum::Author(authors) => {
943 for a in authors {
944 if let Some(name) = &a.name {
945 let lc = name.to_lowercase();
946 if lc.contains("unknown") {
947 return true;
948 }
949 if lc.contains("maintainer") {
950 return true;
951 }
952 if lc.contains("contributor") {
953 return true;
954 }
955 }
956 }
957 }
958 UpstreamDatum::Name(s) => {
959 let lc = s.to_lowercase();
960 if lc.contains("unknown") {
961 return true;
962 }
963 if lc == "package" {
964 return true;
965 }
966 }
967 UpstreamDatum::Version(s) => {
968 let lc = s.to_lowercase();
969 if ["devel", "unknown"].contains(&lc.as_str()) {
970 return true;
971 }
972 }
973 _ => (),
974 }
975 false
976 }
977}
978
979impl std::fmt::Display for UpstreamDatum {
980 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
981 match self {
982 UpstreamDatum::Name(s) => write!(f, "Name: {}", s),
983 UpstreamDatum::Homepage(s) => write!(f, "Homepage: {}", s),
984 UpstreamDatum::Repository(s) => write!(f, "Repository: {}", s),
985 UpstreamDatum::RepositoryBrowse(s) => write!(f, "RepositoryBrowse: {}", s),
986 UpstreamDatum::Description(s) => write!(f, "Description: {}", s),
987 UpstreamDatum::Summary(s) => write!(f, "Summary: {}", s),
988 UpstreamDatum::License(s) => write!(f, "License: {}", s),
989 UpstreamDatum::BugDatabase(s) => write!(f, "BugDatabase: {}", s),
990 UpstreamDatum::BugSubmit(s) => write!(f, "BugSubmit: {}", s),
991 UpstreamDatum::Contact(s) => write!(f, "Contact: {}", s),
992 UpstreamDatum::CargoCrate(s) => write!(f, "CargoCrate: {}", s),
993 UpstreamDatum::SecurityMD(s) => write!(f, "SecurityMD: {}", s),
994 UpstreamDatum::SecurityContact(s) => write!(f, "SecurityContact: {}", s),
995 UpstreamDatum::Version(s) => write!(f, "Version: {}", s),
996 UpstreamDatum::Documentation(s) => write!(f, "Documentation: {}", s),
997 UpstreamDatum::APIDocumentation(s) => write!(f, "API-Documentation: {}", s),
998 UpstreamDatum::GoImportPath(s) => write!(f, "GoImportPath: {}", s),
999 UpstreamDatum::Download(s) => write!(f, "Download: {}", s),
1000 UpstreamDatum::Wiki(s) => write!(f, "Wiki: {}", s),
1001 UpstreamDatum::MailingList(s) => write!(f, "MailingList: {}", s),
1002 UpstreamDatum::SourceForgeProject(s) => write!(f, "SourceForgeProject: {}", s),
1003 UpstreamDatum::Archive(s) => write!(f, "Archive: {}", s),
1004 UpstreamDatum::Demo(s) => write!(f, "Demo: {}", s),
1005 UpstreamDatum::PeclPackage(s) => write!(f, "PeclPackage: {}", s),
1006 UpstreamDatum::Author(authors) => {
1007 write!(
1008 f,
1009 "Author: {}",
1010 authors
1011 .iter()
1012 .map(|a| a.to_string())
1013 .collect::<Vec<_>>()
1014 .join(", ")
1015 )
1016 }
1017 UpstreamDatum::Maintainer(maintainer) => {
1018 write!(f, "Maintainer: {}", maintainer)
1019 }
1020 UpstreamDatum::Keywords(keywords) => {
1021 write!(
1022 f,
1023 "Keywords: {}",
1024 keywords
1025 .iter()
1026 .map(|a| a.to_string())
1027 .collect::<Vec<_>>()
1028 .join(", ")
1029 )
1030 }
1031 UpstreamDatum::Copyright(s) => {
1032 write!(f, "Copyright: {}", s)
1033 }
1034 UpstreamDatum::Funding(s) => {
1035 write!(f, "Funding: {}", s)
1036 }
1037 UpstreamDatum::Changelog(s) => {
1038 write!(f, "Changelog: {}", s)
1039 }
1040 UpstreamDatum::DebianITP(s) => {
1041 write!(f, "DebianITP: {}", s)
1042 }
1043 UpstreamDatum::HaskellPackage(p) => {
1044 write!(f, "HaskellPackage: {}", p)
1045 }
1046 UpstreamDatum::Screenshots(s) => {
1047 write!(f, "Screenshots: {}", s.join(", "))
1048 }
1049 UpstreamDatum::Registry(r) => {
1050 write!(f, "Registry:")?;
1051 for (k, v) in r {
1052 write!(f, " - Name: {}", k)?;
1053 write!(f, " Entry: {}", v)?;
1054 }
1055 Ok(())
1056 }
1057 UpstreamDatum::CiteAs(c) => {
1058 write!(f, "Cite-As: {}", c)
1059 }
1060 UpstreamDatum::Donation(d) => {
1061 write!(f, "Donation: {}", d)
1062 }
1063 UpstreamDatum::Webservice(w) => {
1064 write!(f, "Webservice: {}", w)
1065 }
1066 UpstreamDatum::BuildSystem(bs) => {
1067 write!(f, "BuildSystem: {}", bs)
1068 }
1069 UpstreamDatum::FAQ(faq) => {
1070 write!(f, "FAQ: {}", faq)
1071 }
1072 }
1073 }
1074}
1075
1076impl serde::ser::Serialize for UpstreamDatum {
1077 fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1078 match self {
1079 UpstreamDatum::Name(s) => serializer.serialize_str(s),
1080 UpstreamDatum::Homepage(s) => serializer.serialize_str(s),
1081 UpstreamDatum::Repository(s) => serializer.serialize_str(s),
1082 UpstreamDatum::RepositoryBrowse(s) => serializer.serialize_str(s),
1083 UpstreamDatum::Description(s) => serializer.serialize_str(s),
1084 UpstreamDatum::Summary(s) => serializer.serialize_str(s),
1085 UpstreamDatum::License(s) => serializer.serialize_str(s),
1086 UpstreamDatum::BugDatabase(s) => serializer.serialize_str(s),
1087 UpstreamDatum::BugSubmit(s) => serializer.serialize_str(s),
1088 UpstreamDatum::Contact(s) => serializer.serialize_str(s),
1089 UpstreamDatum::CargoCrate(s) => serializer.serialize_str(s),
1090 UpstreamDatum::SecurityMD(s) => serializer.serialize_str(s),
1091 UpstreamDatum::SecurityContact(s) => serializer.serialize_str(s),
1092 UpstreamDatum::Version(s) => serializer.serialize_str(s),
1093 UpstreamDatum::Documentation(s) => serializer.serialize_str(s),
1094 UpstreamDatum::APIDocumentation(s) => serializer.serialize_str(s),
1095 UpstreamDatum::GoImportPath(s) => serializer.serialize_str(s),
1096 UpstreamDatum::Download(s) => serializer.serialize_str(s),
1097 UpstreamDatum::Wiki(s) => serializer.serialize_str(s),
1098 UpstreamDatum::MailingList(s) => serializer.serialize_str(s),
1099 UpstreamDatum::SourceForgeProject(s) => serializer.serialize_str(s),
1100 UpstreamDatum::Archive(s) => serializer.serialize_str(s),
1101 UpstreamDatum::Demo(s) => serializer.serialize_str(s),
1102 UpstreamDatum::PeclPackage(s) => serializer.serialize_str(s),
1103 UpstreamDatum::Author(authors) => {
1104 let mut seq = serializer.serialize_seq(Some(authors.len()))?;
1105 for a in authors {
1106 seq.serialize_element(a)?;
1107 }
1108 seq.end()
1109 }
1110 UpstreamDatum::Maintainer(maintainer) => maintainer.serialize(serializer),
1111 UpstreamDatum::Keywords(keywords) => {
1112 let mut seq = serializer.serialize_seq(Some(keywords.len()))?;
1113 for a in keywords {
1114 seq.serialize_element(a)?;
1115 }
1116 seq.end()
1117 }
1118 UpstreamDatum::Copyright(s) => serializer.serialize_str(s),
1119 UpstreamDatum::Funding(s) => serializer.serialize_str(s),
1120 UpstreamDatum::Changelog(s) => serializer.serialize_str(s),
1121 UpstreamDatum::DebianITP(s) => serializer.serialize_i32(*s),
1122 UpstreamDatum::HaskellPackage(p) => serializer.serialize_str(p),
1123 UpstreamDatum::Screenshots(s) => {
1124 let mut seq = serializer.serialize_seq(Some(s.len()))?;
1125 for s in s {
1126 seq.serialize_element(s)?;
1127 }
1128 seq.end()
1129 }
1130 UpstreamDatum::CiteAs(c) => serializer.serialize_str(c),
1131 UpstreamDatum::Registry(r) => {
1132 let mut l = serializer.serialize_seq(Some(r.len()))?;
1133 for (k, v) in r {
1134 let mut m = serde_norway::Mapping::new();
1135 m.insert(
1136 serde_norway::Value::String("Name".to_string()),
1137 serde_norway::to_value(k).unwrap(),
1138 );
1139 m.insert(
1140 serde_norway::Value::String("Entry".to_string()),
1141 serde_norway::to_value(v).unwrap(),
1142 );
1143 l.serialize_element(&m)?;
1144 }
1145 l.end()
1146 }
1147 UpstreamDatum::Donation(d) => serializer.serialize_str(d),
1148 UpstreamDatum::Webservice(w) => serializer.serialize_str(w),
1149 UpstreamDatum::BuildSystem(bs) => serializer.serialize_str(bs),
1150 UpstreamDatum::FAQ(faq) => serializer.serialize_str(faq),
1151 }
1152 }
1153}
1154
1155#[derive(PartialEq, Eq, Debug, Clone)]
1157pub struct UpstreamMetadata(Vec<UpstreamDatumWithMetadata>);
1158
1159impl UpstreamMetadata {
1160 pub fn new() -> Self {
1162 UpstreamMetadata(Vec::new())
1163 }
1164
1165 pub fn is_empty(&self) -> bool {
1167 self.0.is_empty()
1168 }
1169
1170 pub fn len(&self) -> usize {
1172 self.0.len()
1173 }
1174
1175 pub fn sort(&mut self) {
1177 self.0.sort_by(|a, b| a.datum.field().cmp(b.datum.field()));
1178 }
1179
1180 pub fn from_data(data: Vec<UpstreamDatumWithMetadata>) -> Self {
1182 Self(data)
1183 }
1184
1185 pub fn mut_items(&mut self) -> &mut Vec<UpstreamDatumWithMetadata> {
1187 &mut self.0
1188 }
1189
1190 pub fn iter(&self) -> impl Iterator<Item = &UpstreamDatumWithMetadata> {
1192 self.0.iter()
1193 }
1194
1195 pub fn mut_iter(&mut self) -> impl Iterator<Item = &mut UpstreamDatumWithMetadata> {
1197 self.0.iter_mut()
1198 }
1199
1200 pub fn get(&self, field: &str) -> Option<&UpstreamDatumWithMetadata> {
1202 self.0.iter().find(|d| d.datum.field() == field)
1203 }
1204
1205 pub fn get_mut(&mut self, field: &str) -> Option<&mut UpstreamDatumWithMetadata> {
1207 self.0.iter_mut().find(|d| d.datum.field() == field)
1208 }
1209
1210 pub fn insert(&mut self, datum: UpstreamDatumWithMetadata) {
1212 self.0.push(datum);
1213 }
1214
1215 pub fn contains_key(&self, field: &str) -> bool {
1217 self.get(field).is_some()
1218 }
1219
1220 pub fn discard_known_bad(&mut self) {
1222 self.0.retain(|d| !d.datum.known_bad_guess());
1223 }
1224
1225 pub fn update(
1227 &mut self,
1228 new_items: impl Iterator<Item = UpstreamDatumWithMetadata>,
1229 ) -> Vec<UpstreamDatumWithMetadata> {
1230 update_from_guesses(&mut self.0, new_items)
1231 }
1232
1233 pub fn remove(&mut self, field: &str) -> Option<UpstreamDatumWithMetadata> {
1235 let index = self.0.iter().position(|d| d.datum.field() == field)?;
1236 Some(self.0.remove(index))
1237 }
1238
1239 pub fn name(&self) -> Option<&str> {
1241 self.get("Name").and_then(|d| d.datum.as_str())
1242 }
1243
1244 pub fn homepage(&self) -> Option<&str> {
1246 self.get("Homepage").and_then(|d| d.datum.as_str())
1247 }
1248
1249 pub fn repository(&self) -> Option<&str> {
1251 self.get("Repository").and_then(|d| d.datum.as_str())
1252 }
1253
1254 pub fn repository_browse(&self) -> Option<&str> {
1256 self.get("Repository-Browse").and_then(|d| d.datum.as_str())
1257 }
1258
1259 pub fn description(&self) -> Option<&str> {
1261 self.get("Description").and_then(|d| d.datum.as_str())
1262 }
1263
1264 pub fn summary(&self) -> Option<&str> {
1266 self.get("Summary").and_then(|d| d.datum.as_str())
1267 }
1268
1269 pub fn license(&self) -> Option<&str> {
1271 self.get("License").and_then(|d| d.datum.as_str())
1272 }
1273
1274 pub fn author(&self) -> Option<&Vec<Person>> {
1276 self.get("Author").map(|d| match &d.datum {
1277 UpstreamDatum::Author(authors) => authors,
1278 _ => unreachable!(),
1279 })
1280 }
1281
1282 pub fn maintainer(&self) -> Option<&Person> {
1284 self.get("Maintainer").map(|d| match &d.datum {
1285 UpstreamDatum::Maintainer(maintainer) => maintainer,
1286 _ => unreachable!(),
1287 })
1288 }
1289
1290 pub fn bug_database(&self) -> Option<&str> {
1292 self.get("Bug-Database").and_then(|d| d.datum.as_str())
1293 }
1294
1295 pub fn bug_submit(&self) -> Option<&str> {
1297 self.get("Bug-Submit").and_then(|d| d.datum.as_str())
1298 }
1299
1300 pub fn contact(&self) -> Option<&str> {
1302 self.get("Contact").and_then(|d| d.datum.as_str())
1303 }
1304
1305 pub fn cargo_crate(&self) -> Option<&str> {
1307 self.get("Cargo-Crate").and_then(|d| d.datum.as_str())
1308 }
1309
1310 pub fn security_md(&self) -> Option<&str> {
1312 self.get("Security-MD").and_then(|d| d.datum.as_str())
1313 }
1314
1315 pub fn security_contact(&self) -> Option<&str> {
1317 self.get("Security-Contact").and_then(|d| d.datum.as_str())
1318 }
1319
1320 pub fn version(&self) -> Option<&str> {
1322 self.get("Version").and_then(|d| d.datum.as_str())
1323 }
1324
1325 pub fn keywords(&self) -> Option<&Vec<String>> {
1327 self.get("Keywords").map(|d| match &d.datum {
1328 UpstreamDatum::Keywords(keywords) => keywords,
1329 _ => unreachable!(),
1330 })
1331 }
1332
1333 pub fn documentation(&self) -> Option<&str> {
1335 self.get("Documentation").and_then(|d| d.datum.as_str())
1336 }
1337
1338 pub fn api_documentation(&self) -> Option<&str> {
1340 self.get("API-Documentation").and_then(|d| d.datum.as_str())
1341 }
1342
1343 pub fn go_import_path(&self) -> Option<&str> {
1345 self.get("Go-Import-Path").and_then(|d| d.datum.as_str())
1346 }
1347
1348 pub fn download(&self) -> Option<&str> {
1350 self.get("Download").and_then(|d| d.datum.as_str())
1351 }
1352
1353 pub fn wiki(&self) -> Option<&str> {
1355 self.get("Wiki").and_then(|d| d.datum.as_str())
1356 }
1357
1358 pub fn mailing_list(&self) -> Option<&str> {
1360 self.get("MailingList").and_then(|d| d.datum.as_str())
1361 }
1362
1363 pub fn sourceforge_project(&self) -> Option<&str> {
1365 self.get("SourceForge-Project")
1366 .and_then(|d| d.datum.as_str())
1367 }
1368
1369 pub fn archive(&self) -> Option<&str> {
1371 self.get("Archive").and_then(|d| d.datum.as_str())
1372 }
1373
1374 pub fn demo(&self) -> Option<&str> {
1376 self.get("Demo").and_then(|d| d.datum.as_str())
1377 }
1378
1379 pub fn pecl_package(&self) -> Option<&str> {
1381 self.get("Pecl-Package").and_then(|d| d.datum.as_str())
1382 }
1383
1384 pub fn haskell_package(&self) -> Option<&str> {
1386 self.get("Haskell-Package").and_then(|d| d.datum.as_str())
1387 }
1388
1389 pub fn funding(&self) -> Option<&str> {
1391 self.get("Funding").and_then(|d| d.datum.as_str())
1392 }
1393
1394 pub fn changelog(&self) -> Option<&str> {
1396 self.get("Changelog").and_then(|d| d.datum.as_str())
1397 }
1398
1399 pub fn debian_itp(&self) -> Option<i32> {
1401 self.get("Debian-ITP").and_then(|d| match &d.datum {
1402 UpstreamDatum::DebianITP(itp) => Some(*itp),
1403 _ => unreachable!(),
1404 })
1405 }
1406
1407 pub fn screenshots(&self) -> Option<&Vec<String>> {
1409 self.get("Screenshots").map(|d| match &d.datum {
1410 UpstreamDatum::Screenshots(screenshots) => screenshots,
1411 _ => unreachable!(),
1412 })
1413 }
1414
1415 pub fn donation(&self) -> Option<&str> {
1417 self.get("Donation").and_then(|d| d.datum.as_str())
1418 }
1419
1420 pub fn cite_as(&self) -> Option<&str> {
1422 self.get("Cite-As").and_then(|d| d.datum.as_str())
1423 }
1424
1425 pub fn registry(&self) -> Option<&Vec<(String, String)>> {
1427 self.get("Registry").map(|d| match &d.datum {
1428 UpstreamDatum::Registry(registry) => registry,
1429 _ => unreachable!(),
1430 })
1431 }
1432
1433 pub fn webservice(&self) -> Option<&str> {
1435 self.get("Webservice").and_then(|d| d.datum.as_str())
1436 }
1437
1438 pub fn buildsystem(&self) -> Option<&str> {
1440 self.get("BuildSystem").and_then(|d| d.datum.as_str())
1441 }
1442
1443 pub fn copyright(&self) -> Option<&str> {
1445 self.get("Copyright").and_then(|d| d.datum.as_str())
1446 }
1447
1448 pub fn faq(&self) -> Option<&str> {
1450 self.get("FAQ").and_then(|d| d.datum.as_str())
1451 }
1452}
1453
1454impl std::ops::Index<&str> for UpstreamMetadata {
1455 type Output = UpstreamDatumWithMetadata;
1456
1457 fn index(&self, index: &str) -> &Self::Output {
1458 self.get(index).unwrap()
1459 }
1460}
1461
1462impl Default for UpstreamMetadata {
1463 fn default() -> Self {
1464 UpstreamMetadata::new()
1465 }
1466}
1467
1468impl Iterator for UpstreamMetadata {
1469 type Item = UpstreamDatumWithMetadata;
1470
1471 fn next(&mut self) -> Option<Self::Item> {
1472 self.0.pop()
1473 }
1474}
1475
1476impl From<UpstreamDatum> for UpstreamDatumWithMetadata {
1477 fn from(d: UpstreamDatum) -> Self {
1478 UpstreamDatumWithMetadata {
1479 datum: d,
1480 certainty: None,
1481 origin: None,
1482 }
1483 }
1484}
1485
1486impl From<Vec<UpstreamDatumWithMetadata>> for UpstreamMetadata {
1487 fn from(v: Vec<UpstreamDatumWithMetadata>) -> Self {
1488 UpstreamMetadata(v)
1489 }
1490}
1491
1492impl From<Vec<UpstreamDatum>> for UpstreamMetadata {
1493 fn from(v: Vec<UpstreamDatum>) -> Self {
1494 UpstreamMetadata(
1495 v.into_iter()
1496 .map(|d| UpstreamDatumWithMetadata {
1497 datum: d,
1498 certainty: None,
1499 origin: None,
1500 })
1501 .collect(),
1502 )
1503 }
1504}
1505
1506impl From<UpstreamMetadata> for Vec<UpstreamDatumWithMetadata> {
1507 fn from(v: UpstreamMetadata) -> Self {
1508 v.0
1509 }
1510}
1511
1512impl From<UpstreamMetadata> for Vec<UpstreamDatum> {
1513 fn from(v: UpstreamMetadata) -> Self {
1514 v.0.into_iter().map(|d| d.datum).collect()
1515 }
1516}
1517
1518impl serde::ser::Serialize for UpstreamMetadata {
1519 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1520 where
1521 S: serde::ser::Serializer,
1522 {
1523 let mut map = serde_norway::Mapping::new();
1524 for datum in &self.0 {
1525 map.insert(
1526 serde_norway::Value::String(datum.datum.field().to_string()),
1527 serde_norway::to_value(datum).unwrap(),
1528 );
1529 }
1530 map.serialize(serializer)
1531 }
1532}
1533
1534#[cfg(feature = "pyo3")]
1535impl<'py> IntoPyObject<'py> for &UpstreamDatumWithMetadata {
1536 type Target = PyAny;
1537 type Output = Bound<'py, Self::Target>;
1538 type Error = PyErr;
1539
1540 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
1541 let m = PyModule::import(py, "upstream_ontologist.guess")?;
1542
1543 let cls = m.getattr("UpstreamDatum")?;
1544
1545 let (field, py_datum) = self
1546 .datum
1547 .into_pyobject(py)?
1548 .extract::<(String, Bound<PyAny>)>()?;
1549
1550 let kwargs = pyo3::types::PyDict::new(py);
1551 kwargs.set_item("certainty", self.certainty.map(|x| x.to_string()))?;
1552 kwargs.set_item("origin", self.origin.as_ref())?;
1553
1554 cls.call((field, py_datum), Some(&kwargs))
1555 }
1556}
1557
1558impl serde::ser::Serialize for UpstreamDatumWithMetadata {
1559 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1560 where
1561 S: serde::ser::Serializer,
1562 {
1563 UpstreamDatum::serialize(&self.datum, serializer)
1564 }
1565}
1566
1567pub trait UpstreamDataProvider {
1569 fn provide(
1571 path: &std::path::Path,
1572 trust_package: bool,
1573 ) -> dyn Iterator<Item = (UpstreamDatum, Certainty)>;
1574}
1575
1576#[derive(Debug)]
1578pub enum HTTPJSONError {
1579 HTTPError(reqwest::Error),
1581 Timeout(tokio::time::Duration),
1583 Error {
1585 url: reqwest::Url,
1587 status: u16,
1589 response: Box<reqwest::Response>,
1591 },
1592}
1593
1594impl std::fmt::Display for HTTPJSONError {
1595 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1596 match self {
1597 HTTPJSONError::HTTPError(e) => write!(f, "{}", e),
1598 HTTPJSONError::Timeout(timeout) => write!(f, "Timeout after {:?}", timeout),
1599 HTTPJSONError::Error {
1600 url,
1601 status,
1602 response: _,
1603 } => write!(f, "HTTP error {} for {}:", status, url,),
1604 }
1605 }
1606}
1607
1608pub async fn load_json_url(
1610 http_url: &Url,
1611 timeout: Option<std::time::Duration>,
1612) -> Result<serde_json::Value, HTTPJSONError> {
1613 let mut headers = HeaderMap::new();
1614 headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
1615
1616 let client = crate::http::build_client()
1617 .default_headers(headers)
1618 .build()
1619 .map_err(HTTPJSONError::HTTPError)?;
1620
1621 let http_url: reqwest::Url = Into::<String>::into(http_url.clone()).parse().unwrap();
1622
1623 let request = client
1624 .get(http_url)
1625 .build()
1626 .map_err(HTTPJSONError::HTTPError)?;
1627
1628 let timeout = timeout.unwrap_or(std::time::Duration::from_secs(30));
1629
1630 let response = tokio::time::timeout(timeout, client.execute(request))
1631 .await
1632 .map_err(|_| HTTPJSONError::Timeout(timeout))?
1633 .map_err(HTTPJSONError::HTTPError)?;
1634
1635 if !response.status().is_success() {
1636 return Err(HTTPJSONError::Error {
1637 url: response.url().clone(),
1638 status: response.status().as_u16(),
1639 response: Box::new(response),
1640 });
1641 }
1642
1643 let json_contents: serde_json::Value =
1644 response.json().await.map_err(HTTPJSONError::HTTPError)?;
1645
1646 Ok(json_contents)
1647}
1648
1649fn xmlparse_simplify_namespaces(path: &Path, namespaces: &[&str]) -> Option<xmltree::Element> {
1650 let namespaces = namespaces
1651 .iter()
1652 .map(|ns| format!("{{{}{}}}", ns, ns))
1653 .collect::<Vec<_>>();
1654 let mut f = std::fs::File::open(path).unwrap();
1655 let mut buf = Vec::new();
1656 f.read_to_end(&mut buf).ok()?;
1657 let mut tree = xmltree::Element::parse(std::io::Cursor::new(buf)).ok()?;
1658 simplify_namespaces(&mut tree, &namespaces);
1659 Some(tree)
1660}
1661
1662fn simplify_namespaces(element: &mut xmltree::Element, namespaces: &[String]) {
1663 use xmltree::XMLNode;
1664 element.prefix = None;
1665 if let Some(namespace) = namespaces.iter().find(|&ns| element.name.starts_with(ns)) {
1666 element.name = element.name[namespace.len()..].to_string();
1667 }
1668 for child in &mut element.children {
1669 if let XMLNode::Element(ref mut child_element) = child {
1670 simplify_namespaces(child_element, namespaces);
1671 }
1672 }
1673}
1674
1675pub enum CanonicalizeError {
1677 InvalidUrl(Url, String),
1679 Unverifiable(Url, String),
1681 RateLimited(Url),
1683}
1684
1685#[derive(Debug)]
1686pub struct PathSegmentError;
1688
1689pub async fn check_url_canonical(url: &Url) -> Result<Url, CanonicalizeError> {
1691 if url.scheme() != "http" && url.scheme() != "https" {
1692 return Err(CanonicalizeError::Unverifiable(
1693 url.clone(),
1694 format!("Unsupported scheme {}", url.scheme()),
1695 ));
1696 }
1697
1698 let client = crate::http::build_client()
1699 .build()
1700 .map_err(|e| CanonicalizeError::Unverifiable(url.clone(), format!("HTTP error {}", e)))?;
1701
1702 let response =
1703 client.get(url.as_str()).send().await.map_err(|e| {
1704 CanonicalizeError::Unverifiable(url.clone(), format!("HTTP error {}", e))
1705 })?;
1706
1707 match response.status() {
1708 status if status.is_success() => Ok(response.url().clone()),
1709 status if status == reqwest::StatusCode::TOO_MANY_REQUESTS => {
1710 Err(CanonicalizeError::RateLimited(url.clone()))
1711 }
1712 status if status == reqwest::StatusCode::NOT_FOUND => Err(CanonicalizeError::InvalidUrl(
1713 url.clone(),
1714 format!("Not found: {}", response.status()),
1715 )),
1716 status if status.is_server_error() => Err(CanonicalizeError::Unverifiable(
1717 url.clone(),
1718 format!("Server down: {}", response.status()),
1719 )),
1720 _ => Err(CanonicalizeError::Unverifiable(
1721 url.clone(),
1722 format!("Unknown HTTP error {}", response.status()),
1723 )),
1724 }
1725}
1726
1727pub fn with_path_segments(url: &Url, path_segments: &[&str]) -> Result<Url, PathSegmentError> {
1729 let mut url = url.clone();
1730 url.path_segments_mut()
1731 .map_err(|_| PathSegmentError)?
1732 .clear()
1733 .extend(path_segments.iter().copied());
1734 Ok(url)
1735}
1736
1737fn to_https_url(url: &Url) -> Option<Url> {
1743 let host = url.host_str()?;
1744 let port = url.port().map(|p| format!(":{}", p)).unwrap_or_default();
1745 Url::parse(&format!("https://{}{}{}", host, port, url.path())).ok()
1746}
1747
1748#[async_trait::async_trait]
1750pub trait Forge: Send + Sync {
1751 fn repository_browse_can_be_homepage(&self) -> bool;
1753
1754 fn name(&self) -> &'static str;
1756
1757 fn bug_database_url_from_bug_submit_url(&self, _url: &Url) -> Option<Url> {
1759 None
1760 }
1761
1762 fn bug_submit_url_from_bug_database_url(&self, _url: &Url) -> Option<Url> {
1764 None
1765 }
1766
1767 async fn check_bug_database_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
1769 Err(CanonicalizeError::Unverifiable(
1770 url.clone(),
1771 "Not implemented".to_string(),
1772 ))
1773 }
1774
1775 async fn check_bug_submit_url_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
1777 Err(CanonicalizeError::Unverifiable(
1778 url.clone(),
1779 "Not implemented".to_string(),
1780 ))
1781 }
1782
1783 fn bug_database_from_issue_url(&self, _url: &Url) -> Option<Url> {
1785 None
1786 }
1787
1788 fn bug_database_url_from_repo_url(&self, _url: &Url) -> Option<Url> {
1790 None
1791 }
1792
1793 fn repo_url_from_merge_request_url(&self, _url: &Url) -> Option<Url> {
1795 None
1796 }
1797
1798 async fn extend_metadata(
1800 &self,
1801 _metadata: &mut Vec<UpstreamDatumWithMetadata>,
1802 _project: &str,
1803 _max_certainty: Option<Certainty>,
1804 ) {
1805 }
1806}
1807
1808pub struct GitHub;
1810
1811impl Default for GitHub {
1812 fn default() -> Self {
1813 Self::new()
1814 }
1815}
1816
1817impl GitHub {
1818 pub fn new() -> Self {
1820 Self
1821 }
1822}
1823
1824#[async_trait::async_trait]
1825impl Forge for GitHub {
1826 fn name(&self) -> &'static str {
1827 "GitHub"
1828 }
1829
1830 fn repository_browse_can_be_homepage(&self) -> bool {
1831 true
1832 }
1833
1834 fn bug_database_url_from_bug_submit_url(&self, url: &Url) -> Option<Url> {
1835 assert_eq!(url.host(), Some(url::Host::Domain("github.com")));
1836 let path_elements = url.path_segments().unwrap().collect::<Vec<_>>();
1837
1838 if path_elements.len() != 3 && path_elements.len() != 4 {
1839 return None;
1840 }
1841 if path_elements[2] != "issues" {
1842 return None;
1843 }
1844
1845 let url = to_https_url(url)?;
1846
1847 Some(with_path_segments(&url, &path_elements[0..3]).unwrap())
1848 }
1849
1850 fn bug_submit_url_from_bug_database_url(&self, url: &Url) -> Option<Url> {
1851 assert_eq!(url.host(), Some(url::Host::Domain("github.com")));
1852 let path_elements = url.path_segments().unwrap().collect::<Vec<_>>();
1853
1854 if path_elements.len() != 3 {
1855 return None;
1856 }
1857 if path_elements[2] != "issues" {
1858 return None;
1859 }
1860
1861 let mut url = to_https_url(url)?;
1862 url.path_segments_mut().unwrap().push("new");
1863 Some(url)
1864 }
1865
1866 async fn check_bug_database_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
1867 assert_eq!(url.host(), Some(url::Host::Domain("github.com")));
1868 let path_elements = url.path_segments().unwrap().collect::<Vec<_>>();
1869
1870 if path_elements.len() != 3 {
1871 return Err(CanonicalizeError::InvalidUrl(
1872 url.clone(),
1873 "GitHub URL with missing path elements".to_string(),
1874 ));
1875 }
1876 if path_elements[2] != "issues" {
1877 return Err(CanonicalizeError::InvalidUrl(
1878 url.clone(),
1879 "GitHub URL with missing path elements".to_string(),
1880 ));
1881 }
1882
1883 let api_path = format!("repos/{}/{}", path_elements[0], path_elements[1]);
1884
1885 let data = match crate::github::load_github_json(&api_path).await {
1886 Ok(data) => data,
1887 Err(HTTPJSONError::Error { status: 404, .. }) => {
1888 return Err(CanonicalizeError::InvalidUrl(
1889 url.clone(),
1890 "Project does not exist".to_string(),
1891 ));
1892 }
1893 Err(HTTPJSONError::Error { status: 403, .. }) => {
1894 warn!("Unable to verify bug database URL {}: rate limited", url);
1896 return Err(CanonicalizeError::RateLimited(url.clone()));
1897 }
1898 Err(e) => {
1899 return Err(CanonicalizeError::Unverifiable(
1900 url.clone(),
1901 format!("Unable to verify bug database URL: {}", e),
1902 ));
1903 }
1904 };
1905
1906 if data["has_issues"].as_bool() != Some(true) {
1907 return Err(CanonicalizeError::InvalidUrl(
1908 url.clone(),
1909 "Project does not have issues enabled".to_string(),
1910 ));
1911 }
1912
1913 if data.get("archived").unwrap_or(&serde_json::Value::Null)
1914 == &serde_json::Value::Bool(true)
1915 {
1916 return Err(CanonicalizeError::InvalidUrl(
1917 url.clone(),
1918 "Project is archived".to_string(),
1919 ));
1920 }
1921
1922 let mut url = Url::parse(data["html_url"].as_str().ok_or_else(|| {
1923 CanonicalizeError::Unverifiable(
1924 url.clone(),
1925 "Unable to verify bug database URL: no html_url".to_string(),
1926 )
1927 })?)
1928 .map_err(|e| {
1929 CanonicalizeError::Unverifiable(
1930 url.clone(),
1931 format!("Unable to verify bug database URL: {}", e),
1932 )
1933 })?;
1934
1935 url.set_scheme("https").expect("valid scheme");
1936 url.path_segments_mut()
1937 .expect("path segments")
1938 .push("issues");
1939
1940 Ok(url)
1941 }
1942
1943 async fn check_bug_submit_url_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
1944 let mut path_segments = url.path_segments().unwrap().collect::<Vec<_>>();
1945 path_segments.pop();
1946 let db_url = with_path_segments(url, &path_segments).unwrap();
1947 let mut canonical_db_url = self.check_bug_database_canonical(&db_url).await?;
1948 canonical_db_url.set_scheme("https").expect("valid scheme");
1949 canonical_db_url
1950 .path_segments_mut()
1951 .expect("path segments")
1952 .push("new");
1953 Ok(canonical_db_url)
1954 }
1955
1956 fn bug_database_from_issue_url(&self, url: &Url) -> Option<Url> {
1957 let path_elements = url.path_segments()?.collect::<Vec<_>>();
1958 if path_elements.len() < 4
1959 || path_elements[2] != "issues"
1960 || path_elements[3].parse::<u32>().is_err()
1961 {
1962 return None;
1963 }
1964 let url = to_https_url(url)?;
1965 Some(with_path_segments(&url, &path_elements[0..3]).unwrap())
1966 }
1967
1968 fn bug_database_url_from_repo_url(&self, url: &Url) -> Option<Url> {
1969 let path = url.path_segments()?.take(2).collect::<Vec<&str>>();
1970 if path.len() < 2 {
1971 return None;
1972 }
1973 let repo = path[1].strip_suffix(".git").unwrap_or(path[1]);
1974
1975 let url = to_https_url(url)?;
1976 Some(with_path_segments(&url, &[path[0], repo, "issues"]).unwrap())
1977 }
1978
1979 fn repo_url_from_merge_request_url(&self, url: &Url) -> Option<Url> {
1980 let path_elements = url.path_segments()?.collect::<Vec<_>>();
1981 if path_elements.len() < 4
1982 || path_elements[2] != "pull"
1983 || path_elements[3].parse::<u32>().is_err()
1984 {
1985 return None;
1986 }
1987 let url = to_https_url(url)?;
1988 Some(with_path_segments(&url, &path_elements[0..2]).unwrap())
1989 }
1990}
1991
1992static DEFAULT_ASCII_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS
1993 .add(b'/')
1994 .add(b'?')
1995 .add(b'#')
1996 .add(b'%');
1997
1998pub struct GitLab;
2000
2001impl Default for GitLab {
2002 fn default() -> Self {
2003 Self::new()
2004 }
2005}
2006
2007impl GitLab {
2008 pub fn new() -> Self {
2010 Self
2011 }
2012}
2013
2014#[async_trait::async_trait]
2015impl Forge for GitLab {
2016 fn name(&self) -> &'static str {
2017 "GitLab"
2018 }
2019
2020 fn repository_browse_can_be_homepage(&self) -> bool {
2021 true
2022 }
2023
2024 fn bug_database_url_from_bug_submit_url(&self, url: &Url) -> Option<Url> {
2025 let mut path_elements = url
2026 .path_segments()
2027 .expect("path segments")
2028 .collect::<Vec<_>>();
2029
2030 if path_elements.len() < 2 {
2031 return None;
2032 }
2033 if path_elements[path_elements.len() - 2] != "issues" {
2034 return None;
2035 }
2036 if path_elements[path_elements.len() - 1] != "new" {
2037 path_elements.pop();
2038 }
2039
2040 Some(with_path_segments(url, &path_elements[0..path_elements.len() - 3]).unwrap())
2041 }
2042
2043 fn bug_submit_url_from_bug_database_url(&self, url: &Url) -> Option<Url> {
2044 let path_elements = url
2045 .path_segments()
2046 .expect("path segments")
2047 .collect::<Vec<_>>();
2048
2049 if path_elements.len() < 2 {
2050 return None;
2051 }
2052 if path_elements[path_elements.len() - 1] != "issues" {
2053 return None;
2054 }
2055
2056 let mut url = url.clone();
2057 url.path_segments_mut().expect("path segments").push("new");
2058
2059 Some(url)
2060 }
2061
2062 async fn check_bug_database_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
2063 let host = url
2064 .host()
2065 .ok_or_else(|| CanonicalizeError::InvalidUrl(url.clone(), "no host".to_string()))?;
2066 let mut path_elements = url
2067 .path_segments()
2068 .expect("path segments")
2069 .collect::<Vec<_>>();
2070 if path_elements.len() < 2 || path_elements[path_elements.len() - 1] != "issues" {
2071 return Err(CanonicalizeError::InvalidUrl(
2072 url.clone(),
2073 "GitLab URL with missing path elements".to_string(),
2074 ));
2075 }
2076
2077 path_elements.pop();
2078
2079 let proj = path_elements.join("/");
2080 let proj_segment = utf8_percent_encode(proj.as_str(), &DEFAULT_ASCII_SET);
2081 let api_url = Url::parse(&format!(
2082 "https://{}/api/v4/projects/{}",
2083 host, proj_segment
2084 ))
2085 .map_err(|_| {
2086 CanonicalizeError::InvalidUrl(
2087 url.clone(),
2088 "GitLab URL with invalid project path".to_string(),
2089 )
2090 })?;
2091 match load_json_url(&api_url, None).await {
2092 Ok(data) => {
2093 let issues_enabled = data
2096 .get("issues_enabled")
2097 .unwrap_or(&serde_json::Value::Null);
2098 if issues_enabled.as_bool() == Some(false) {
2099 return Err(CanonicalizeError::InvalidUrl(
2100 url.clone(),
2101 "Project does not have issues enabled".to_string(),
2102 ));
2103 }
2104
2105 let mut canonical_url = Url::parse(data["web_url"].as_str().unwrap()).unwrap();
2106 canonical_url
2107 .path_segments_mut()
2108 .unwrap()
2109 .extend(&["-", "issues"]);
2110 if issues_enabled.as_bool() == Some(true) {
2111 return Ok(canonical_url);
2112 }
2113
2114 check_url_canonical(&canonical_url).await
2115 }
2116 Err(HTTPJSONError::Error { status, .. })
2117 if status == reqwest::StatusCode::NOT_FOUND =>
2118 {
2119 Err(CanonicalizeError::InvalidUrl(
2120 url.clone(),
2121 "Project not found".to_string(),
2122 ))
2123 }
2124 Err(e) => Err(CanonicalizeError::Unverifiable(
2125 url.clone(),
2126 format!("Unable to verify bug database URL: {:?}", e),
2127 )),
2128 }
2129 }
2130
2131 async fn check_bug_submit_url_canonical(&self, url: &Url) -> Result<Url, CanonicalizeError> {
2132 let path_elements = url
2133 .path_segments()
2134 .expect("valid segments")
2135 .collect::<Vec<_>>();
2136 if path_elements.len() < 2 || path_elements[path_elements.len() - 2] != "issues" {
2137 return Err(CanonicalizeError::InvalidUrl(
2138 url.clone(),
2139 "GitLab URL with missing path elements".to_string(),
2140 ));
2141 }
2142
2143 if path_elements[path_elements.len() - 1] != "new" {
2144 return Err(CanonicalizeError::InvalidUrl(
2145 url.clone(),
2146 "GitLab URL with missing path elements".to_string(),
2147 ));
2148 }
2149
2150 let db_url = with_path_segments(url, &path_elements[0..path_elements.len() - 1]).unwrap();
2151 let mut canonical_db_url = self.check_bug_database_canonical(&db_url).await?;
2152 canonical_db_url
2153 .path_segments_mut()
2154 .expect("valid segments")
2155 .push("new");
2156 Ok(canonical_db_url)
2157 }
2158
2159 fn bug_database_from_issue_url(&self, url: &Url) -> Option<Url> {
2160 let path_elements = url
2161 .path_segments()
2162 .expect("valid segments")
2163 .collect::<Vec<_>>();
2164 if path_elements.len() < 2
2165 || path_elements[path_elements.len() - 2] != "issues"
2166 || path_elements[path_elements.len() - 1]
2167 .parse::<u32>()
2168 .is_err()
2169 {
2170 return None;
2171 }
2172 Some(with_path_segments(url, &path_elements[0..path_elements.len() - 1]).unwrap())
2173 }
2174
2175 fn bug_database_url_from_repo_url(&self, url: &Url) -> Option<Url> {
2176 let mut url = url.clone();
2177 let last = url
2178 .path_segments()
2179 .expect("valid segments")
2180 .next_back()
2181 .unwrap()
2182 .to_string();
2183 url.path_segments_mut()
2184 .unwrap()
2185 .pop()
2186 .push(last.trim_end_matches(".git"))
2187 .push("issues");
2188 Some(url)
2189 }
2190
2191 fn repo_url_from_merge_request_url(&self, url: &Url) -> Option<Url> {
2192 let path_elements = url
2193 .path_segments()
2194 .expect("path segments")
2195 .collect::<Vec<_>>();
2196 if path_elements.len() < 3
2197 || path_elements[path_elements.len() - 2] != "merge_requests"
2198 || path_elements[path_elements.len() - 1]
2199 .parse::<u32>()
2200 .is_err()
2201 {
2202 return None;
2203 }
2204 Some(with_path_segments(url, &path_elements[0..path_elements.len() - 2]).unwrap())
2205 }
2206}
2207
2208pub fn guess_from_travis_yml(
2210 path: &Path,
2211 _settings: &GuesserSettings,
2212) -> std::result::Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
2213 let mut file = File::open(path)?;
2214
2215 let mut contents = String::new();
2216 file.read_to_string(&mut contents)?;
2217
2218 let data: serde_norway::Value =
2219 serde_norway::from_str(&contents).map_err(|e| ProviderError::ParseError(e.to_string()))?;
2220
2221 let mut ret = Vec::new();
2222
2223 if let Some(go_import_path) = data.get("go_import_path") {
2224 if let Some(go_import_path) = go_import_path.as_str() {
2225 ret.push(UpstreamDatumWithMetadata {
2226 datum: UpstreamDatum::GoImportPath(go_import_path.to_string()),
2227 certainty: Some(Certainty::Certain),
2228 origin: Some(path.into()),
2229 });
2230 }
2231 }
2232
2233 Ok(ret)
2234}
2235
2236pub fn guess_from_environment() -> std::result::Result<Vec<UpstreamDatumWithMetadata>, ProviderError>
2238{
2239 let mut results = Vec::new();
2240 if let Ok(url) = std::env::var("UPSTREAM_BRANCH_URL") {
2241 results.push(UpstreamDatumWithMetadata {
2242 datum: UpstreamDatum::Repository(url),
2243 certainty: Some(Certainty::Certain),
2244 origin: Some(Origin::Other("environment".to_string())),
2245 });
2246 }
2247 Ok(results)
2248}
2249
2250fn find_datum<'a>(
2251 metadata: &'a [UpstreamDatumWithMetadata],
2252 field: &str,
2253) -> Option<&'a UpstreamDatumWithMetadata> {
2254 metadata.iter().find(|d| d.datum.field() == field)
2255}
2256
2257fn set_datum(metadata: &mut Vec<UpstreamDatumWithMetadata>, datum: UpstreamDatumWithMetadata) {
2258 if let Some(idx) = metadata
2259 .iter()
2260 .position(|d| d.datum.field() == datum.datum.field())
2261 {
2262 metadata[idx] = datum;
2263 } else {
2264 metadata.push(datum);
2265 }
2266}
2267
2268pub fn update_from_guesses(
2270 metadata: &mut Vec<UpstreamDatumWithMetadata>,
2271 new_items: impl Iterator<Item = UpstreamDatumWithMetadata>,
2272) -> Vec<UpstreamDatumWithMetadata> {
2273 let mut changed = vec![];
2274 for datum in new_items {
2275 let current_datum = find_datum(metadata, datum.datum.field());
2276 if current_datum.is_none() || datum.certainty > current_datum.unwrap().certainty {
2277 changed.push(datum.clone());
2278 set_datum(metadata, datum);
2279 }
2280 }
2281 changed
2282}
2283
2284fn possible_fields_missing(
2285 upstream_metadata: &[UpstreamDatumWithMetadata],
2286 fields: &[&str],
2287 _field_certainty: Certainty,
2288) -> bool {
2289 for field in fields {
2290 match find_datum(upstream_metadata, field) {
2291 Some(datum) if datum.certainty != Some(Certainty::Certain) => return true,
2292 None => return true,
2293 _ => (),
2294 }
2295 }
2296 false
2297}
2298
2299async fn extend_from_external_guesser<
2300 F: Fn() -> Fut,
2301 Fut: std::future::Future<Output = Vec<UpstreamDatum>>,
2302>(
2303 metadata: &mut Vec<UpstreamDatumWithMetadata>,
2304 max_certainty: Option<Certainty>,
2305 supported_fields: &[&str],
2306 new_items: F,
2307) {
2308 if max_certainty.is_some()
2309 && !possible_fields_missing(metadata, supported_fields, max_certainty.unwrap())
2310 {
2311 return;
2312 }
2313
2314 let new_items = new_items()
2315 .await
2316 .into_iter()
2317 .map(|item| UpstreamDatumWithMetadata {
2318 datum: item,
2319 certainty: max_certainty,
2320 origin: None,
2321 });
2322
2323 update_from_guesses(metadata, new_items);
2324}
2325
2326pub struct SourceForge;
2328
2329impl Default for SourceForge {
2330 fn default() -> Self {
2331 Self::new()
2332 }
2333}
2334
2335impl SourceForge {
2336 pub fn new() -> Self {
2338 Self
2339 }
2340}
2341
2342#[async_trait::async_trait]
2343impl Forge for SourceForge {
2344 fn name(&self) -> &'static str {
2345 "SourceForge"
2346 }
2347 fn repository_browse_can_be_homepage(&self) -> bool {
2348 false
2349 }
2350
2351 fn bug_database_url_from_bug_submit_url(&self, url: &Url) -> Option<Url> {
2352 let mut segments = url.path_segments()?;
2353 if segments.next() != Some("p") {
2354 return None;
2355 }
2356 let project = segments.next()?;
2357 if segments.next() != Some("bugs") {
2358 return None;
2359 }
2360 with_path_segments(url, &["p", project, "bugs"]).ok()
2361 }
2362
2363 async fn extend_metadata(
2364 &self,
2365 metadata: &mut Vec<UpstreamDatumWithMetadata>,
2366 project: &str,
2367 max_certainty: Option<Certainty>,
2368 ) {
2369 let subproject = find_datum(metadata, "Name").and_then(|f| match f.datum {
2370 UpstreamDatum::Name(ref name) => Some(name.to_string()),
2371 _ => None,
2372 });
2373
2374 extend_from_external_guesser(
2375 metadata,
2376 max_certainty,
2377 &["Homepage", "Name", "Repository", "Bug-Database"],
2378 || async {
2379 crate::forges::sourceforge::guess_from_sf(project, subproject.as_deref()).await
2380 },
2381 )
2382 .await
2383 }
2384}
2385
2386pub struct Launchpad;
2388
2389impl Default for Launchpad {
2390 fn default() -> Self {
2391 Self::new()
2392 }
2393}
2394
2395impl Launchpad {
2396 pub fn new() -> Self {
2398 Self
2399 }
2400}
2401
2402impl Forge for Launchpad {
2403 fn name(&self) -> &'static str {
2404 "launchpad"
2405 }
2406
2407 fn repository_browse_can_be_homepage(&self) -> bool {
2408 false
2409 }
2410 fn bug_database_url_from_bug_submit_url(&self, url: &Url) -> Option<Url> {
2411 if url.host_str()? != "bugs.launchpad.net" {
2412 return None;
2413 }
2414
2415 let mut segments = url.path_segments()?;
2416 let project = segments.next()?;
2417
2418 with_path_segments(url, &[project]).ok()
2419 }
2420
2421 fn bug_submit_url_from_bug_database_url(&self, url: &Url) -> Option<Url> {
2422 if url.host_str()? != "bugs.launchpad.net" {
2423 return None;
2424 }
2425
2426 let mut segments = url.path_segments()?;
2427 let project = segments.next()?;
2428
2429 with_path_segments(url, &[project, "+filebug"]).ok()
2430 }
2431}
2432
2433pub async fn find_forge(url: &Url, net_access: Option<bool>) -> Option<Box<dyn Forge>> {
2435 if url.host_str()? == "sourceforge.net" {
2436 return Some(Box::new(SourceForge::new()));
2437 }
2438
2439 if url.host_str()?.ends_with(".launchpad.net") {
2440 return Some(Box::new(Launchpad::new()));
2441 }
2442
2443 if url.host_str()? == "github.com" {
2444 return Some(Box::new(GitHub::new()));
2445 }
2446
2447 if vcs::is_gitlab_site(url.host_str()?, net_access).await {
2448 return Some(Box::new(GitLab::new()));
2449 }
2450
2451 None
2452}
2453
2454pub async fn check_bug_database_canonical(
2456 url: &Url,
2457 net_access: Option<bool>,
2458) -> Result<Url, CanonicalizeError> {
2459 if let Some(forge) = find_forge(url, net_access).await {
2460 forge
2461 .bug_database_url_from_bug_submit_url(url)
2462 .ok_or(CanonicalizeError::Unverifiable(
2463 url.clone(),
2464 "no bug database URL found".to_string(),
2465 ))
2466 } else {
2467 Err(CanonicalizeError::Unverifiable(
2468 url.clone(),
2469 "unknown forge".to_string(),
2470 ))
2471 }
2472}
2473
2474pub async fn bug_submit_url_from_bug_database_url(
2476 url: &Url,
2477 net_access: Option<bool>,
2478) -> Option<Url> {
2479 if let Some(forge) = find_forge(url, net_access).await {
2480 forge.bug_submit_url_from_bug_database_url(url)
2481 } else {
2482 None
2483 }
2484}
2485
2486pub async fn bug_database_url_from_bug_submit_url(
2488 url: &Url,
2489 net_access: Option<bool>,
2490) -> Option<Url> {
2491 if let Some(forge) = find_forge(url, net_access).await {
2492 forge.bug_database_url_from_bug_submit_url(url)
2493 } else {
2494 None
2495 }
2496}
2497
2498pub async fn guess_bug_database_url_from_repo_url(
2500 url: &Url,
2501 net_access: Option<bool>,
2502) -> Option<Url> {
2503 if let Some(forge) = find_forge(url, net_access).await {
2504 forge.bug_database_url_from_repo_url(url)
2505 } else {
2506 None
2507 }
2508}
2509
2510pub async fn repo_url_from_merge_request_url(url: &Url, net_access: Option<bool>) -> Option<Url> {
2512 if let Some(forge) = find_forge(url, net_access).await {
2513 forge.repo_url_from_merge_request_url(url)
2514 } else {
2515 None
2516 }
2517}
2518
2519pub async fn bug_database_from_issue_url(url: &Url, net_access: Option<bool>) -> Option<Url> {
2521 if let Some(forge) = find_forge(url, net_access).await {
2522 forge.bug_database_from_issue_url(url)
2523 } else {
2524 None
2525 }
2526}
2527
2528pub async fn check_bug_submit_url_canonical(
2530 url: &Url,
2531 net_access: Option<bool>,
2532) -> Result<Url, CanonicalizeError> {
2533 if let Some(forge) = find_forge(url, net_access).await {
2534 forge
2535 .bug_submit_url_from_bug_database_url(url)
2536 .ok_or(CanonicalizeError::Unverifiable(
2537 url.clone(),
2538 "no bug submit URL found".to_string(),
2539 ))
2540 } else {
2541 Err(CanonicalizeError::Unverifiable(
2542 url.clone(),
2543 "unknown forge".to_string(),
2544 ))
2545 }
2546}
2547
2548pub fn extract_pecl_package_name(url: &str) -> Option<String> {
2550 let pecl_regex = regex!(r"https?://pecl\.php\.net/package/(.*)");
2551 if let Some(captures) = pecl_regex.captures(url) {
2552 return captures.get(1).map(|m| m.as_str().to_string());
2553 }
2554 None
2555}
2556
2557pub fn extract_hackage_package(url: &str) -> Option<String> {
2559 let hackage_regex = regex!(r"https?://hackage\.haskell\.org/package/([^/]+)/.*");
2560 if let Some(captures) = hackage_regex.captures(url) {
2561 return captures.get(1).map(|m| m.as_str().to_string());
2562 }
2563 None
2564}
2565
2566pub fn metadata_from_url(url: &str, origin: &Origin) -> Vec<UpstreamDatumWithMetadata> {
2568 let mut results = Vec::new();
2569 if let Some(sf_project) = crate::forges::sourceforge::extract_sf_project_name(url) {
2570 results.push(UpstreamDatumWithMetadata {
2571 datum: UpstreamDatum::SourceForgeProject(sf_project),
2572 certainty: Some(Certainty::Certain),
2573 origin: Some(origin.clone()),
2574 });
2575 results.push(UpstreamDatumWithMetadata {
2576 datum: UpstreamDatum::Archive("SourceForge".to_string()),
2577 certainty: Some(Certainty::Certain),
2578 origin: Some(origin.clone()),
2579 });
2580 }
2581
2582 if let Some(pecl_package) = extract_pecl_package_name(url) {
2583 results.push(UpstreamDatumWithMetadata {
2584 datum: UpstreamDatum::PeclPackage(pecl_package),
2585 certainty: Some(Certainty::Certain),
2586 origin: Some(origin.clone()),
2587 });
2588 results.push(UpstreamDatumWithMetadata {
2589 datum: UpstreamDatum::Archive("Pecl".to_string()),
2590 certainty: Some(Certainty::Certain),
2591 origin: Some(origin.clone()),
2592 });
2593 }
2594
2595 if let Some(haskell_package) = extract_hackage_package(url) {
2596 results.push(UpstreamDatumWithMetadata {
2597 datum: UpstreamDatum::HaskellPackage(haskell_package),
2598 certainty: Some(Certainty::Certain),
2599 origin: Some(origin.clone()),
2600 });
2601 results.push(UpstreamDatumWithMetadata {
2602 datum: UpstreamDatum::Archive("Hackage".to_string()),
2603 certainty: Some(Certainty::Certain),
2604 origin: Some(origin.clone()),
2605 });
2606 }
2607 results
2608}
2609
2610pub async fn get_repology_metadata(srcname: &str, repo: Option<&str>) -> Option<serde_json::Value> {
2612 let repo = repo.unwrap_or("debian_unstable");
2613 let url = format!(
2614 "https://repology.org/tools/project-by?repo={}&name_type=srcname'
2615 '&target_page=api_v1_project&name={}",
2616 repo, srcname
2617 );
2618
2619 match load_json_url(&Url::parse(url.as_str()).unwrap(), None).await {
2620 Ok(json) => Some(json),
2621 Err(HTTPJSONError::Error { status: 404, .. }) => None,
2622 Err(e) => {
2623 debug!("Failed to load repology metadata: {:?}", e);
2624 None
2625 }
2626 }
2627}
2628
2629pub fn guess_from_path(
2631 path: &Path,
2632 _settings: &GuesserSettings,
2633) -> std::result::Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
2634 let basename = path.file_name().and_then(|s| s.to_str());
2635 let mut ret = Vec::new();
2636 if let Some(basename_str) = basename {
2637 let re = regex!(r"(.*)-([0-9.]+)");
2638 if let Some(captures) = re.captures(basename_str) {
2639 if let Some(name) = captures.get(1) {
2640 ret.push(UpstreamDatumWithMetadata {
2641 datum: UpstreamDatum::Name(name.as_str().to_string()),
2642 certainty: Some(Certainty::Possible),
2643 origin: Some(path.into()),
2644 });
2645 }
2646 if let Some(version) = captures.get(2) {
2647 ret.push(UpstreamDatumWithMetadata {
2648 datum: UpstreamDatum::Version(version.as_str().to_string()),
2649 certainty: Some(Certainty::Possible),
2650 origin: Some(path.into()),
2651 });
2652 }
2653 } else {
2654 ret.push(UpstreamDatumWithMetadata {
2655 datum: UpstreamDatum::Name(basename_str.to_string()),
2656 certainty: Some(Certainty::Possible),
2657 origin: Some(path.into()),
2658 });
2659 }
2660 }
2661 Ok(ret)
2662}
2663
2664#[cfg(feature = "pyo3")]
2665impl<'py> FromPyObject<'_, 'py> for UpstreamDatum {
2666 type Error = PyErr;
2667
2668 fn extract(obj: pyo3::Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
2669 let (field, val): (String, Bound<'py, PyAny>) = if let Ok((field, val)) =
2670 obj.extract::<(String, Bound<'py, PyAny>)>()
2671 {
2672 (field, val)
2673 } else if let Ok(datum) = obj.getattr("datum") {
2674 let field = datum.getattr("field")?.extract::<String>()?;
2675 let val = datum.getattr("value")?;
2676 (field, val)
2677 } else if obj.hasattr("field")? && obj.hasattr("value")? {
2678 let field = obj.getattr("field")?.extract::<String>()?;
2679 let val = obj.getattr("value")?;
2680 (field, val)
2681 } else {
2682 return Err(PyTypeError::new_err((
2683 format!("Expected a tuple of (field, value) or an object with field and value attributesm, found {:?}", obj),
2684 )));
2685 };
2686
2687 match field.as_str() {
2688 "Name" => Ok(UpstreamDatum::Name(val.extract::<String>()?)),
2689 "Version" => Ok(UpstreamDatum::Version(val.extract::<String>()?)),
2690 "Homepage" => Ok(UpstreamDatum::Homepage(val.extract::<String>()?)),
2691 "Bug-Database" => Ok(UpstreamDatum::BugDatabase(val.extract::<String>()?)),
2692 "Bug-Submit" => Ok(UpstreamDatum::BugSubmit(val.extract::<String>()?)),
2693 "Contact" => Ok(UpstreamDatum::Contact(val.extract::<String>()?)),
2694 "Repository" => Ok(UpstreamDatum::Repository(val.extract::<String>()?)),
2695 "Repository-Browse" => Ok(UpstreamDatum::RepositoryBrowse(val.extract::<String>()?)),
2696 "License" => Ok(UpstreamDatum::License(val.extract::<String>()?)),
2697 "Description" => Ok(UpstreamDatum::Description(val.extract::<String>()?)),
2698 "Summary" => Ok(UpstreamDatum::Summary(val.extract::<String>()?)),
2699 "Cargo-Crate" => Ok(UpstreamDatum::CargoCrate(val.extract::<String>()?)),
2700 "Security-MD" => Ok(UpstreamDatum::SecurityMD(val.extract::<String>()?)),
2701 "Security-Contact" => Ok(UpstreamDatum::SecurityContact(val.extract::<String>()?)),
2702 "Keywords" => Ok(UpstreamDatum::Keywords(val.extract::<Vec<String>>()?)),
2703 "Copyright" => Ok(UpstreamDatum::Copyright(val.extract::<String>()?)),
2704 "Documentation" => Ok(UpstreamDatum::Documentation(val.extract::<String>()?)),
2705 "API-Documentation" => Ok(UpstreamDatum::APIDocumentation(val.extract::<String>()?)),
2706 "Go-Import-Path" => Ok(UpstreamDatum::GoImportPath(val.extract::<String>()?)),
2707 "Download" => Ok(UpstreamDatum::Download(val.extract::<String>()?)),
2708 "Wiki" => Ok(UpstreamDatum::Wiki(val.extract::<String>()?)),
2709 "MailingList" => Ok(UpstreamDatum::MailingList(val.extract::<String>()?)),
2710 "Funding" => Ok(UpstreamDatum::Funding(val.extract::<String>()?)),
2711 "SourceForge-Project" => {
2712 Ok(UpstreamDatum::SourceForgeProject(val.extract::<String>()?))
2713 }
2714 "Archive" => Ok(UpstreamDatum::Archive(val.extract::<String>()?)),
2715 "Demo" => Ok(UpstreamDatum::Demo(val.extract::<String>()?)),
2716 "Pecl-Package" => Ok(UpstreamDatum::PeclPackage(val.extract::<String>()?)),
2717 "Haskell-Package" => Ok(UpstreamDatum::HaskellPackage(val.extract::<String>()?)),
2718 "Author" => Ok(UpstreamDatum::Author(val.extract::<Vec<Person>>()?)),
2719 "Maintainer" => Ok(UpstreamDatum::Maintainer(val.extract::<Person>()?)),
2720 "Changelog" => Ok(UpstreamDatum::Changelog(val.extract::<String>()?)),
2721 "Screenshots" => Ok(UpstreamDatum::Screenshots(val.extract::<Vec<String>>()?)),
2722 "Cite-As" => Ok(UpstreamDatum::CiteAs(val.extract::<String>()?)),
2723 "Registry" => {
2724 let v = val.extract::<Vec<Bound<'py, PyAny>>>()?;
2725 let mut registry = Vec::new();
2726 for item in v {
2727 let name = item.get_item("Name")?.extract::<String>()?;
2728 let entry = item.get_item("Entry")?.extract::<String>()?;
2729 registry.push((name, entry));
2730 }
2731 Ok(UpstreamDatum::Registry(registry))
2732 }
2733 "Donation" => Ok(UpstreamDatum::Donation(val.extract::<String>()?)),
2734 "Webservice" => Ok(UpstreamDatum::Webservice(val.extract::<String>()?)),
2735 "BuildSystem" => Ok(UpstreamDatum::BuildSystem(val.extract::<String>()?)),
2736 "FAQ" => Ok(UpstreamDatum::FAQ(val.extract::<String>()?)),
2737 _ => Err(PyRuntimeError::new_err(format!("Unknown field: {}", field))),
2738 }
2739 }
2740}
2741
2742#[cfg(feature = "pyo3")]
2743impl<'py> IntoPyObject<'py> for &UpstreamDatum {
2744 type Target = PyAny;
2745 type Output = Bound<'py, Self::Target>;
2746 type Error = PyErr;
2747
2748 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
2749 let field = self.field().to_string();
2750 let value: Bound<'py, PyAny> = match self {
2751 UpstreamDatum::Name(n) => n.into_pyobject(py)?.into_any(),
2752 UpstreamDatum::Version(v) => v.into_pyobject(py)?.into_any(),
2753 UpstreamDatum::Contact(c) => c.into_pyobject(py)?.into_any(),
2754 UpstreamDatum::Summary(s) => s.into_pyobject(py)?.into_any(),
2755 UpstreamDatum::License(l) => l.into_pyobject(py)?.into_any(),
2756 UpstreamDatum::Homepage(h) => h.into_pyobject(py)?.into_any(),
2757 UpstreamDatum::Description(d) => d.into_pyobject(py)?.into_any(),
2758 UpstreamDatum::BugDatabase(b) => b.into_pyobject(py)?.into_any(),
2759 UpstreamDatum::BugSubmit(b) => b.into_pyobject(py)?.into_any(),
2760 UpstreamDatum::Repository(r) => r.into_pyobject(py)?.into_any(),
2761 UpstreamDatum::RepositoryBrowse(r) => r.into_pyobject(py)?.into_any(),
2762 UpstreamDatum::SecurityMD(s) => s.into_pyobject(py)?.into_any(),
2763 UpstreamDatum::SecurityContact(s) => s.into_pyobject(py)?.into_any(),
2764 UpstreamDatum::CargoCrate(c) => c.into_pyobject(py)?.into_any(),
2765 UpstreamDatum::Keywords(ks) => ks.into_pyobject(py)?,
2766 UpstreamDatum::Copyright(c) => c.into_pyobject(py)?.into_any(),
2767 UpstreamDatum::Documentation(a) => a.into_pyobject(py)?.into_any(),
2768 UpstreamDatum::APIDocumentation(a) => a.into_pyobject(py)?.into_any(),
2769 UpstreamDatum::GoImportPath(ip) => ip.into_pyobject(py)?.into_any(),
2770 UpstreamDatum::Archive(a) => a.into_pyobject(py)?.into_any(),
2771 UpstreamDatum::Demo(d) => d.into_pyobject(py)?.into_any(),
2772 UpstreamDatum::Maintainer(m) => m.into_pyobject(py)?,
2773 UpstreamDatum::Author(a) => a.into_pyobject(py)?,
2774 UpstreamDatum::Wiki(w) => w.into_pyobject(py)?.into_any(),
2775 UpstreamDatum::Download(d) => d.into_pyobject(py)?.into_any(),
2776 UpstreamDatum::MailingList(m) => m.into_pyobject(py)?.into_any(),
2777 UpstreamDatum::SourceForgeProject(m) => m.into_pyobject(py)?.into_any(),
2778 UpstreamDatum::PeclPackage(p) => p.into_pyobject(py)?.into_any(),
2779 UpstreamDatum::Funding(p) => p.into_pyobject(py)?.into_any(),
2780 UpstreamDatum::Changelog(c) => c.into_pyobject(py)?.into_any(),
2781 UpstreamDatum::HaskellPackage(p) => p.into_pyobject(py)?.into_any(),
2782 UpstreamDatum::DebianITP(i) => i.into_pyobject(py)?.into_any(),
2783 UpstreamDatum::Screenshots(s) => s.into_pyobject(py)?,
2784 UpstreamDatum::CiteAs(s) => s.into_pyobject(py)?.into_any(),
2785 UpstreamDatum::Registry(r) => {
2786 let list: Result<Vec<_>, _> = r
2787 .iter()
2788 .map(|(name, entry)| {
2789 let dict = PyDict::new(py);
2790 dict.set_item("Name", name)?;
2791 dict.set_item("Entry", entry)?;
2792 Ok::<Bound<PyAny>, PyErr>(dict.into_any())
2793 })
2794 .collect();
2795 list?.into_pyobject(py)?
2796 }
2797 UpstreamDatum::Donation(d) => d.into_pyobject(py)?.into_any(),
2798 UpstreamDatum::Webservice(w) => w.into_pyobject(py)?.into_any(),
2799 UpstreamDatum::BuildSystem(b) => b.into_pyobject(py)?.into_any(),
2800 UpstreamDatum::FAQ(f) => f.into_pyobject(py)?.into_any(),
2801 };
2802 Ok((field, value).into_pyobject(py)?.into_any())
2803 }
2804}
2805
2806#[cfg(feature = "pyo3")]
2807impl<'py> FromPyObject<'_, 'py> for UpstreamDatumWithMetadata {
2808 type Error = PyErr;
2809
2810 fn extract(obj: pyo3::Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
2811 let certainty = obj.getattr("certainty")?.extract::<Option<String>>()?;
2812 let origin = obj.getattr("origin")?.extract::<Option<Origin>>()?;
2813 let datum = if obj.hasattr("datum")? {
2814 obj.getattr("datum")?.extract::<UpstreamDatum>()
2815 } else {
2816 obj.extract::<UpstreamDatum>()
2817 }?;
2818
2819 Ok(UpstreamDatumWithMetadata {
2820 datum,
2821 certainty: certainty.map(|s| s.parse().unwrap()),
2822 origin,
2823 })
2824 }
2825}
2826
2827#[derive(Debug)]
2829pub enum ProviderError {
2830 ParseError(String),
2832 IoError(std::io::Error),
2834 Other(String),
2836 HttpJsonError(Box<HTTPJSONError>),
2838 ExtrapolationLimitExceeded(usize),
2840}
2841
2842impl std::fmt::Display for ProviderError {
2843 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2844 match self {
2845 ProviderError::ParseError(e) => write!(f, "Parse error: {}", e),
2846 ProviderError::IoError(e) => write!(f, "IO error: {}", e),
2847 ProviderError::Other(e) => write!(f, "Other error: {}", e),
2848 ProviderError::HttpJsonError(e) => write!(f, "HTTP JSON error: {}", e),
2849 ProviderError::ExtrapolationLimitExceeded(e) => {
2850 write!(f, "Extrapolation limit exceeded: {}", e)
2851 }
2852 }
2853 }
2854}
2855
2856impl std::error::Error for ProviderError {}
2857
2858impl From<HTTPJSONError> for ProviderError {
2859 fn from(e: HTTPJSONError) -> Self {
2860 ProviderError::HttpJsonError(Box::new(e))
2861 }
2862}
2863
2864impl From<std::io::Error> for ProviderError {
2865 fn from(e: std::io::Error) -> Self {
2866 ProviderError::IoError(e)
2867 }
2868}
2869
2870impl From<reqwest::Error> for ProviderError {
2871 fn from(e: reqwest::Error) -> Self {
2872 ProviderError::Other(e.to_string())
2873 }
2874}
2875
2876#[cfg(feature = "pyo3")]
2877mod py_exceptions {
2878 #![allow(missing_docs)]
2879 pyo3::create_exception!(
2880 upstream_ontologist,
2881 ParseError,
2882 pyo3::exceptions::PyException
2883 );
2884}
2885#[cfg(feature = "pyo3")]
2886pub use py_exceptions::ParseError;
2887
2888#[cfg(feature = "pyo3")]
2889impl From<ProviderError> for PyErr {
2890 fn from(e: ProviderError) -> PyErr {
2891 match e {
2892 ProviderError::IoError(e) => e.into(),
2893 ProviderError::ParseError(e) => ParseError::new_err((e,)),
2894 ProviderError::Other(e) => PyRuntimeError::new_err((e,)),
2895 ProviderError::HttpJsonError(e) => PyRuntimeError::new_err((e.to_string(),)),
2896 ProviderError::ExtrapolationLimitExceeded(e) => {
2897 PyRuntimeError::new_err((e.to_string(),))
2898 }
2899 }
2900 }
2901}
2902
2903#[derive(Debug, Default, Clone)]
2905pub struct GuesserSettings {
2906 pub trust_package: bool,
2908}
2909
2910type GuesserFunction =
2911 Box<dyn FnOnce(&GuesserSettings) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError>>;
2912
2913pub struct UpstreamMetadataGuesser {
2915 pub name: std::path::PathBuf,
2917 pub guess: GuesserFunction,
2919}
2920
2921impl std::fmt::Debug for UpstreamMetadataGuesser {
2922 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2923 f.debug_struct("UpstreamMetadataGuesser")
2924 .field("name", &self.name)
2925 .finish()
2926 }
2927}
2928
2929type OldAsyncGuesser = fn(
2930 PathBuf,
2931 GuesserSettings,
2932) -> Pin<
2933 Box<
2934 dyn std::future::Future<Output = Result<Vec<UpstreamDatumWithMetadata>, ProviderError>>
2935 + Send,
2936 >,
2937>;
2938
2939const OLD_STATIC_GUESSERS: &[(&str, OldAsyncGuesser)] = &[
2940 #[cfg(feature = "debian")]
2941 ("debian/watch", |path, settings| {
2942 Box::pin(async move {
2943 crate::providers::debian::guess_from_debian_watch(&path, &settings).await
2944 })
2945 }),
2946 #[cfg(feature = "debian")]
2947 ("debian/control", |path, settings| {
2948 Box::pin(
2949 async move { crate::providers::debian::guess_from_debian_control(&path, &settings) },
2950 )
2951 }),
2952 #[cfg(feature = "debian")]
2953 ("debian/changelog", |path, settings| {
2954 Box::pin(async move {
2955 crate::providers::debian::guess_from_debian_changelog(&path, &settings).await
2956 })
2957 }),
2958 #[cfg(feature = "debian")]
2959 ("debian/rules", |path, settings| {
2960 Box::pin(async move { crate::providers::debian::guess_from_debian_rules(&path, &settings) })
2961 }),
2962 #[cfg(feature = "python-pkginfo")]
2963 ("PKG-INFO", |path, settings| {
2964 Box::pin(
2965 async move { crate::providers::python::guess_from_pkg_info(&path, &settings).await },
2966 )
2967 }),
2968 ("package.json", |path, settings| {
2969 Box::pin(async move {
2970 crate::providers::package_json::guess_from_package_json(&path, &settings)
2971 })
2972 }),
2973 ("composer.json", |path, settings| {
2974 Box::pin(async move {
2975 crate::providers::composer_json::guess_from_composer_json(&path, &settings)
2976 })
2977 }),
2978 ("package.xml", |path, settings| {
2979 Box::pin(
2980 async move { crate::providers::package_xml::guess_from_package_xml(&path, &settings) },
2981 )
2982 }),
2983 ("package.yaml", |path, settings| {
2984 Box::pin(async move {
2985 crate::providers::package_yaml::guess_from_package_yaml(&path, &settings)
2986 })
2987 }),
2988 #[cfg(feature = "dist-ini")]
2989 ("dist.ini", |path, settings| {
2990 Box::pin(async move { crate::providers::perl::guess_from_dist_ini(&path, &settings) })
2991 }),
2992 #[cfg(feature = "debian")]
2993 ("debian/copyright", |path, settings| {
2994 Box::pin(async move {
2995 crate::providers::debian::guess_from_debian_copyright(&path, &settings).await
2996 })
2997 }),
2998 ("META.json", |path, settings| {
2999 Box::pin(async move { crate::providers::perl::guess_from_meta_json(&path, &settings) })
3000 }),
3001 ("MYMETA.json", |path, settings| {
3002 Box::pin(async move { crate::providers::perl::guess_from_meta_json(&path, &settings) })
3003 }),
3004 ("META.yml", |path, settings| {
3005 Box::pin(async move { crate::providers::perl::guess_from_meta_yml(&path, &settings) })
3006 }),
3007 ("MYMETA.yml", |path, settings| {
3008 Box::pin(async move { crate::providers::perl::guess_from_meta_yml(&path, &settings) })
3009 }),
3010 ("configure", |path, settings| {
3011 Box::pin(async move { crate::providers::autoconf::guess_from_configure(&path, &settings) })
3012 }),
3013 #[cfg(feature = "r-description")]
3014 ("DESCRIPTION", |path, settings| {
3015 Box::pin(
3016 async move { crate::providers::r::guess_from_r_description(&path, &settings).await },
3017 )
3018 }),
3019 #[cfg(feature = "cargo")]
3020 ("Cargo.toml", |path, settings| {
3021 Box::pin(async move { crate::providers::rust::guess_from_cargo(&path, &settings) })
3022 }),
3023 ("pom.xml", |path, settings| {
3024 Box::pin(async move { crate::providers::maven::guess_from_pom_xml(&path, &settings) })
3025 }),
3026 #[cfg(feature = "git-config")]
3027 (".git/config", |path, settings| {
3028 Box::pin(async move { crate::providers::git::guess_from_git_config(&path, &settings) })
3029 }),
3030 ("debian/get-orig-source.sh", |path, settings| {
3031 Box::pin(async move { crate::vcs_command::guess_from_get_orig_source(&path, &settings) })
3032 }),
3033 #[cfg(feature = "pyproject-toml")]
3034 ("pyproject.toml", |path, settings| {
3035 Box::pin(
3036 async move { crate::providers::python::guess_from_pyproject_toml(&path, &settings) },
3037 )
3038 }),
3039 #[cfg(feature = "setup-cfg")]
3040 ("setup.cfg", |path, settings| {
3041 Box::pin(
3042 async move { crate::providers::python::guess_from_setup_cfg(&path, &settings).await },
3043 )
3044 }),
3045 ("go.mod", |path, settings| {
3046 Box::pin(async move { crate::providers::go::guess_from_go_mod(&path, &settings) })
3047 }),
3048 ("Makefile.PL", |path, settings| {
3049 Box::pin(async move { crate::providers::perl::guess_from_makefile_pl(&path, &settings) })
3050 }),
3051 ("wscript", |path, settings| {
3052 Box::pin(async move { crate::providers::waf::guess_from_wscript(&path, &settings) })
3053 }),
3054 ("AUTHORS", |path, settings| {
3055 Box::pin(async move { crate::providers::authors::guess_from_authors(&path, &settings) })
3056 }),
3057 ("INSTALL", |path, settings| {
3058 Box::pin(async move { crate::providers::guess_from_install(&path, &settings).await })
3059 }),
3060 ("pubspec.yaml", |path, settings| {
3061 Box::pin(
3062 async move { crate::providers::pubspec::guess_from_pubspec_yaml(&path, &settings) },
3063 )
3064 }),
3065 ("pubspec.yml", |path, settings| {
3066 Box::pin(
3067 async move { crate::providers::pubspec::guess_from_pubspec_yaml(&path, &settings) },
3068 )
3069 }),
3070 ("meson.build", |path, settings| {
3071 Box::pin(async move { crate::providers::meson::guess_from_meson(&path, &settings) })
3072 }),
3073 ("metadata.json", |path, settings| {
3074 Box::pin(async move {
3075 crate::providers::metadata_json::guess_from_metadata_json(&path, &settings)
3076 })
3077 }),
3078 (".travis.yml", |path, settings| {
3079 Box::pin(async move { crate::guess_from_travis_yml(&path, &settings) })
3080 }),
3081];
3082
3083fn find_guessers(path: &std::path::Path) -> Vec<Box<dyn Guesser>> {
3084 let mut candidates: Vec<Box<dyn Guesser>> = Vec::new();
3085
3086 let path = path.canonicalize().unwrap();
3087
3088 for (name, cb) in OLD_STATIC_GUESSERS {
3089 let subpath = path.join(name);
3090 if subpath.exists() {
3091 candidates.push(Box::new(PathGuesser {
3092 name: name.to_string(),
3093 subpath: subpath.clone(),
3094 cb: Box::new(move |p, s| Box::pin(cb(p.to_path_buf(), s.clone()))),
3095 }));
3096 }
3097 }
3098
3099 for name in ["SECURITY.md", ".github/SECURITY.md", "docs/SECURITY.md"].iter() {
3100 if path.join(name).exists() {
3101 let subpath = path.join(name);
3102 candidates.push(Box::new(PathGuesser {
3103 name: name.to_string(),
3104 subpath: subpath.clone(),
3105 cb: Box::new(|p, s| {
3106 let name = name.to_string();
3107 Box::pin(async move {
3108 crate::providers::security_md::guess_from_security_md(&name, &p, &s)
3109 })
3110 }),
3111 }));
3112 }
3113 }
3114
3115 #[cfg(any(feature = "python-pkginfo", feature = "pyo3"))]
3116 let mut found_pkg_info = path.join("PKG-INFO").exists();
3117 #[cfg(feature = "python-pkginfo")]
3118 for entry in std::fs::read_dir(&path).unwrap() {
3119 let entry = entry.unwrap();
3120 let filename = entry.file_name().to_string_lossy().to_string();
3121 if filename.ends_with(".egg-info") {
3122 candidates.push(Box::new(PathGuesser {
3123 name: format!("{}/PKG-INFO", filename),
3124 subpath: entry.path().join("PKG-INFO"),
3125 cb: Box::new(|p, s| {
3126 Box::pin(
3127 async move { crate::providers::python::guess_from_pkg_info(&p, &s).await },
3128 )
3129 }),
3130 }));
3131 found_pkg_info = true;
3132 } else if filename.ends_with(".dist-info") {
3133 candidates.push(Box::new(PathGuesser {
3134 name: format!("{}/METADATA", filename),
3135 subpath: entry.path().join("METADATA"),
3136 cb: Box::new(|p, s| {
3137 Box::pin(
3138 async move { crate::providers::python::guess_from_pkg_info(&p, &s).await },
3139 )
3140 }),
3141 }));
3142 found_pkg_info = true;
3143 }
3144 }
3145
3146 #[cfg(feature = "pyo3")]
3147 if !found_pkg_info && path.join("setup.py").exists() {
3148 candidates.push(Box::new(PathGuesser {
3149 name: "setup.py".to_string(),
3150 subpath: path.join("setup.py"),
3151 cb: Box::new(|path, s| {
3152 Box::pin(async move {
3153 crate::providers::python::guess_from_setup_py(&path, s.trust_package).await
3154 })
3155 }),
3156 }));
3157 }
3158
3159 for entry in std::fs::read_dir(&path).unwrap() {
3160 let entry = entry.unwrap();
3161
3162 if entry.file_name().to_string_lossy().ends_with(".gemspec") {
3163 candidates.push(Box::new(PathGuesser {
3164 name: entry.file_name().to_string_lossy().to_string(),
3165 subpath: entry.path(),
3166 cb: Box::new(|p, s| {
3167 Box::pin(
3168 async move { crate::providers::ruby::guess_from_gemspec(&p, &s).await },
3169 )
3170 }),
3171 }));
3172 }
3173 }
3174
3175 #[cfg(feature = "r-description")]
3177 for entry in std::fs::read_dir(&path).unwrap() {
3178 let entry = entry.unwrap();
3179 let path = entry.path();
3180
3181 if entry.file_type().unwrap().is_dir() {
3182 let description_name = format!("{}/DESCRIPTION", entry.file_name().to_string_lossy());
3183 if path.join(&description_name).exists() {
3184 candidates.push(Box::new(PathGuesser {
3185 name: description_name,
3186 subpath: path.join("DESCRIPTION"),
3187 cb: Box::new(|p, s| {
3188 Box::pin(async move {
3189 crate::providers::r::guess_from_r_description(&p, &s).await
3190 })
3191 }),
3192 }));
3193 }
3194 }
3195 }
3196
3197 let mut doap_filenames = std::fs::read_dir(&path)
3198 .unwrap()
3199 .filter_map(|entry| {
3200 let entry = entry.unwrap();
3201 let filename = entry.file_name().to_string_lossy().to_string();
3202 if filename.ends_with(".doap")
3203 || (filename.ends_with(".xml") && filename.starts_with("doap_XML_"))
3204 {
3205 Some(entry.file_name())
3206 } else {
3207 None
3208 }
3209 })
3210 .collect::<Vec<_>>();
3211
3212 if doap_filenames.len() == 1 {
3213 let doap_filename = doap_filenames.remove(0);
3214 candidates.push(Box::new(PathGuesser {
3215 name: doap_filename.to_string_lossy().to_string(),
3216 subpath: path.join(&doap_filename),
3217 cb: Box::new(|p, s| {
3218 Box::pin(
3219 async move { crate::providers::doap::guess_from_doap(&p, s.trust_package) },
3220 )
3221 }),
3222 }));
3223 } else if doap_filenames.len() > 1 {
3224 log::warn!(
3225 "Multiple DOAP files found: {:?}, ignoring all.",
3226 doap_filenames
3227 );
3228 }
3229
3230 let mut metainfo_filenames = std::fs::read_dir(&path)
3231 .unwrap()
3232 .filter_map(|entry| {
3233 let entry = entry.unwrap();
3234 if entry
3235 .file_name()
3236 .to_string_lossy()
3237 .ends_with(".metainfo.xml")
3238 {
3239 Some(entry.file_name())
3240 } else {
3241 None
3242 }
3243 })
3244 .collect::<Vec<_>>();
3245
3246 if metainfo_filenames.len() == 1 {
3247 let metainfo_filename = metainfo_filenames.remove(0);
3248 candidates.push(Box::new(PathGuesser {
3249 name: metainfo_filename.to_string_lossy().to_string(),
3250 subpath: path.join(&metainfo_filename),
3251 cb: Box::new(|p, s| {
3252 Box::pin(async move {
3253 crate::providers::metainfo::guess_from_metainfo(&p, s.trust_package)
3254 })
3255 }),
3256 }));
3257 } else if metainfo_filenames.len() > 1 {
3258 log::warn!(
3259 "Multiple metainfo files found: {:?}, ignoring all.",
3260 metainfo_filenames
3261 );
3262 }
3263
3264 let mut cabal_filenames = std::fs::read_dir(&path)
3265 .unwrap()
3266 .filter_map(|entry| {
3267 let entry = entry.unwrap();
3268 if entry.file_name().to_string_lossy().ends_with(".cabal") {
3269 Some(entry.file_name())
3270 } else {
3271 None
3272 }
3273 })
3274 .collect::<Vec<_>>();
3275
3276 if cabal_filenames.len() == 1 {
3277 let cabal_filename = cabal_filenames.remove(0);
3278 candidates.push(Box::new(PathGuesser {
3279 name: cabal_filename.to_string_lossy().to_string(),
3280 subpath: path.join(&cabal_filename),
3281 cb: Box::new(|path, s| {
3282 Box::pin(async move {
3283 crate::providers::haskell::guess_from_cabal(&path, s.trust_package)
3284 })
3285 }),
3286 }));
3287 } else if cabal_filenames.len() > 1 {
3288 log::warn!(
3289 "Multiple cabal files found: {:?}, ignoring all.",
3290 cabal_filenames
3291 );
3292 }
3293
3294 let readme_filenames = std::fs::read_dir(&path)
3295 .unwrap()
3296 .filter_map(|entry| {
3297 let entry = entry.unwrap();
3298 let filename = entry.file_name().to_string_lossy().to_string();
3299 if !(filename.to_lowercase().starts_with("readme")
3300 || filename.to_lowercase().starts_with("hacking")
3301 || filename.to_lowercase().starts_with("contributing"))
3302 {
3303 return None;
3304 }
3305
3306 if filename.ends_with('~') {
3307 return None;
3308 }
3309
3310 let extension = entry
3311 .path()
3312 .extension()
3313 .map(|s| s.to_string_lossy().to_string());
3314
3315 if extension.as_deref() == Some("html")
3316 || extension.as_deref() == Some("pdf")
3317 || extension.as_deref() == Some("xml")
3318 {
3319 return None;
3320 }
3321 Some(entry.file_name())
3322 })
3323 .collect::<Vec<_>>();
3324
3325 for filename in readme_filenames {
3326 candidates.push(Box::new(PathGuesser {
3327 name: filename.to_string_lossy().to_string(),
3328 subpath: path.join(&filename),
3329 cb: Box::new(|path, s| {
3330 Box::pin(
3331 async move { crate::readme::guess_from_readme(&path, s.trust_package).await },
3332 )
3333 }),
3334 }));
3335 }
3336
3337 let mut nuspec_filenames = std::fs::read_dir(&path)
3338 .unwrap()
3339 .filter_map(|entry| {
3340 let entry = entry.unwrap();
3341 if entry.file_name().to_string_lossy().ends_with(".nuspec") {
3342 Some(entry.file_name())
3343 } else {
3344 None
3345 }
3346 })
3347 .collect::<Vec<_>>();
3348
3349 if nuspec_filenames.len() == 1 {
3350 let nuspec_filename = nuspec_filenames.remove(0);
3351 candidates.push(Box::new(PathGuesser {
3352 name: nuspec_filename.to_string_lossy().to_string(),
3353 subpath: path.join(&nuspec_filename),
3354 cb: Box::new(|path, s| {
3355 Box::pin(async move {
3356 crate::providers::nuspec::guess_from_nuspec(&path, s.trust_package).await
3357 })
3358 }),
3359 }));
3360 } else if nuspec_filenames.len() > 1 {
3361 log::warn!(
3362 "Multiple nuspec files found: {:?}, ignoring all.",
3363 nuspec_filenames
3364 );
3365 }
3366
3367 #[cfg(feature = "opam")]
3368 let mut opam_filenames = std::fs::read_dir(&path)
3369 .unwrap()
3370 .filter_map(|entry| {
3371 let entry = entry.unwrap();
3372 if entry.file_name().to_string_lossy().ends_with(".opam") {
3373 Some(entry.file_name())
3374 } else {
3375 None
3376 }
3377 })
3378 .collect::<Vec<_>>();
3379
3380 #[cfg(feature = "opam")]
3381 match opam_filenames.len().cmp(&1) {
3382 Ordering::Equal => {
3383 let opam_filename = opam_filenames.remove(0);
3384 candidates.push(Box::new(PathGuesser {
3385 name: opam_filename.to_string_lossy().to_string(),
3386 subpath: path.join(&opam_filename),
3387 cb: Box::new(|path, s| {
3388 Box::pin(async move {
3389 crate::providers::ocaml::guess_from_opam(&path, s.trust_package)
3390 })
3391 }),
3392 }));
3393 }
3394 Ordering::Greater => {
3395 log::warn!(
3396 "Multiple opam files found: {:?}, ignoring all.",
3397 opam_filenames
3398 );
3399 }
3400 Ordering::Less => {}
3401 }
3402
3403 let debian_patches = match std::fs::read_dir(path.join("debian").join("patches")) {
3404 Ok(patches) => patches
3405 .filter_map(|entry| {
3406 let entry = entry.unwrap();
3407 if entry.file_name().to_string_lossy().ends_with(".patch") {
3408 Some(format!(
3409 "debian/patches/{}",
3410 entry.file_name().to_string_lossy()
3411 ))
3412 } else {
3413 None
3414 }
3415 })
3416 .collect::<Vec<_>>(),
3417 Err(_) => Vec::new(),
3418 };
3419
3420 for filename in debian_patches {
3421 candidates.push(Box::new(PathGuesser {
3422 name: filename.clone(),
3423 subpath: path.join(&filename),
3424 cb: Box::new(|path, s| {
3425 Box::pin(async move {
3426 crate::providers::debian::guess_from_debian_patch(&path, &s).await
3427 })
3428 }),
3429 }));
3430 }
3431
3432 candidates.push(Box::new(EnvironmentGuesser::new()));
3433 candidates.push(Box::new(PathGuesser {
3434 name: ".".to_string(),
3435 subpath: path.clone(),
3436 cb: Box::new(|p, s| Box::pin(async move { crate::guess_from_path(&p, &s) })),
3437 }));
3438
3439 candidates
3440}
3441
3442pub(crate) fn stream(
3443 path: &Path,
3444 config: &GuesserSettings,
3445 guessers: Vec<Box<dyn Guesser>>,
3446) -> impl Stream<Item = Result<UpstreamDatumWithMetadata, ProviderError>> {
3447 let abspath = std::env::current_dir().unwrap().join(path);
3449 let config = config.clone();
3450
3451 futures::stream::iter(guessers)
3453 .map(move |mut guesser| {
3454 let abspath = abspath.clone();
3455 let config = config.clone();
3456 let guesser_name = guesser.name().to_string();
3457
3458 async move {
3459 let results = match guesser.guess(&config).await {
3460 Ok(results) => results,
3461 Err(e) => return futures::stream::iter(vec![Err(e)]).boxed(),
3462 };
3463
3464 futures::stream::iter(results.into_iter().map(move |mut datum| {
3465 rewrite_upstream_datum(&guesser_name, &mut datum, &abspath);
3466 Ok(datum)
3467 }))
3468 .boxed()
3469 }
3470 })
3471 .buffered(10) .flatten()
3473}
3474
3475fn rewrite_upstream_datum(
3476 guesser_name: &str,
3477 datum: &mut UpstreamDatumWithMetadata,
3478 abspath: &std::path::Path,
3479) {
3480 log::trace!("{}: {:?}", guesser_name, datum);
3481 datum.origin = datum
3482 .origin
3483 .clone()
3484 .or(Some(Origin::Other(guesser_name.to_string())));
3485 if let Some(Origin::Path(p)) = datum.origin.as_ref() {
3486 if let Ok(suffix) = p.strip_prefix(abspath) {
3487 if suffix.to_str().unwrap().is_empty() {
3488 datum.origin = Some(Origin::Path(PathBuf::from_str(".").unwrap()));
3489 } else {
3490 datum.origin = Some(Origin::Path(PathBuf::from_str(".").unwrap().join(suffix)));
3491 }
3492 }
3493 }
3494}
3495
3496pub fn upstream_metadata_stream(
3498 path: &std::path::Path,
3499 trust_package: Option<bool>,
3500) -> impl Stream<Item = Result<UpstreamDatumWithMetadata, ProviderError>> {
3501 let trust_package = trust_package.unwrap_or(false);
3502
3503 let guessers = find_guessers(path);
3504
3505 stream(path, &GuesserSettings { trust_package }, guessers)
3506}
3507
3508pub async fn extend_upstream_metadata(
3510 upstream_metadata: &mut UpstreamMetadata,
3511 path: &std::path::Path,
3512 minimum_certainty: Option<Certainty>,
3513 net_access: Option<bool>,
3514 consult_external_directory: Option<bool>,
3515) -> Result<(), ProviderError> {
3516 let net_access = net_access.unwrap_or(false);
3517 let consult_external_directory = consult_external_directory.unwrap_or(false);
3518 let minimum_certainty = minimum_certainty.unwrap_or(Certainty::Confident);
3519
3520 for field in [
3522 "Homepage",
3523 "Bug-Database",
3524 "Bug-Submit",
3525 "Repository",
3526 "Repository-Browse",
3527 "Download",
3528 ] {
3529 let value = match upstream_metadata.get(field) {
3530 Some(value) => value,
3531 None => continue,
3532 };
3533
3534 if let Some(project) =
3535 crate::forges::sourceforge::extract_sf_project_name(value.datum.as_str().unwrap())
3536 {
3537 let certainty = Some(
3538 std::cmp::min(Some(Certainty::Likely), value.certainty)
3539 .unwrap_or(Certainty::Likely),
3540 );
3541 upstream_metadata.insert(UpstreamDatumWithMetadata {
3542 datum: UpstreamDatum::Archive("SourceForge".to_string()),
3543 certainty,
3544 origin: Some(Origin::Other(format!("derived from {}", field))),
3545 });
3546 upstream_metadata.insert(UpstreamDatumWithMetadata {
3547 datum: UpstreamDatum::SourceForgeProject(project),
3548 certainty,
3549 origin: Some(Origin::Other(format!("derived from {}", field))),
3550 });
3551 break;
3552 }
3553 }
3554
3555 let archive = upstream_metadata.get("Archive");
3556 if archive.is_some()
3557 && archive.unwrap().datum.as_str().unwrap() == "SourceForge"
3558 && upstream_metadata.contains_key("SourceForge-Project")
3559 && net_access
3560 {
3561 let sf_project = upstream_metadata
3562 .get("SourceForge-Project")
3563 .unwrap()
3564 .datum
3565 .as_str()
3566 .unwrap()
3567 .to_string();
3568 let sf_certainty = archive.unwrap().certainty;
3569 SourceForge::new()
3570 .extend_metadata(
3571 upstream_metadata.mut_items(),
3572 sf_project.as_str(),
3573 sf_certainty,
3574 )
3575 .await;
3576 }
3577
3578 let archive = upstream_metadata.get("Archive");
3579 if archive.is_some()
3580 && archive.unwrap().datum.as_str().unwrap() == "Hackage"
3581 && upstream_metadata.contains_key("Hackage-Package")
3582 && net_access
3583 {
3584 let hackage_package = upstream_metadata
3585 .get("Hackage-Package")
3586 .unwrap()
3587 .datum
3588 .as_str()
3589 .unwrap()
3590 .to_string();
3591 let hackage_certainty = archive.unwrap().certainty;
3592
3593 crate::providers::haskell::Hackage::new()
3594 .extend_metadata(
3595 upstream_metadata.mut_items(),
3596 hackage_package.as_str(),
3597 hackage_certainty,
3598 )
3599 .await
3600 .unwrap();
3601 }
3602
3603 let archive = upstream_metadata.get("Archive");
3604 #[cfg(feature = "cargo")]
3605 if archive.is_some()
3606 && archive.unwrap().datum.as_str().unwrap() == "crates.io"
3607 && upstream_metadata.contains_key("Cargo-Crate")
3608 && net_access
3609 {
3610 let cargo_crate = upstream_metadata
3611 .get("Cargo-Crate")
3612 .unwrap()
3613 .datum
3614 .as_str()
3615 .unwrap()
3616 .to_string();
3617 let crates_io_certainty = upstream_metadata.get("Archive").unwrap().certainty;
3618 crate::providers::rust::CratesIo::new()
3619 .extend_metadata(
3620 upstream_metadata.mut_items(),
3621 cargo_crate.as_str(),
3622 crates_io_certainty,
3623 )
3624 .await
3625 .unwrap();
3626 }
3627
3628 let archive = upstream_metadata.get("Archive");
3629 if archive.is_some()
3630 && archive.unwrap().datum.as_str().unwrap() == "Pecl"
3631 && upstream_metadata.contains_key("Pecl-Package")
3632 && net_access
3633 {
3634 let pecl_package = upstream_metadata
3635 .get("Pecl-Package")
3636 .unwrap()
3637 .datum
3638 .as_str()
3639 .unwrap()
3640 .to_string();
3641 let pecl_certainty = upstream_metadata.get("Archive").unwrap().certainty;
3642 crate::providers::php::Pecl::new()
3643 .extend_metadata(
3644 upstream_metadata.mut_items(),
3645 pecl_package.as_str(),
3646 pecl_certainty,
3647 )
3648 .await
3649 .unwrap();
3650 }
3651
3652 #[cfg(feature = "debian")]
3653 if net_access && consult_external_directory {
3654 let package = match debian_control::Control::from_file_relaxed(path.join("debian/control"))
3656 {
3657 Ok((control, _)) => control.source().and_then(|s| s.name()),
3658 Err(_) => None,
3659 };
3660
3661 if let Some(package) = package {
3662 #[cfg(feature = "launchpad")]
3663 extend_from_lp(
3664 upstream_metadata.mut_items(),
3665 minimum_certainty,
3666 package.as_str(),
3667 None,
3668 None,
3669 )
3670 .await;
3671 crate::providers::arch::Aur::new()
3672 .extend_metadata(
3673 upstream_metadata.mut_items(),
3674 package.as_str(),
3675 Some(minimum_certainty),
3676 )
3677 .await
3678 .unwrap();
3679 crate::providers::gobo::Gobo::new()
3680 .extend_metadata(
3681 upstream_metadata.mut_items(),
3682 package.as_str(),
3683 Some(minimum_certainty),
3684 )
3685 .await
3686 .unwrap();
3687 extend_from_repology(
3688 upstream_metadata.mut_items(),
3689 minimum_certainty,
3690 package.as_str(),
3691 )
3692 .await;
3693 }
3694 }
3695 crate::extrapolate::extrapolate_fields(upstream_metadata, net_access, None).await?;
3696 Ok(())
3697}
3698
3699#[async_trait::async_trait]
3701pub trait ThirdPartyRepository {
3702 fn name(&self) -> &'static str;
3704 fn supported_fields(&self) -> &'static [&'static str];
3706 fn max_supported_certainty(&self) -> Certainty;
3708
3709 async fn extend_metadata(
3711 &self,
3712 metadata: &mut Vec<UpstreamDatumWithMetadata>,
3713 name: &str,
3714 min_certainty: Option<Certainty>,
3715 ) -> Result<(), ProviderError> {
3716 if min_certainty.is_some() && min_certainty.unwrap() > self.max_supported_certainty() {
3717 return Ok(());
3719 }
3720
3721 extend_from_external_guesser(
3722 metadata,
3723 Some(self.max_supported_certainty()),
3724 self.supported_fields(),
3725 || async { self.guess_metadata(name).await.unwrap() },
3726 )
3727 .await;
3728
3729 Ok(())
3730 }
3731
3732 async fn guess_metadata(&self, name: &str) -> Result<Vec<UpstreamDatum>, ProviderError>;
3734}
3735
3736#[cfg(feature = "launchpad")]
3737async fn extend_from_lp(
3738 upstream_metadata: &mut Vec<UpstreamDatumWithMetadata>,
3739 minimum_certainty: Certainty,
3740 package: &str,
3741 distribution: Option<&str>,
3742 suite: Option<&str>,
3743) {
3744 let lp_fields = &["Homepage", "Repository", "Name", "Download"][..];
3746 let lp_certainty = Certainty::Possible;
3747
3748 if lp_certainty < minimum_certainty {
3749 return;
3752 }
3753
3754 extend_from_external_guesser(upstream_metadata, Some(lp_certainty), lp_fields, || async {
3755 crate::providers::launchpad::guess_from_launchpad(package, distribution, suite)
3756 .await
3757 .unwrap()
3758 })
3759 .await
3760}
3761
3762async fn extend_from_repology(
3763 upstream_metadata: &mut Vec<UpstreamDatumWithMetadata>,
3764 minimum_certainty: Certainty,
3765 source_package: &str,
3766) {
3767 let repology_fields = &["Homepage", "License", "Summary", "Download"][..];
3769 let certainty = Certainty::Confident;
3770
3771 if certainty < minimum_certainty {
3772 return;
3774 }
3775
3776 extend_from_external_guesser(
3777 upstream_metadata,
3778 Some(certainty),
3779 repology_fields,
3780 || async {
3781 crate::providers::repology::guess_from_repology(source_package)
3782 .await
3783 .unwrap()
3784 },
3785 )
3786 .await
3787}
3788
3789pub async fn fix_upstream_metadata(upstream_metadata: &mut UpstreamMetadata) {
3791 if let Some(repository) = upstream_metadata.get_mut("Repository") {
3792 if let Some(repo_str) = repository.datum.as_str() {
3793 let url = crate::vcs::sanitize_url(repo_str).await;
3794 repository.datum = UpstreamDatum::Repository(url.to_string());
3795 }
3796 }
3797
3798 if let Some(summary) = upstream_metadata.get_mut("Summary") {
3799 if let Some(s) = summary.datum.as_str() {
3800 let s = s.split_once(". ").map_or(s, |(a, _)| a);
3801 let s = s.trim_end().trim_end_matches('.');
3802 summary.datum = UpstreamDatum::Summary(s.to_string());
3803 }
3804 }
3805}
3806
3807pub async fn summarize_upstream_metadata(
3816 metadata_items: impl Stream<Item = UpstreamDatumWithMetadata>,
3817 path: &std::path::Path,
3818 net_access: Option<bool>,
3819 consult_external_directory: Option<bool>,
3820 check: Option<bool>,
3821) -> Result<UpstreamMetadata, ProviderError> {
3822 let check = check.unwrap_or(false);
3823 let mut upstream_metadata = UpstreamMetadata::new();
3824
3825 let metadata_items = metadata_items.filter_map(|item| async move {
3826 let bad: bool = item.datum.known_bad_guess();
3827 if bad {
3828 log::debug!("Excluding known bad item {:?}", item);
3829 None
3830 } else {
3831 Some(item)
3832 }
3833 });
3834
3835 let metadata_items = metadata_items.collect::<Vec<_>>().await;
3836
3837 upstream_metadata.update(metadata_items.into_iter());
3838
3839 extend_upstream_metadata(
3840 &mut upstream_metadata,
3841 path,
3842 None,
3843 net_access,
3844 consult_external_directory,
3845 )
3846 .await?;
3847
3848 if check {
3849 check_upstream_metadata(&mut upstream_metadata, None).await;
3850 }
3851
3852 fix_upstream_metadata(&mut upstream_metadata).await;
3853
3854 upstream_metadata.sort();
3856
3857 Ok(upstream_metadata)
3858}
3859
3860pub fn guess_upstream_metadata_items(
3867 path: &std::path::Path,
3868 trust_package: Option<bool>,
3869 minimum_certainty: Option<Certainty>,
3870) -> impl Stream<Item = Result<UpstreamDatumWithMetadata, ProviderError>> {
3871 let items = upstream_metadata_stream(path, trust_package);
3872
3873 items.filter_map(move |e| async move {
3874 match e {
3875 Err(e) => Some(Err(e)),
3876 Ok(UpstreamDatumWithMetadata {
3877 datum,
3878 certainty,
3879 origin,
3880 }) => {
3881 if minimum_certainty.is_some() && certainty < minimum_certainty {
3882 None
3883 } else {
3884 Some(Ok(UpstreamDatumWithMetadata {
3885 datum,
3886 certainty,
3887 origin,
3888 }))
3889 }
3890 }
3891 }
3892 })
3893}
3894
3895pub async fn get_upstream_info(
3897 path: &std::path::Path,
3898 trust_package: Option<bool>,
3899 net_access: Option<bool>,
3900 consult_external_directory: Option<bool>,
3901 check: Option<bool>,
3902) -> Result<UpstreamMetadata, ProviderError> {
3903 let metadata_items = upstream_metadata_stream(path, trust_package);
3904
3905 let metadata_items = metadata_items.filter_map(|x| async {
3906 match x {
3907 Ok(x) => Some(x),
3908 Err(e) => {
3909 log::error!("{}", e);
3910 None
3911 }
3912 }
3913 });
3914
3915 summarize_upstream_metadata(
3916 metadata_items,
3917 path,
3918 net_access,
3919 consult_external_directory,
3920 check,
3921 )
3922 .await
3923}
3924
3925pub async fn guess_upstream_metadata(
3933 path: &std::path::Path,
3934 trust_package: Option<bool>,
3935 net_access: Option<bool>,
3936 consult_external_directory: Option<bool>,
3937 check: Option<bool>,
3938) -> Result<UpstreamMetadata, ProviderError> {
3939 let metadata_items = guess_upstream_metadata_items(path, trust_package, None);
3940
3941 let metadata_items = metadata_items.filter_map(|x| async {
3942 match x {
3943 Ok(x) => Some(x),
3944 Err(e) => {
3945 log::error!("{}", e);
3946 None
3947 }
3948 }
3949 });
3950 summarize_upstream_metadata(
3951 metadata_items,
3952 path,
3953 net_access,
3954 consult_external_directory,
3955 check,
3956 )
3957 .await
3958}
3959
3960pub async fn verify_screenshots(urls: &[&str]) -> Vec<(String, Option<bool>)> {
3962 let mut ret = Vec::new();
3963 for url in urls {
3964 let mut request = reqwest::Request::new(reqwest::Method::GET, url.parse().unwrap());
3965 request.headers_mut().insert(
3966 reqwest::header::USER_AGENT,
3967 reqwest::header::HeaderValue::from_static(USER_AGENT),
3968 );
3969
3970 match reqwest::Client::new().execute(request).await {
3971 Ok(response) => {
3972 let status = response.status();
3973 if status.is_success() {
3974 ret.push((url.to_string(), Some(true)));
3975 } else if status.is_client_error() {
3976 ret.push((url.to_string(), Some(false)));
3977 } else {
3978 ret.push((url.to_string(), None));
3979 }
3980 }
3981 Err(e) => {
3982 log::debug!("Error fetching {}: {}", url, e);
3983 ret.push((url.to_string(), None));
3984 }
3985 }
3986 }
3987
3988 ret
3989}
3990
3991pub async fn check_upstream_metadata(
3995 upstream_metadata: &mut UpstreamMetadata,
3996 version: Option<&str>,
3997) {
3998 let repository = upstream_metadata.get_mut("Repository");
3999 if let Some(repository) = repository {
4000 if let Some(repo_url) = repository.datum.to_url() {
4001 match vcs::check_repository_url_canonical(repo_url, version).await {
4002 Ok(canonical_url) => {
4003 repository.datum = UpstreamDatum::Repository(canonical_url.to_string());
4004 if repository.certainty == Some(Certainty::Confident) {
4005 repository.certainty = Some(Certainty::Certain);
4006 }
4007 if let Some(url) = repository.datum.to_url() {
4008 let derived_browse_url = vcs::browse_url_from_repo_url(
4009 &vcs::VcsLocation {
4010 url,
4011 branch: None,
4012 subpath: None,
4013 },
4014 Some(true),
4015 )
4016 .await;
4017 let certainty = repository.certainty;
4018 if let Some(browse_repo) = upstream_metadata.get_mut("Repository-Browse") {
4019 if derived_browse_url == browse_repo.datum.to_url() {
4020 browse_repo.certainty = certainty;
4021 }
4022 }
4023 }
4024 }
4025 Err(CanonicalizeError::Unverifiable(u, _))
4026 | Err(CanonicalizeError::RateLimited(u)) => {
4027 log::debug!("Unverifiable URL: {}", u);
4028 }
4029 Err(CanonicalizeError::InvalidUrl(u, e)) => {
4030 log::debug!("Deleting invalid Repository URL {}: {}", u, e);
4031 upstream_metadata.remove("Repository");
4032 }
4033 }
4034 } else {
4035 log::debug!("Repository field is not a valid URL, skipping check");
4036 }
4037 }
4038 let homepage = upstream_metadata.get_mut("Homepage");
4039 if let Some(homepage) = homepage {
4040 if let Some(homepage_url) = homepage.datum.to_url() {
4041 match check_url_canonical(&homepage_url).await {
4042 Ok(canonical_url) => {
4043 homepage.datum = UpstreamDatum::Homepage(canonical_url.to_string());
4044 if homepage.certainty >= Some(Certainty::Likely) {
4045 homepage.certainty = Some(Certainty::Certain);
4046 }
4047 }
4048 Err(CanonicalizeError::Unverifiable(u, _))
4049 | Err(CanonicalizeError::RateLimited(u)) => {
4050 log::debug!("Unverifiable URL: {}", u);
4051 }
4052 Err(CanonicalizeError::InvalidUrl(u, e)) => {
4053 log::debug!("Deleting invalid Homepage URL {}: {}", u, e);
4054 upstream_metadata.remove("Homepage");
4055 }
4056 }
4057 } else {
4058 log::debug!("Homepage field is not a valid URL, skipping check");
4059 }
4060 }
4061 if let Some(repository_browse) = upstream_metadata.get_mut("Repository-Browse") {
4062 if let Some(browse_url) = repository_browse.datum.to_url() {
4063 match check_url_canonical(&browse_url).await {
4064 Ok(u) => {
4065 repository_browse.datum = UpstreamDatum::RepositoryBrowse(u.to_string());
4066 if repository_browse.certainty >= Some(Certainty::Likely) {
4067 repository_browse.certainty = Some(Certainty::Certain);
4068 }
4069 }
4070 Err(CanonicalizeError::InvalidUrl(u, e)) => {
4071 log::debug!("Deleting invalid Repository-Browse URL {}: {}", u, e);
4072 upstream_metadata.remove("Repository-Browse");
4073 }
4074 Err(CanonicalizeError::Unverifiable(u, _))
4075 | Err(CanonicalizeError::RateLimited(u)) => {
4076 log::debug!("Unable to verify Repository-Browse URL {}", u);
4077 }
4078 }
4079 } else {
4080 log::debug!("Repository-Browse field is not a valid URL, skipping check");
4081 }
4082 }
4083 if let Some(bug_database) = upstream_metadata.get_mut("Bug-Database") {
4084 if let Some(bug_db_url) = bug_database.datum.to_url() {
4085 match check_bug_database_canonical(&bug_db_url, Some(true)).await {
4086 Ok(u) => {
4087 bug_database.datum = UpstreamDatum::BugDatabase(u.to_string());
4088 if bug_database.certainty >= Some(Certainty::Likely) {
4089 bug_database.certainty = Some(Certainty::Certain);
4090 }
4091 }
4092 Err(CanonicalizeError::InvalidUrl(u, e)) => {
4093 log::debug!("Deleting invalid Bug-Database URL {}: {}", u, e);
4094 upstream_metadata.remove("Bug-Database");
4095 }
4096 Err(CanonicalizeError::Unverifiable(u, _))
4097 | Err(CanonicalizeError::RateLimited(u)) => {
4098 log::debug!("Unable to verify Bug-Database URL {}", u);
4099 }
4100 }
4101 } else {
4102 log::debug!("Bug-Database field is not a valid URL, skipping check");
4103 }
4104 }
4105 let bug_submit = upstream_metadata.get_mut("Bug-Submit");
4106 if let Some(bug_submit) = bug_submit {
4107 if let Some(bug_submit_url) = bug_submit.datum.to_url() {
4108 match check_bug_submit_url_canonical(&bug_submit_url, Some(true)).await {
4109 Ok(u) => {
4110 bug_submit.datum = UpstreamDatum::BugSubmit(u.to_string());
4111 if bug_submit.certainty >= Some(Certainty::Likely) {
4112 bug_submit.certainty = Some(Certainty::Certain);
4113 }
4114 }
4115 Err(CanonicalizeError::InvalidUrl(u, e)) => {
4116 log::debug!("Deleting invalid Bug-Submit URL {}: {}", u, e);
4117 upstream_metadata.remove("Bug-Submit");
4118 }
4119 Err(CanonicalizeError::Unverifiable(u, _))
4120 | Err(CanonicalizeError::RateLimited(u)) => {
4121 log::debug!("Unable to verify Bug-Submit URL {}", u);
4122 }
4123 }
4124 } else {
4125 log::debug!("Bug-Submit field is not a valid URL, skipping check");
4126 }
4127 }
4128 let mut screenshots = upstream_metadata.get_mut("Screenshots");
4129 if screenshots.is_some() && screenshots.as_ref().unwrap().certainty == Some(Certainty::Likely) {
4130 let mut newvalue = vec![];
4131 screenshots.as_mut().unwrap().certainty = Some(Certainty::Certain);
4132 let urls = match &screenshots.as_ref().unwrap().datum {
4133 UpstreamDatum::Screenshots(urls) => urls,
4134 _ => unreachable!(),
4135 };
4136 for (url, status) in verify_screenshots(
4137 urls.iter()
4138 .map(|x| x.as_str())
4139 .collect::<Vec<&str>>()
4140 .as_slice(),
4141 )
4142 .await
4143 {
4144 match status {
4145 Some(true) => {
4146 newvalue.push(url);
4147 }
4148 Some(false) => {}
4149 None => {
4150 screenshots.as_mut().unwrap().certainty = Some(Certainty::Likely);
4151 }
4152 }
4153 }
4154 screenshots.as_mut().unwrap().datum = UpstreamDatum::Screenshots(newvalue);
4155 }
4156}
4157
4158#[async_trait::async_trait]
4159pub(crate) trait Guesser: Send {
4160 fn name(&self) -> &str;
4161
4162 async fn guess(
4164 &mut self,
4165 settings: &GuesserSettings,
4166 ) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError>;
4167}
4168
4169type AsyncGuesserFunction = Box<
4170 dyn FnMut(
4171 PathBuf,
4172 GuesserSettings,
4173 ) -> Pin<
4174 Box<
4175 dyn std::future::Future<
4176 Output = Result<Vec<UpstreamDatumWithMetadata>, ProviderError>,
4177 > + Send,
4178 >,
4179 > + Send,
4180>;
4181
4182pub struct PathGuesser {
4184 name: String,
4185 subpath: std::path::PathBuf,
4186 cb: AsyncGuesserFunction,
4187}
4188
4189#[async_trait::async_trait]
4190impl Guesser for PathGuesser {
4191 fn name(&self) -> &str {
4192 &self.name
4193 }
4194
4195 async fn guess(
4196 &mut self,
4197 settings: &GuesserSettings,
4198 ) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
4199 (self.cb)(self.subpath.clone(), settings.clone()).await
4200 }
4201}
4202
4203pub struct EnvironmentGuesser;
4205
4206impl EnvironmentGuesser {
4207 pub fn new() -> Self {
4209 Self
4210 }
4211}
4212
4213impl Default for EnvironmentGuesser {
4214 fn default() -> Self {
4215 Self::new()
4216 }
4217}
4218
4219#[async_trait::async_trait]
4220impl Guesser for EnvironmentGuesser {
4221 fn name(&self) -> &str {
4222 "environment"
4223 }
4224
4225 async fn guess(
4226 &mut self,
4227 _settings: &GuesserSettings,
4228 ) -> Result<Vec<UpstreamDatumWithMetadata>, ProviderError> {
4229 crate::guess_from_environment()
4230 }
4231}
4232
4233#[cfg(test)]
4234mod tests {
4235 use super::*;
4236
4237 #[test]
4238 fn test_upstream_metadata() {
4239 let mut data = UpstreamMetadata::new();
4240 assert_eq!(data.len(), 0);
4241
4242 data.insert(UpstreamDatumWithMetadata {
4243 datum: UpstreamDatum::Homepage("https://example.com".to_string()),
4244 certainty: Some(Certainty::Certain),
4245 origin: None,
4246 });
4247
4248 assert_eq!(data.len(), 1);
4249 assert_eq!(
4250 data.get("Homepage").unwrap().datum.as_str().unwrap(),
4251 "https://example.com"
4252 );
4253
4254 assert_eq!(data.homepage(), Some("https://example.com"));
4255 }
4256
4257 #[tokio::test]
4258 async fn test_bug_database_url_from_bug_submit_url() {
4259 let url = Url::parse("https://bugs.launchpad.net/bugs/+filebug").unwrap();
4260 assert_eq!(
4261 bug_database_url_from_bug_submit_url(&url, None)
4262 .await
4263 .unwrap(),
4264 Url::parse("https://bugs.launchpad.net/bugs").unwrap()
4265 );
4266
4267 let url = Url::parse("https://github.com/dulwich/dulwich/issues/new").unwrap();
4268
4269 assert_eq!(
4270 bug_database_url_from_bug_submit_url(&url, None)
4271 .await
4272 .unwrap(),
4273 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4274 );
4275
4276 let url = Url::parse("https://sourceforge.net/p/dulwich/bugs/new").unwrap();
4277
4278 assert_eq!(
4279 bug_database_url_from_bug_submit_url(&url, None)
4280 .await
4281 .unwrap(),
4282 Url::parse("https://sourceforge.net/p/dulwich/bugs").unwrap()
4283 );
4284 }
4285
4286 #[test]
4287 fn test_person_from_str() {
4288 assert_eq!(
4289 Person::from("Foo Bar <foo@example.com>"),
4290 Person {
4291 name: Some("Foo Bar".to_string()),
4292 email: Some("foo@example.com".to_string()),
4293 url: None
4294 }
4295 );
4296 assert_eq!(
4297 Person::from("Foo Bar"),
4298 Person {
4299 name: Some("Foo Bar".to_string()),
4300 email: None,
4301 url: None
4302 }
4303 );
4304 assert_eq!(
4305 Person::from("foo@example.com"),
4306 Person {
4307 name: None,
4308 email: Some("foo@example.com".to_string()),
4309 url: None
4310 }
4311 );
4312 assert_eq!(
4314 Person::from("<foo@example.com>"),
4315 Person {
4316 name: Some("".to_string()),
4317 email: Some("foo@example.com".to_string()),
4318 url: None
4319 }
4320 );
4321 }
4322
4323 #[test]
4324 fn test_upstream_metadata_accessors() {
4325 let mut metadata = UpstreamMetadata::default();
4326
4327 assert_eq!(metadata.version(), None);
4329 assert_eq!(metadata.description(), None);
4330 assert_eq!(metadata.wiki(), None);
4331 assert_eq!(metadata.download(), None);
4332 assert_eq!(metadata.security_contact(), None);
4333 assert_eq!(metadata.donation(), None);
4334 assert_eq!(metadata.cite_as(), None);
4335 assert_eq!(metadata.webservice(), None);
4336 assert_eq!(metadata.copyright(), None);
4337 assert_eq!(metadata.sourceforge_project(), None);
4338 assert_eq!(metadata.pecl_package(), None);
4339
4340 metadata.insert(UpstreamDatumWithMetadata {
4342 datum: UpstreamDatum::Version("1.0.0".to_string()),
4343 certainty: Some(Certainty::Certain),
4344 origin: None,
4345 });
4346 assert_eq!(metadata.version(), Some("1.0.0"));
4347
4348 metadata.insert(UpstreamDatumWithMetadata {
4349 datum: UpstreamDatum::Description("Test description".to_string()),
4350 certainty: Some(Certainty::Certain),
4351 origin: None,
4352 });
4353 assert_eq!(metadata.description(), Some("Test description"));
4354 }
4355
4356 #[test]
4357 fn test_upstream_metadata_iterators() {
4358 let mut metadata = UpstreamMetadata::default();
4359
4360 assert_eq!(metadata.iter().count(), 0);
4362 assert_eq!(metadata.mut_iter().count(), 0);
4363
4364 metadata.insert(UpstreamDatumWithMetadata {
4366 datum: UpstreamDatum::Name("test".to_string()),
4367 certainty: Some(Certainty::Certain),
4368 origin: None,
4369 });
4370
4371 assert_eq!(metadata.iter().count(), 1);
4372 assert_eq!(metadata.mut_iter().count(), 1);
4373 }
4374
4375 #[test]
4376 fn test_extract_pecl_package_name() {
4377 use super::extract_pecl_package_name;
4378
4379 assert_eq!(
4380 extract_pecl_package_name("https://pecl.php.net/package/redis"),
4381 Some("redis".to_string())
4382 );
4383 assert_eq!(
4384 extract_pecl_package_name("https://pecl.php.net/package/xdebug/2.9.0"),
4385 Some("xdebug/2.9.0".to_string())
4386 );
4387 assert_eq!(
4388 extract_pecl_package_name("https://example.com/something"),
4389 None
4390 );
4391 }
4392
4393 #[test]
4394 fn test_github_bug_database_url_from_repo_url() {
4395 let github = GitHub;
4396
4397 let url = Url::parse("https://github.com/dulwich/dulwich.git").unwrap();
4398 assert_eq!(
4399 github.bug_database_url_from_repo_url(&url).unwrap(),
4400 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4401 );
4402
4403 let url = Url::parse("ssh://git@github.com/dulwich/dulwich.git").unwrap();
4406 assert_eq!(
4407 github.bug_database_url_from_repo_url(&url).unwrap(),
4408 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4409 );
4410
4411 let url = Url::parse("git://github.com/dulwich/dulwich").unwrap();
4412 assert_eq!(
4413 github.bug_database_url_from_repo_url(&url).unwrap(),
4414 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4415 );
4416
4417 let url = Url::parse("https://github.com/dulwich").unwrap();
4419 assert_eq!(github.bug_database_url_from_repo_url(&url), None);
4420 }
4421
4422 #[test]
4423 fn test_github_bug_database_from_issue_url() {
4424 let github = GitHub;
4425
4426 let url = Url::parse("https://github.com/dulwich/dulwich/issues/123").unwrap();
4427 assert_eq!(
4428 github.bug_database_from_issue_url(&url).unwrap(),
4429 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4430 );
4431
4432 let url = Url::parse("ssh://git@github.com/dulwich/dulwich/issues/123").unwrap();
4433 assert_eq!(
4434 github.bug_database_from_issue_url(&url).unwrap(),
4435 Url::parse("https://github.com/dulwich/dulwich/issues").unwrap()
4436 );
4437
4438 let url = Url::parse("https://github.com/dulwich/dulwich").unwrap();
4440 assert_eq!(github.bug_database_from_issue_url(&url), None);
4441 }
4442
4443 #[test]
4444 fn test_github_repo_url_from_merge_request_url() {
4445 let github = GitHub;
4446
4447 let url = Url::parse("https://github.com/dulwich/dulwich/pull/123").unwrap();
4448 assert_eq!(
4449 github.repo_url_from_merge_request_url(&url).unwrap(),
4450 Url::parse("https://github.com/dulwich/dulwich").unwrap()
4451 );
4452
4453 let url = Url::parse("ssh://git@github.com/dulwich/dulwich/pull/123").unwrap();
4454 assert_eq!(
4455 github.repo_url_from_merge_request_url(&url).unwrap(),
4456 Url::parse("https://github.com/dulwich/dulwich").unwrap()
4457 );
4458
4459 let url = Url::parse("https://github.com/dulwich/dulwich/issues/123").unwrap();
4461 assert_eq!(github.repo_url_from_merge_request_url(&url), None);
4462 }
4463
4464 #[test]
4465 fn test_forge_names() {
4466 let github = GitHub;
4467 assert_eq!(github.name(), "GitHub");
4468
4469 let gitlab = GitLab;
4470 assert_eq!(gitlab.name(), "GitLab");
4471
4472 let sourceforge = SourceForge;
4473 assert_eq!(sourceforge.name(), "SourceForge");
4474
4475 let launchpad = Launchpad;
4476 assert_eq!(launchpad.name(), "launchpad");
4477 }
4478}