flatpak_rs/
source.rs

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)]
42/// The Flatpak sources can be of multiple different types, determined
43/// by the `type` field. The type of the Flatpak source will determine which
44/// other fields should be populated.
45pub 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    /// Determines if a Flatpak source points to a code project.
64    /// See [struct@crate::source::CODE_TYPES] for the list of code types.
65    pub fn is_code(&self) -> bool {
66        CODE_TYPES.contains(self)
67    }
68
69    /// Determines if a Flatpak source points to a version-control system
70    /// repository.
71    /// See [struct@crate::source::VCS_TYPES] for the list of VCS types.
72    pub fn is_vcs(&self) -> bool {
73        VCS_TYPES.contains(self)
74    }
75
76    /// Determines if mirror urls can be used with that source type.
77    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        /// This is not supported by flatpak-builder by default. This might
85        /// be a simple mistake though, so might be worth it to reasses later.
86        #[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)]
175/// The sources are a list pointer to the source code that needs to be extracted into
176/// the build directory before the build starts.
177/// They can be of several types, distinguished by the type property.
178///
179/// Additionally, the sources list can contain a plain string, which is interpreted as the name
180/// of a separate json or yaml file that is read and inserted at this
181/// point. The file can contain a single source, or an array of sources.
182pub 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")]
194/// These contain a pointer to the source that will be extracted into the
195/// source directory before the build starts. They can be of several types,
196/// distinguished by the type property.
197pub struct FlatpakSource {
198    /// Defines the type of the source description.
199    #[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    /// An array of shell commands.
206    /// types: script, shell
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub commands: Option<Vec<String>>,
209
210    /// Filename to use inside the source dir.
211    /// types: script, archive, file
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub dest_filename: Option<String>,
214
215    /// The name to use for the downloaded extra data
216    /// types: extra-data
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub filename: Option<String>,
219
220    /// The url to the resource.
221    /// types: extra-data, svn, bzr, git, archive, file
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub url: Option<String>,
224
225    /// A list of alternative urls that are used if the main url fails.
226    /// types: archive, file
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub mirror_urls: Option<Vec<String>>,
229
230    /// The md5 checksum of the file, verified after download
231    /// Note that md5 is no longer considered a safe checksum, we recommend you use at least sha256.
232    /// types: archive, file
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub md5: Option<String>,
235
236    /// The sha1 checksum of the file, verified after download
237    /// Note that sha1 is no longer considered a safe checksum, we recommend you use at least sha256.
238    /// types: archive, file
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub sha1: Option<String>,
241
242    /// The sha256 of the resource.
243    /// types: extra-data, archive, file
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub sha256: Option<String>,
246
247    /// The sha512 checksum of the file, verified after download
248    /// types: archive, file
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub sha512: Option<String>,
251
252    /// The size of the extra data in bytes.
253    /// types: extra-data
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub size: Option<i64>,
256
257    /// Whether to initialise the repository as a git repository.
258    /// types: archive
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub git_init: Option<bool>,
261
262    /// The extra installed size this adds to the app (optional).
263    /// types: extra-data
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub installed_size: Option<i64>,
266
267    /// A specific revision number to use
268    /// types: svn, bzr
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub revision: Option<String>,
271
272    /// The branch to use from the git repository
273    /// types: git
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub branch: Option<String>,
276
277    /// The type of archive if it cannot be guessed from the path.
278    /// types: archive
279    #[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    /// The commit to use from the git repository.
286    /// If branch is also specified, then it is verified that the branch/tag is at this specific commit.
287    /// This is a readable way to document that you're using a particular tag,
288    /// but verify that it does not change.
289    /// types: git
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub commit: Option<String>,
292
293    /// The tag to use from the git repository
294    /// types: git
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub tag: Option<String>,
297
298    /// The path to associated with the resource.
299    /// types: git, archive, dir, patch, file
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub path: Option<String>,
302
303    /// An list of paths to a patch files that will be applied in the source dir, in order
304    /// types: patch
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub paths: Option<Vec<String>>,
307
308    /// Whether to use "git apply" rather than "patch" to apply the patch, required when the
309    /// patch file contains binary diffs.
310    /// types: patch
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub use_git: Option<bool>,
313
314    /// Whether to use "git am" rather than "patch" to apply the patch, required when the patch
315    /// file contains binary diffs.
316    /// You cannot use this at the same time as use-git.
317    /// types: patch
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub use_git_am: Option<bool>,
320
321    /// Extra options to pass to the patch command.
322    /// types: patch
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub options: Option<Vec<String>>,
325
326    /// Don't use transfer.fsckObjects=1 to mirror git repository. This may be needed for some
327    /// (broken) repositories.
328    /// types: git
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub disable_fsckobjects: Option<bool>,
331
332    /// Don't optimize by making a shallow clone when downloading the git repo.
333    /// types: git
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub disable_shallow_clone: Option<bool>,
336
337    /// Don't checkout the git submodules when cloning the repository.
338    /// types: git
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub disable_submodules: Option<bool>,
341
342    /// The number of initial pathname components to strip.
343    /// defaults to 1.
344    /// types: archive, patch
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub strip_components: Option<i64>,
347
348    /// Source files to ignore in the directory.
349    /// types: dir
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub skip: Option<Vec<String>>,
352
353    /// If non-empty, only build the module on the arches listed.
354    /// types: all
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub only_arches: Option<Vec<String>>,
357
358    /// Don't build on any of the arches listed.
359    /// types: all
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub skip_arches: Option<Vec<String>>,
362
363    /// Directory inside the source dir where this source will be extracted.
364    /// types: all
365    #[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    /// Get the type for the Flatpak source.
373    pub fn get_type(&self) -> Option<FlatpakSourceType> {
374        self.r#type.clone()
375    }
376
377    pub fn file_path_matches(path: &str) -> bool {
378        // The file path for a module is not necessarily in reverse DNS, so we can only test
379        // for the extension of the file.
380        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        // A standalone source manifest can contain a single source, or an array
406        // of sources!!
407        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(&current_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)]
529/// See <https://github.com/flathub/flatpak-external-data-checker#changes-to-flatpak-manifests>
530/// for the specification
531pub 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    /// The constraints placed on version checking.
545    /// See <https://github.com/flathub/flatpak-external-data-checker#version-constraining>
546    #[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}