dalbit_core/
polyfill.rs

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
23/// Cleans cache from polyfill repository url.
24pub 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
30/// Cleans every caches of polyfill.
31pub async fn clean_cache_all() -> Result<()> {
32    let path = cache_dir()?;
33    fs::remove_dir_all(path).await?;
34    Ok(())
35}
36
37/// Gets cache directory path of polyfills.
38pub 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/// Polyfill-related manifest.
46#[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    /// Loads polyfill cache.
80    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/// Polyfill's manifest (`/polyfill.toml` in a polyfill repository)
106#[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/// Polyfill's globals.
117#[derive(Debug)]
118pub struct Globals {
119    path: PathBuf,
120    exports: HashSet<String>,
121}
122
123/// Represents a loaded polyfill cache.
124pub 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    /// Creates a new polyfill from git repository.
152    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 = Manifest::from_file(path.join("polyfill.toml")).await?;
172        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    /// Fetches and updates polyfill repository using git.
199    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}