libwally/
package_contents.rs1use 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#[derive(Clone)]
23pub struct PackageContents {
24 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 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 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 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}