1use std::collections::BTreeMap;
2use std::fs;
3use std::path;
4
5use lazy_static::lazy_static;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use crate::archive::FlatpakArchiveType;
9use crate::format::FlatpakManifestFormat;
10
11pub const ARCHIVE: &str = "archive";
12pub const GIT: &str = "git";
13pub const BAZAAR: &str = "bzr";
14pub const SVN: &str = "svn";
15pub const DIR: &str = "dir";
16pub const FILE: &str = "file";
17pub const SCRIPT: &str = "script";
18pub const SHELL: &str = "shell";
19pub const PATCH: &str = "patch";
20pub const EXTRA_DATA: &str = "extra-data";
21
22lazy_static! {
23 pub static ref CODE_TYPES: Vec<FlatpakSourceType> = vec![
24 FlatpakSourceType::Archive,
25 FlatpakSourceType::Git,
26 FlatpakSourceType::Bazaar,
27 FlatpakSourceType::Svn,
28 FlatpakSourceType::Dir,
29 ];
30 pub static ref VCS_TYPES: Vec<FlatpakSourceType> = vec![
31 FlatpakSourceType::Git,
32 FlatpakSourceType::Bazaar,
33 FlatpakSourceType::Svn,
34 ];
35}
36
37#[derive(Clone)]
38#[derive(Deserialize)]
39#[derive(Debug)]
40#[derive(Hash)]
41#[derive(PartialEq)]
42pub enum FlatpakSourceType {
46 Archive,
47 Git,
48 Bazaar,
49 Svn,
50 Dir,
51 File,
52 Script,
53 Shell,
54 Patch,
55 ExtraData,
56}
57impl Default for FlatpakSourceType {
58 fn default() -> Self {
59 FlatpakSourceType::Archive
60 }
61}
62impl FlatpakSourceType {
63 pub fn is_code(&self) -> bool {
66 CODE_TYPES.contains(self)
67 }
68
69 pub fn is_vcs(&self) -> bool {
73 VCS_TYPES.contains(self)
74 }
75
76 pub fn supports_mirror_urls(&self) -> bool {
78 if *self == FlatpakSourceType::Archive {
79 return true;
80 }
81 if *self == FlatpakSourceType::File {
82 return true;
83 }
84 #[cfg(feature = "extended_mirror_urls_support")]
87 if *self == FlatpakSourceType::Git
88 || *self == FlatpakSourceType::Svn
89 || *self == FlatpakSourceType::Bazaar
90 {
91 return true;
92 }
93 false
94 }
95
96 pub fn to_string(&self) -> String {
97 match &self {
98 FlatpakSourceType::Archive => ARCHIVE.to_string(),
99 FlatpakSourceType::Git => GIT.to_string(),
100 FlatpakSourceType::Bazaar => BAZAAR.to_string(),
101 FlatpakSourceType::Svn => SVN.to_string(),
102 FlatpakSourceType::Dir => DIR.to_string(),
103 FlatpakSourceType::File => FILE.to_string(),
104 FlatpakSourceType::Script => SCRIPT.to_string(),
105 FlatpakSourceType::Shell => SHELL.to_string(),
106 FlatpakSourceType::Patch => PATCH.to_string(),
107 FlatpakSourceType::ExtraData => EXTRA_DATA.to_string(),
108 }
109 }
110
111 pub fn from_string(source_type: &str) -> Result<FlatpakSourceType, String> {
112 if source_type == ARCHIVE {
113 return Ok(FlatpakSourceType::Archive);
114 }
115 if source_type == GIT {
116 return Ok(FlatpakSourceType::Git);
117 }
118 if source_type == BAZAAR {
119 return Ok(FlatpakSourceType::Bazaar);
120 }
121 if source_type == SVN {
122 return Ok(FlatpakSourceType::Svn);
123 }
124 if source_type == DIR {
125 return Ok(FlatpakSourceType::Dir);
126 }
127 if source_type == FILE {
128 return Ok(FlatpakSourceType::File);
129 }
130 if source_type == SCRIPT {
131 return Ok(FlatpakSourceType::Script);
132 }
133 if source_type == SHELL {
134 return Ok(FlatpakSourceType::Shell);
135 }
136 if source_type == PATCH {
137 return Ok(FlatpakSourceType::Patch);
138 }
139 if source_type == EXTRA_DATA {
140 return Ok(FlatpakSourceType::ExtraData);
141 }
142 Err(format!("Invalid source type {}.", source_type))
143 }
144
145 pub fn serialize<S>(x: &Option<FlatpakSourceType>, s: S) -> Result<S::Ok, S::Error>
146 where
147 S: Serializer,
148 {
149 if let Some(build_system) = x {
150 return s.serialize_str(&build_system.to_string());
151 }
152 panic!("This should not happen.");
153 }
154
155 pub fn deserialization<'de, D>(deserializer: D) -> Result<Option<FlatpakSourceType>, D::Error>
156 where
157 D: Deserializer<'de>,
158 {
159 let buf = String::deserialize(deserializer)?;
160
161 match FlatpakSourceType::from_string(&buf) {
162 Ok(b) => Ok(Some(b)),
163 Err(e) => Err(e).map_err(serde::de::Error::custom),
164 }
165 }
166}
167
168#[derive(Clone)]
169#[derive(Deserialize)]
170#[derive(Serialize)]
171#[derive(Debug)]
172#[derive(Hash)]
173#[serde(rename_all = "kebab-case")]
174#[serde(untagged)]
175pub enum FlatpakSourceItem {
183 Path(String),
184 Description(FlatpakSource),
185}
186
187#[derive(Clone)]
188#[derive(Deserialize)]
189#[derive(Serialize)]
190#[derive(Debug)]
191#[derive(Default)]
192#[derive(Hash)]
193#[serde(rename_all = "kebab-case")]
194pub struct FlatpakSource {
198 #[serde(deserialize_with = "crate::source::FlatpakSourceType::deserialization")]
200 #[serde(serialize_with = "crate::source::FlatpakSourceType::serialize")]
201 #[serde(skip_serializing_if = "Option::is_none")]
202 #[serde(default)]
203 pub r#type: Option<FlatpakSourceType>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
208 pub commands: Option<Vec<String>>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
213 pub dest_filename: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
218 pub filename: Option<String>,
219
220 #[serde(skip_serializing_if = "Option::is_none")]
223 pub url: Option<String>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
228 pub mirror_urls: Option<Vec<String>>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
234 pub md5: Option<String>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
240 pub sha1: Option<String>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
245 pub sha256: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
250 pub sha512: Option<String>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
255 pub size: Option<i64>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
260 pub git_init: Option<bool>,
261
262 #[serde(skip_serializing_if = "Option::is_none")]
265 pub installed_size: Option<i64>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
270 pub revision: Option<String>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
275 pub branch: Option<String>,
276
277 #[serde(deserialize_with = "crate::archive::FlatpakArchiveType::deserialize")]
280 #[serde(serialize_with = "crate::archive::FlatpakArchiveType::serialize")]
281 #[serde(skip_serializing_if = "Option::is_none")]
282 #[serde(default)]
283 pub archive_type: Option<FlatpakArchiveType>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
291 pub commit: Option<String>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
296 pub tag: Option<String>,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
301 pub path: Option<String>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
306 pub paths: Option<Vec<String>>,
307
308 #[serde(skip_serializing_if = "Option::is_none")]
312 pub use_git: Option<bool>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
319 pub use_git_am: Option<bool>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
324 pub options: Option<Vec<String>>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
330 pub disable_fsckobjects: Option<bool>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
335 pub disable_shallow_clone: Option<bool>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
340 pub disable_submodules: Option<bool>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
346 pub strip_components: Option<i64>,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
351 pub skip: Option<Vec<String>>,
352
353 #[serde(skip_serializing_if = "Option::is_none")]
356 pub only_arches: Option<Vec<String>>,
357
358 #[serde(skip_serializing_if = "Option::is_none")]
361 pub skip_arches: Option<Vec<String>>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
366 pub dest: Option<String>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub x_checker_data: Option<FlatpakDataCheckerConfig>,
370}
371impl FlatpakSource {
372 pub fn get_type(&self) -> Option<FlatpakSourceType> {
374 self.r#type.clone()
375 }
376
377 pub fn file_path_matches(path: &str) -> bool {
378 crate::filename::extension_is_valid(path)
381 }
382
383 pub fn load_from_file(path: String) -> Result<Vec<FlatpakSource>, String> {
384 let file_path = path::Path::new(&path);
385 if !file_path.is_file() {
386 return Err(format!("{} is not a file.", path));
387 }
388
389 let manifest_format = match FlatpakManifestFormat::from_path(&path) {
390 Some(f) => f,
391 None => return Err(format!("{} is not a Flatpak source manifest.", path)),
392 };
393
394 let manifest_content = match fs::read_to_string(file_path) {
395 Ok(content) => content,
396 Err(e) => {
397 return Err(format!(
398 "Could not read file {}: {}!",
399 file_path.to_str().unwrap(),
400 e
401 ))
402 }
403 };
404
405 if let Ok(source) = FlatpakSource::parse(manifest_format.clone(), &manifest_content) {
408 return Ok(vec![source]);
409 }
410 if let Ok(sources) = FlatpakSource::parse_many(manifest_format, &manifest_content) {
411 return Ok(sources);
412 }
413
414 return Err(format!("Failed to parse Flatpak source manifest at {}.", path));
415 }
416
417 pub fn parse(format: FlatpakManifestFormat, manifest_content: &str) -> Result<FlatpakSource, String> {
418 let flatpak_source: FlatpakSource = match format.parse(manifest_content) {
419 Ok(m) => m,
420 Err(e) => {
421 return Err(format!("Failed to parse the Flatpak source manifest: {}.", e));
422 }
423 };
424
425 if let Err(e) = flatpak_source.is_valid() {
426 return Err(e);
427 }
428 Ok(flatpak_source)
429 }
430
431 pub fn parse_many(
432 format: FlatpakManifestFormat,
433 manifest_content: &str,
434 ) -> Result<Vec<FlatpakSource>, String> {
435 let flatpak_sources: Vec<FlatpakSource> = match format.parse(manifest_content) {
436 Ok(m) => m,
437 Err(e) => {
438 return Err(format!("Failed to parse the Flatpak source manifest: {}.", e));
439 }
440 };
441
442 if flatpak_sources.len() == 0 {
443 return Err("Empty array is not a valid source manifest!".to_string());
444 }
445
446 for flatpak_source in &flatpak_sources {
447 if let Err(e) = flatpak_source.is_valid() {
448 return Err(e);
449 }
450 }
451 Ok(flatpak_sources)
452 }
453
454 pub fn is_valid(&self) -> Result<(), String> {
455 if self.url.is_none() && self.path.is_none() && self.commands.is_none() {
456 return Err("There should be at least a url, a path or inline commands in a source!".to_string());
457 }
458 Ok(())
459 }
460
461 pub fn get_url(&self) -> Option<String> {
462 match &self.url {
463 Some(s) => Some(s.to_string()),
464 None => None,
465 }
466 }
467
468 pub fn get_urls(
469 &self,
470 include_mirror_urls: bool,
471 include_source_types: Option<Vec<FlatpakSourceType>>,
472 ) -> Vec<String> {
473 if let Some(current_type) = self.get_type() {
474 if !include_source_types.unwrap_or(vec![]).contains(¤t_type) {
475 return vec![];
476 }
477 }
478
479 let mut response: Vec<String> = vec![];
480 if let Some(url) = &self.url {
481 response.push(url.to_string());
482 }
483
484 if !include_mirror_urls {
485 return response;
486 }
487
488 if let Some(urls) = &self.mirror_urls {
489 for url in urls {
490 response.push(url.to_string());
491 }
492 }
493 return response;
494 }
495
496 pub fn get_mirror_urls(&self) -> Vec<String> {
497 let mut response: Vec<String> = vec![];
498 if let Some(urls) = &self.mirror_urls {
499 for url in urls {
500 response.push(url.to_string());
501 }
502 }
503 return response;
504 }
505
506 pub fn get_type_name(&self) -> String {
507 if let Some(t) = self.get_type() {
508 return t.to_string();
509 }
510 return "empty".to_string();
511 }
512
513 pub fn supports_mirror_urls(&self) -> bool {
514 if let Some(t) = self.get_type() {
515 return t.supports_mirror_urls();
516 }
517 return false;
518 }
519}
520
521#[derive(Clone)]
522#[derive(Deserialize)]
523#[derive(Serialize)]
524#[derive(Debug)]
525#[derive(Default)]
526#[derive(Hash)]
527#[serde(rename_all = "kebab-case")]
528#[serde(default)]
529pub struct FlatpakDataCheckerConfig {
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub r#type: Option<String>,
534
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub url: Option<String>,
537
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub version_pattern: Option<String>,
540
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub is_main_source: Option<bool>,
543
544 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
547 pub versions: BTreeMap<String, String>,
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 pub fn test_parse_single_source_manifest() {
556 match FlatpakSource::parse(
557 FlatpakManifestFormat::YAML,
558 r###"
559 type: file
560 path: apply_extra.sh
561 "###,
562 ) {
563 Err(e) => std::panic::panic_any(e),
564 Ok(source) => {
565 assert_eq!(source.path, Some("apply_extra.sh".to_string()));
566 assert_eq!(source.get_type(), Some(FlatpakSourceType::File));
567 }
568 }
569 }
570
571 #[test]
572 pub fn test_parse_multiple_source_manifests() {
573 match FlatpakSource::parse_many(
574 FlatpakManifestFormat::YAML,
575 r###"
576 - type: file
577 path: apply_extra.sh
578
579 - type: file
580 path: com.wps.Office.metainfo.xml
581
582 - type: file
583 path: wps.sh
584
585 - type: extra-data
586 filename: wps-office.deb
587 only-arches:
588 - x86_64
589 url: https://wdl1.pcfg.cache.wpscdn.com/wps-office_11.1.0.10702.XA_amd64.deb
590 sha256: 390a8b358aaccdfda54740d10d5306c2543c5cd42a7a8fd5c776ccff38492992
591 size: 275210770
592 installed-size: 988671247
593 x-checker-data:
594 type: html
595 url: https://linux.wps.com/js/meta.js
596 version-pattern: version\s*=\s*"([\d.-]+)"
597 url-pattern: download_link_deb\s*=\s*"(http[s]?://[\w\d$-_@.&+]+)"
598 "###,
599 ) {
600 Err(e) => std::panic::panic_any(e),
601 Ok(sources) => {
602 assert_eq!(sources.len(), 4);
603 let last_source = sources.last().unwrap();
604 assert_eq!(last_source.filename, Some("wps-office.deb".to_string()));
605 }
606 }
607 }
608
609 #[test]
610 pub fn test_parse_invalid_type() {
611 let source_manifest = r###"
612 type: not_a_valid_source_type
613 path: apply_extra.sh
614 "###;
615 match FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest) {
616 Ok(_source) => {
617 panic!("We should not be able to parse a source manifest with an invalid source type");
618 }
619 Err(e) => {
620 assert!(e.to_string().contains("Invalid source type"));
621 }
622 }
623 }
624
625 #[test]
626 pub fn test_parse_archive_type() {
627 let source_manifest = r###"
628 type: archive
629 url: https://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz
630 archive_type: tar-gz
631 "###;
632 match FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest) {
633 Ok(source) => {
634 assert!(source.url.is_some());
635 assert_eq!(source.get_type(), Some(FlatpakSourceType::Archive));
636 }
637 Err(e) => {
638 panic!(
639 "We should be able to parse a source manifest with an archive type: {}",
640 e
641 );
642 }
643 }
644 }
645
646 #[test]
647 pub fn test_parse_shell_type() {
648 let source_manifest = r###"
649 type: "shell"
650 commands:
651 -
652 sed -i -e 's/\${\${NAME}_BIN}-NOTFOUND/\${NAME}_BIN-NOTFOUND/' cpp/CMakeLists.txt
653 "###;
654 match FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest) {
655 Ok(source) => {
656 assert_eq!(source.get_type(), Some(FlatpakSourceType::Shell));
657 }
658 Err(e) => {
659 panic!(
660 "We should be able to parse a source manifest with a shell type: {}",
661 e
662 );
663 }
664 }
665 }
666
667 #[test]
668 pub fn test_parse_invalid_archive_type() {
669 let source_manifest = r###"
670 type: archive
671 archive-type: blahblah
672 url: https://ftp.gnu.org/gnu/glibc/glibc-2.0.1.tar.gz
673 "###;
674 match FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest) {
675 Ok(_source) => {
676 println!("{:?}", _source);
677 panic!("We should not be able to parse a source manifest with an invalid source type");
678 }
679 Err(e) => {
680 assert!(e.to_string().contains("Invalid archive type"));
681 }
682 }
683 }
684
685 #[test]
686 pub fn test_parse_missing_source_type() {
687 let source_manifest = r###"
688 url: "https://ftp.gnu.org/gnu/gcc/gcc-7.5.0/gcc-7.5.0.tar.xz"
689 sha256: "b81946e7f01f90528a1f7352ab08cc602b9ccc05d4e44da4bd501c5a189ee661"
690 "###;
691 match FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest) {
692 Ok(source) => {
693 assert!(source.url.is_some());
694 assert!(source.get_type().is_none());
695 }
696 Err(e) => {
697 panic!(
698 "We should be able to parse a source manifest without a source type: {}",
699 e
700 );
701 }
702 }
703 }
704
705 #[test]
706 pub fn test_parse_random_yaml_file() {
707 let source_manifest = r###"
708 title: "Copying the Diagram to the Clipboard"
709 content: []
710 description: "This is where you click for the action to happen"
711 "###;
712 if FlatpakSource::parse(FlatpakManifestFormat::YAML, source_manifest).is_ok() {
713 panic!("We should not parse a random yaml file as a source manifest",);
714 }
715 }
716
717 #[test]
718 pub fn test_parse_empty_array() {
719 let source_manifest = "[]";
720 if FlatpakSource::parse(FlatpakManifestFormat::JSON, source_manifest).is_ok() {
721 panic!("We should not parse an empty json array as a source manifest",);
722 }
723 if FlatpakSource::parse_many(FlatpakManifestFormat::JSON, source_manifest).is_ok() {
724 panic!("We should not parse an empty json array as many source manifests",);
725 }
726 }
727}