runtime/cache/
cache.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::ffi::OsStr;
8use std::fmt::{Display, Formatter};
9use std::fs::{create_dir_all, metadata, read_dir, read_to_string, remove_dir_all, write};
10use std::path::{Path, PathBuf};
11use std::str::{Utf8Error, from_utf8};
12use std::{fmt, io};
13
14use base64::Engine as _;
15use base64::prelude::BASE64_URL_SAFE;
16use dirs::home_dir;
17use dunce::canonicalize;
18use sha3::{Digest as _, Sha3_512};
19use swc_sourcemap::SourceMap;
20
21use crate::config::Config;
22use crate::typescript;
23use crate::typescript::compile_typescript;
24
25pub struct Cache {
26	dir: PathBuf,
27}
28
29impl Cache {
30	pub fn new() -> Option<Cache> {
31		home_dir().map(|mut path| {
32			path.extend([".spiderfire", "cache"]);
33			let _ = create_dir_all(&path);
34			Cache { dir: path }
35		})
36	}
37
38	pub fn dir(&self) -> &Path {
39		self.dir.as_path()
40	}
41
42	pub fn clear(&self) -> io::Result<()> {
43		for entry in read_dir(&self.dir)? {
44			remove_dir_all(entry?.path())?;
45		}
46		create_dir_all(&self.dir)?;
47		Ok(())
48	}
49
50	pub fn find_folder<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, Error> {
51		let canonical = canonicalize(path)?;
52		let folder = canonical.parent().ok_or(Error::Other)?;
53		let folder_name = folder.file_name().and_then(OsStr::to_str).ok_or(Error::Other)?;
54
55		let hash = hash(folder.as_os_str().to_str().unwrap().as_bytes(), Some(16));
56		let folder = self.dir.join(format!("{folder_name}-{hash}"));
57		Ok(folder)
58	}
59
60	pub fn check_cache<P: AsRef<Path>>(
61		&self, path: P, folder: &Path, source: &str,
62	) -> Result<(String, SourceMap), Error> {
63		let path = path.as_ref();
64		let source_file = path.file_stem().and_then(OsStr::to_str).ok_or(Error::Other)?;
65		let extension = path.extension().and_then(OsStr::to_str).ok_or(Error::Other)?;
66
67		let source_hash = hash(source, None);
68		let destination_file = folder.join(source_file).with_extension("js");
69		let map_file = folder.join(source_file).with_extension("js.map");
70
71		let source_hash_file = folder.join(format!("{source_file}.{extension}.sha512"));
72		let destination_hash_file = destination_file.with_extension("js.sha512");
73		let map_hash_file = map_file.with_extension("js.map.sha512");
74
75		if folder.exists() && metadata(folder).unwrap().is_dir() && is_file(&source_hash_file) {
76			let cached_source_hash = read_to_string(&source_hash_file)?;
77
78			if cached_source_hash.trim() == source_hash
79				&& is_file(&destination_file)
80				&& is_file(&destination_hash_file)
81				&& is_file(&map_file)
82				&& is_file(&map_hash_file)
83			{
84				let destination = read_to_string(&destination_file)?;
85				let destination_hash = hash(&destination, None);
86				let cached_destination_hash = read_to_string(&destination_hash_file)?;
87
88				let map = read_to_string(&map_file)?;
89				let map_hash = hash(&map, None);
90				let cached_map_hash = read_to_string(&map_hash_file)?;
91
92				if cached_destination_hash.trim() == destination_hash && cached_map_hash.trim() == map_hash {
93					let sourcemap = SourceMap::from_slice(map.as_bytes()).unwrap();
94					return Ok((destination, sourcemap));
95				}
96			}
97		}
98		Err(Error::HashedSource(source_hash))
99	}
100
101	pub fn save_to_cache<P: AsRef<Path>>(
102		&self, path: P, folder: &Path, source: &str, source_hash: Option<&str>,
103	) -> Result<(String, SourceMap), Error> {
104		let path = path.as_ref();
105		if Config::global().typescript && path.extension() == Some(OsStr::new("ts")) {
106			let source_name = path.file_name().and_then(OsStr::to_str).ok_or(Error::Other)?;
107			let source_file = path.file_stem().and_then(OsStr::to_str).ok_or(Error::Other)?;
108			let extension = path.extension().and_then(OsStr::to_str).ok_or(Error::Other)?;
109
110			let source_hash = source_hash.map_or_else(|| hash(source, None), String::from);
111			let destination_file = folder.join(source_file).with_extension("js");
112			let map_file = folder.join(source_file).with_extension("js.map");
113
114			let source_hash_file = folder.join(format!("{source_file}.{extension}.sha512"));
115			let destination_hash_file = destination_file.with_extension("js.sha512");
116			let map_hash_file = map_file.with_extension("map.sha512");
117
118			let (destination, sourcemap) = compile_typescript(source_name, source)?;
119			let mut sourcemap_str: Vec<u8> = Vec::new();
120			sourcemap.to_writer(&mut sourcemap_str).unwrap();
121			let sourcemap_str = from_utf8(&sourcemap_str)?;
122
123			if !folder.exists() || !metadata(folder)?.is_dir() {
124				create_dir_all(folder)?;
125			}
126			write(&destination_file, &destination)?;
127			write(&map_file, sourcemap_str)?;
128
129			write(source_hash_file, source_hash)?;
130			write(destination_hash_file, hash(&destination, None))?;
131			write(map_hash_file, hash(sourcemap_str, None))?;
132
133			Ok((destination, sourcemap))
134		} else {
135			Err(Error::Other)
136		}
137	}
138}
139
140#[derive(Debug)]
141pub enum Error {
142	HashedSource(String),
143	Other,
144	TypeScript(typescript::Error),
145	Io(io::Error),
146	FromUtf8(Utf8Error),
147}
148
149impl From<io::Error> for Error {
150	fn from(err: io::Error) -> Error {
151		Error::Io(err)
152	}
153}
154
155impl From<Utf8Error> for Error {
156	fn from(err: Utf8Error) -> Error {
157		Error::FromUtf8(err)
158	}
159}
160
161impl From<typescript::Error> for Error {
162	fn from(err: typescript::Error) -> Error {
163		Error::TypeScript(err)
164	}
165}
166
167impl Display for Error {
168	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
169		match self {
170			Error::TypeScript(err) => f.write_str(&err.to_string()),
171			Error::Io(err) => f.write_str(&err.to_string()),
172			Error::FromUtf8(err) => f.write_str(&err.to_string()),
173			_ => Ok(()),
174		}
175	}
176}
177
178fn hash<T: AsRef<[u8]>>(bytes: T, len: Option<usize>) -> String {
179	let hash = BASE64_URL_SAFE.encode(Sha3_512::new().chain_update(bytes).finalize());
180	len.map_or(hash.clone(), |len| String::from(&hash[0..len]))
181}
182
183fn is_file(path: &Path) -> bool {
184	path.exists() && metadata(path).unwrap().is_file()
185}