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::{utils, TargetVersion};
18
19pub const DEFAULT_REPO_URL: &str = "https://github.com/CavefulGames/dalbit-polyfill";
20pub const DEFAULT_INJECTION_PATH: &str = "__polyfill__";
21
22/// Cleans cache from polyfill repository url.
23pub 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
29/// Cleans every caches of polyfill.
30pub async fn clean_cache_all() -> Result<()> {
31    let path = cache_dir()?;
32    fs::remove_dir_all(path).await?;
33    Ok(())
34}
35
36/// Gets cache directory path of polyfills.
37pub 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/// Polyfill-related manifest.
45#[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    /// Loads polyfill cache.
79    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/// Polyfill's manifest (`/polyfill.toml` in a polyfill repository)
105#[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    /// Load polyfill manifest from file.
115    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    /// Write polyfill manifest to file.
124    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/// Polyfill's globals.
131#[derive(Debug)]
132pub struct Globals {
133    path: PathBuf,
134    exports: HashSet<String>,
135}
136
137/// Represents a loaded polyfill cache.
138pub 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    /// Creates a new polyfill from git repository.
166    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 = Manifest::from_file(path.join("polyfill.toml")).await?;
186        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    /// Fetches and updates polyfill repository using git.
213    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}