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