Skip to main content

libwally/
package_contents.rs

1use std::io::{self, BufRead, BufReader, Cursor};
2use std::path::{Path, PathBuf};
3
4use anyhow::format_err;
5use fs_err::File;
6use globset::{Glob, GlobSet, GlobSetBuilder};
7use serde_json::json;
8use walkdir::WalkDir;
9use zip::{write::FileOptions, ZipArchive, ZipWriter};
10
11use crate::manifest::Manifest;
12
13static EXCLUDED_GLOBS: &[&str] = &[
14    ".*",
15    "wally.lock",
16    "Packages",
17    "ServerPackages",
18    "DevPackages",
19];
20
21/// Container for the contents of a package that have been downloaded.
22#[derive(Clone)]
23pub struct PackageContents {
24    /// Contains a zip with the contents of the package.
25    data: Vec<u8>,
26}
27
28impl PackageContents {
29    pub fn pack_from_path(input: &Path) -> anyhow::Result<Self> {
30        let manifest = Manifest::load(input)?;
31        let package_name = manifest.package.name.name();
32
33        let mut data = Vec::new();
34        let mut archive = ZipWriter::new(Cursor::new(&mut data));
35
36        for path in Self::filtered_contents(input)? {
37            let relative_path = path.strip_prefix(input).unwrap();
38            let archive_name = relative_path.to_str().ok_or_else(|| {
39                format_err!(
40                    "Path {} contained invalid Unicode characters",
41                    relative_path.display()
42                )
43            })?;
44
45            // Zips embed \ from windows paths causing incorrect extraction on unix operating
46            // systems; we must sanitise here. See: https://github.com/UpliftGames/wally/issues/15
47            // This may be fixed in the zip crate. See: https://github.com/zip-rs/zip/issues/253
48            let archive_name = str::replace(archive_name, "\\", "/");
49
50            if path.is_dir() {
51                archive.add_directory(archive_name, FileOptions::default())?;
52            } else {
53                archive.start_file(archive_name, FileOptions::default())?;
54
55                if path.ends_with("default.project.json") {
56                    let project_file = File::open(path)?;
57                    let mut project_json: serde_json::Value =
58                        serde_json::from_reader(project_file)?;
59                    let project_name = project_json
60                        .get("name")
61                        .and_then(|name| name.as_str())
62                        .expect("Couldn't parse name in default.project.json");
63
64                    if project_name != package_name {
65                        log::info!(
66                            "The project and package names are mismatched. The project name in \
67                            `default.project.json` has been renamed to '{}' in the uploaded package \
68                            to match the name provided by `wally.toml`",
69                            package_name
70                        );
71
72                        *project_json.get_mut("name").unwrap() = json!(package_name);
73                    }
74
75                    serde_json::to_writer_pretty(&mut archive, &project_json)?;
76                } else {
77                    let mut file = BufReader::new(File::open(path)?);
78                    io::copy(&mut file, &mut archive)?;
79                }
80            }
81        }
82
83        archive.finish()?;
84        drop(archive);
85
86        Ok(PackageContents { data })
87    }
88
89    /// Unpack the package into the given path on the filesystem.
90    pub fn unpack_into_path(&self, output: &Path) -> anyhow::Result<()> {
91        let mut archive = ZipArchive::new(Cursor::new(self.data.as_slice()))?;
92        archive.extract(output)?;
93        Ok(())
94    }
95
96    pub fn filtered_contents(input: &Path) -> anyhow::Result<Vec<PathBuf>> {
97        let manifest = Manifest::load(input)?;
98        let includes = manifest.package.include;
99        let mut excludes = manifest.package.exclude;
100
101        if includes.is_empty() && Path::new(".gitignore").exists() {
102            let gitignore = File::open(Path::new(".gitignore"))?;
103
104            BufReader::new(gitignore)
105                .lines()
106                .flatten()
107                .for_each(|pattern| {
108                    excludes.push(pattern);
109                });
110        }
111
112        EXCLUDED_GLOBS
113            .iter()
114            .map(|pattern| pattern.to_string())
115            .for_each(|pattern| excludes.push(pattern));
116
117        let include = build_glob_set(&includes)?;
118        let exclude = build_glob_set(&excludes)?;
119
120        Ok(WalkDir::new(input)
121            .min_depth(1)
122            .into_iter()
123            .filter_entry(|entry| {
124                let relative = entry.path().strip_prefix(input).unwrap();
125
126                if !includes.is_empty() && !include.matches(relative).is_empty() {
127                    return true;
128                };
129
130                exclude.matches(relative).is_empty()
131            })
132            .flatten()
133            .map(|entry| entry.path().to_path_buf())
134            .collect())
135    }
136
137    pub fn data(&self) -> &[u8] {
138        &self.data
139    }
140
141    /// Create a new PackageContents object from a buffer.
142    pub fn from_buffer(data: Vec<u8>) -> PackageContents {
143        PackageContents { data }
144    }
145}
146
147fn build_glob_set(patterns: &[String]) -> anyhow::Result<GlobSet> {
148    let mut builder = GlobSetBuilder::new();
149
150    for pattern in patterns {
151        builder.add(Glob::new(pattern)?);
152    }
153
154    Ok(builder.build()?)
155}