1use 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}