1use anyhow::Result;
2use anyhow::{anyhow, Context};
3use auth_git2::GitAuthenticator;
4use blake3;
5use dirs;
6use fs_err;
7use git2::Repository;
8use hex;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::io;
12use std::path::PathBuf;
13use std::str::FromStr;
14use tokio::fs;
15use url::Url;
16
17use crate::manifest::WritableManifest;
18use crate::{utils, TargetVersion};
19
20pub const DEFAULT_REPO_URL: &str = "https://github.com/CavefulGames/dalbit-polyfill";
21pub const DEFAULT_INJECTION_PATH: &str = "__polyfill__";
22
23pub async fn clean_cache(url: &Url) -> Result<()> {
25 let index_path = index_path(url)?;
26 fs::remove_dir_all(index_path).await?;
27 Ok(())
28}
29
30pub async fn clean_cache_all() -> Result<()> {
32 let path = cache_dir()?;
33 fs::remove_dir_all(path).await?;
34 Ok(())
35}
36
37pub fn cache_dir() -> Result<PathBuf> {
39 Ok(dirs::cache_dir()
40 .ok_or_else(|| anyhow!("could not find cache directory"))?
41 .join("dalbit")
42 .join("polyfills"))
43}
44
45#[derive(Debug, Deserialize, Serialize)]
47pub struct Polyfill {
48 repository: Url,
49 #[serde(skip_serializing_if = "HashMap::is_empty")]
50 #[serde(default)]
51 globals: HashMap<String, bool>,
52 #[serde(skip_serializing_if = "HashMap::is_empty")]
53 #[serde(default)]
54 config: HashMap<String, bool>,
55 injection_path: PathBuf,
56}
57
58impl Default for Polyfill {
59 fn default() -> Self {
60 Self {
61 repository: Url::from_str(DEFAULT_REPO_URL).unwrap(),
62 globals: HashMap::new(),
63 config: HashMap::new(),
64 injection_path: PathBuf::from_str(DEFAULT_INJECTION_PATH).unwrap(),
65 }
66 }
67}
68
69impl Polyfill {
70 pub fn new(repository: Url, injection_path: PathBuf) -> Self {
71 Self {
72 repository,
73 globals: HashMap::new(),
74 config: HashMap::new(),
75 injection_path,
76 }
77 }
78
79 pub async fn cache(&self) -> Result<PolyfillCache> {
81 PolyfillCache::new(&self.repository).await
82 }
83
84 #[inline]
85 pub fn repository(&self) -> &Url {
86 &self.repository
87 }
88
89 #[inline]
90 pub fn globals(&self) -> &HashMap<String, bool> {
91 &self.globals
92 }
93
94 #[inline]
95 pub fn config(&self) -> &HashMap<String, bool> {
96 &self.config
97 }
98
99 #[inline]
100 pub fn injection_path(&self) -> &PathBuf {
101 &self.injection_path
102 }
103}
104
105#[derive(Debug, Deserialize, Serialize)]
107pub struct PolyfillManifest {
108 globals: PathBuf,
109 removes: Option<Vec<String>>,
110 config: HashMap<String, bool>,
111 lua_version: TargetVersion,
112}
113
114impl WritableManifest for PolyfillManifest {}
115
116#[derive(Debug)]
118pub struct Globals {
119 path: PathBuf,
120 exports: HashSet<String>,
121}
122
123pub struct PolyfillCache {
125 repository: Repository,
126 path: PathBuf,
127 globals: Globals,
128 removes: Option<Vec<String>>,
129 config: HashMap<String, bool>,
130}
131
132fn index_path(url: &Url) -> anyhow::Result<PathBuf> {
133 let name = match (url.domain(), url.scheme()) {
134 (Some(domain), _) => domain,
135 (None, "file") => "local",
136 _ => "unknown",
137 };
138
139 let hash = blake3::hash(url.to_string().as_bytes());
140 let hash_hex = hex::encode(&hash.as_bytes()[..8]);
141 let ident = format!("{}-{}", name, hash_hex);
142
143 let path = cache_dir()?.join(ident);
144
145 log::debug!("index path {:?}", path);
146
147 Ok(path)
148}
149
150impl PolyfillCache {
151 pub async fn new(url: &Url) -> Result<Self> {
153 let path = index_path(url)?;
154 let repository = match Repository::open(path.as_path()) {
155 Ok(repo) => repo,
156 Err(_) => {
157 if let Err(err) = fs_err::remove_dir_all(path.as_path()) {
158 if err.kind() != io::ErrorKind::NotFound {
159 return Err(err.into());
160 }
161 }
162
163 fs_err::create_dir_all(path.as_path())?;
164 let auth = GitAuthenticator::new();
165 auth.clone_repo(url, &path.as_path())?
166 }
167 };
168
169 log::info!("repository is ready");
170
171 let manifest_content = fs::read_to_string(path.join("polyfill.toml")).await?;
173 let manifest: PolyfillManifest = toml::from_str(&manifest_content)?;
174
175 let globals_path = path.join(&manifest.globals);
176 log::debug!("globals path {:?}", globals_path);
177 let globals_ast = utils::parse_file(&globals_path, &manifest.lua_version).await?;
178 let exports = utils::get_exports_from_last_stmt(&utils::ParseTarget::FullMoonAst(globals_ast))
179 .await?
180 .ok_or_else(|| anyhow!("Invalid polyfill structure. Polyfills' globals must return at least one global in a table."))?;
181
182 let globals = Globals {
183 path: globals_path,
184 exports,
185 };
186
187 log::info!("polyfill ready");
188
189 Ok(Self {
190 path,
191 repository,
192 globals: globals,
193 removes: manifest.removes,
194 config: manifest.config,
195 })
196 }
197
198 pub fn fetch(&self) -> Result<()> {
200 let mut remote = self.repository.find_remote("origin")?;
201 let auth = GitAuthenticator::new();
202 auth.fetch(&self.repository, &mut remote, &["main"], None)
203 .with_context(|| format!("Could not fetch git repository"))?;
204
205 let mut options = git2::build::CheckoutBuilder::new();
206 options.force();
207
208 let commit = self
209 .repository
210 .find_reference("FETCH_HEAD")?
211 .peel_to_commit()?;
212 self.repository
213 .reset(
214 &commit.into_object(),
215 git2::ResetType::Hard,
216 Some(&mut options),
217 )
218 .with_context(|| format!("Could not reset git repo to fetch_head"))?;
219
220 Ok(())
221 }
222
223 #[inline]
224 pub fn path(&self) -> &PathBuf {
225 &self.path
226 }
227
228 #[inline]
229 pub fn globals_path(&self) -> &PathBuf {
230 &self.globals.path
231 }
232
233 #[inline]
234 pub fn globals_exports(&self) -> &HashSet<String> {
235 &self.globals.exports
236 }
237
238 #[inline]
239 pub fn removes(&self) -> &Option<Vec<String>> {
240 &self.removes
241 }
242
243 #[inline]
244 pub fn config(&self) -> &HashMap<String, bool> {
245 &self.config
246 }
247}