tools.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. use std::{
  2. fs::{create_dir_all, File},
  3. io::{ErrorKind, Read, Write},
  4. path::{Path, PathBuf},
  5. process::Command,
  6. };
  7. use anyhow::Context;
  8. use flate2::read::GzDecoder;
  9. use futures_util::StreamExt;
  10. use tar::Archive;
  11. use tokio::io::AsyncWriteExt;
  12. #[derive(Debug, PartialEq, Eq)]
  13. pub enum Tool {
  14. Sass,
  15. Tailwind,
  16. }
  17. pub fn app_path() -> PathBuf {
  18. let data_local = dirs::data_local_dir().unwrap();
  19. let dioxus_dir = data_local.join("dioxus");
  20. if !dioxus_dir.is_dir() {
  21. create_dir_all(&dioxus_dir).unwrap();
  22. }
  23. dioxus_dir
  24. }
  25. pub fn temp_path() -> PathBuf {
  26. let app_path = app_path();
  27. let temp_path = app_path.join("temp");
  28. if !temp_path.is_dir() {
  29. create_dir_all(&temp_path).unwrap();
  30. }
  31. temp_path
  32. }
  33. pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
  34. let target_dir = dir.parent().unwrap();
  35. let dir_name = dir.file_name().unwrap();
  36. let mut cmd = Command::new("git");
  37. let cmd = cmd.current_dir(target_dir);
  38. let res = cmd.arg("clone").arg(url).arg(dir_name).output();
  39. if let Err(err) = res {
  40. if ErrorKind::NotFound == err.kind() {
  41. tracing::warn!("Git program not found. Hint: Install git or check $PATH.");
  42. return Err(err.into());
  43. }
  44. }
  45. Ok(())
  46. }
  47. pub fn tools_path() -> PathBuf {
  48. let app_path = app_path();
  49. let temp_path = app_path.join("tools");
  50. if !temp_path.is_dir() {
  51. create_dir_all(&temp_path).unwrap();
  52. }
  53. temp_path
  54. }
  55. #[allow(clippy::should_implement_trait)]
  56. impl Tool {
  57. /// from str to tool enum
  58. pub fn from_str(name: &str) -> Option<Self> {
  59. match name {
  60. "sass" => Some(Self::Sass),
  61. "tailwindcss" => Some(Self::Tailwind),
  62. _ => None,
  63. }
  64. }
  65. /// get current tool name str
  66. pub fn name(&self) -> &str {
  67. match self {
  68. Self::Sass => "sass",
  69. Self::Tailwind => "tailwindcss",
  70. }
  71. }
  72. /// get tool bin dir path
  73. pub fn bin_path(&self) -> &str {
  74. match self {
  75. Self::Sass => ".",
  76. Self::Tailwind => ".",
  77. }
  78. }
  79. /// get target platform
  80. pub fn target_platform(&self) -> &str {
  81. match self {
  82. Self::Sass => {
  83. if cfg!(target_os = "windows") {
  84. "windows"
  85. } else if cfg!(target_os = "macos") {
  86. "macos"
  87. } else if cfg!(target_os = "linux") {
  88. "linux"
  89. } else {
  90. panic!("unsupported platformm");
  91. }
  92. }
  93. Self::Tailwind => {
  94. if cfg!(target_os = "windows") {
  95. "windows"
  96. } else if cfg!(target_os = "macos") {
  97. "macos"
  98. } else if cfg!(target_os = "linux") {
  99. "linux"
  100. } else {
  101. panic!("unsupported platformm");
  102. }
  103. }
  104. }
  105. }
  106. /// get tool version
  107. pub fn tool_version(&self) -> &str {
  108. match self {
  109. Self::Sass => "1.51.0",
  110. Self::Tailwind => "v3.1.6",
  111. }
  112. }
  113. /// get tool package download url
  114. pub fn download_url(&self) -> String {
  115. match self {
  116. Self::Sass => {
  117. format!(
  118. "https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
  119. version = self.tool_version(),
  120. target = self.target_platform(),
  121. extension = self.extension()
  122. )
  123. }
  124. Self::Tailwind => {
  125. let windows_extension = match self.target_platform() {
  126. "windows" => ".exe",
  127. _ => "",
  128. };
  129. format!(
  130. "https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
  131. version = self.tool_version(),
  132. target = self.target_platform(),
  133. optional_ext = windows_extension
  134. )
  135. }
  136. }
  137. }
  138. /// get package extension name
  139. pub fn extension(&self) -> &str {
  140. match self {
  141. Self::Sass => {
  142. if cfg!(target_os = "windows") {
  143. "zip"
  144. } else {
  145. "tar.gz"
  146. }
  147. }
  148. Self::Tailwind => "bin",
  149. }
  150. }
  151. /// check tool state
  152. pub fn is_installed(&self) -> bool {
  153. tools_path().join(self.name()).is_dir()
  154. }
  155. /// get download temp path
  156. pub fn temp_out_path(&self) -> PathBuf {
  157. temp_path().join(format!("{}-tool.tmp", self.name()))
  158. }
  159. /// start to download package
  160. pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
  161. let download_url = self.download_url();
  162. let temp_out = self.temp_out_path();
  163. let mut file = tokio::fs::File::create(&temp_out)
  164. .await
  165. .context("failed creating temporary output file")?;
  166. let resp = reqwest::get(download_url).await.unwrap();
  167. let mut res_bytes = resp.bytes_stream();
  168. while let Some(chunk_res) = res_bytes.next().await {
  169. let chunk = chunk_res.context("error reading chunk from download")?;
  170. let _ = file.write(chunk.as_ref()).await;
  171. }
  172. // tracing::info!("temp file path: {:?}", temp_out);
  173. Ok(temp_out)
  174. }
  175. /// start to install package
  176. pub async fn install_package(&self) -> anyhow::Result<()> {
  177. let temp_path = self.temp_out_path();
  178. let tool_path = tools_path();
  179. let dir_name = match self {
  180. Self::Sass => "dart-sass".to_string(),
  181. Self::Tailwind => self.name().to_string(),
  182. };
  183. if self.extension() == "tar.gz" {
  184. let tar_gz = File::open(temp_path)?;
  185. let tar = GzDecoder::new(tar_gz);
  186. let mut archive = Archive::new(tar);
  187. archive.unpack(&tool_path)?;
  188. std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
  189. } else if self.extension() == "zip" {
  190. // decompress the `zip` file
  191. extract_zip(&temp_path, &tool_path)?;
  192. std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
  193. } else if self.extension() == "bin" {
  194. let bin_path = match self.target_platform() {
  195. "windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
  196. _ => tool_path.join(&dir_name).join(self.name()),
  197. };
  198. // Manualy creating tool directory because we directly download the binary via Github
  199. std::fs::create_dir(tool_path.join(dir_name))?;
  200. let mut final_file = std::fs::File::create(&bin_path)?;
  201. let mut temp_file = File::open(&temp_path)?;
  202. let mut content = Vec::new();
  203. temp_file.read_to_end(&mut content)?;
  204. final_file.write_all(&content)?;
  205. if self.target_platform() == "linux" {
  206. // This code does not update permissions idk why
  207. // let mut perms = final_file.metadata()?.permissions();
  208. // perms.set_mode(0o744);
  209. // Adding to the binary execution rights with "chmod"
  210. let mut command = Command::new("chmod");
  211. let _ = command
  212. .args(vec!["+x", bin_path.to_str().unwrap()])
  213. .stdout(std::process::Stdio::inherit())
  214. .stderr(std::process::Stdio::inherit())
  215. .output()?;
  216. }
  217. std::fs::remove_file(&temp_path)?;
  218. }
  219. Ok(())
  220. }
  221. pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
  222. let bin_path = tools_path().join(self.name()).join(self.bin_path());
  223. let command_file = match self {
  224. Tool::Sass => {
  225. if cfg!(target_os = "windows") {
  226. format!("{}.bat", command)
  227. } else {
  228. command.to_string()
  229. }
  230. }
  231. Tool::Tailwind => {
  232. if cfg!(target_os = "windows") {
  233. format!("{}.exe", command)
  234. } else {
  235. command.to_string()
  236. }
  237. }
  238. };
  239. if !bin_path.join(&command_file).is_file() {
  240. return Err(anyhow::anyhow!("Command file not found."));
  241. }
  242. let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
  243. let output = command
  244. .args(&args[..])
  245. .stdout(std::process::Stdio::inherit())
  246. .stderr(std::process::Stdio::inherit())
  247. .output()?;
  248. Ok(output.stdout)
  249. }
  250. }
  251. pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
  252. let zip_file = std::fs::File::open(file)?;
  253. let mut zip = zip::ZipArchive::new(zip_file)?;
  254. if !target.exists() {
  255. std::fs::create_dir_all(target)?;
  256. }
  257. for i in 0..zip.len() {
  258. let mut zip_entry = zip.by_index(i)?;
  259. // check for dangerous paths
  260. // see https://docs.rs/zip/latest/zip/read/struct.ZipFile.html#warnings
  261. let Some(enclosed_name) = zip_entry.enclosed_name() else {
  262. return Err(anyhow::anyhow!(
  263. "Refusing to unpack zip entry with potentially dangerous path: zip={} entry={:?}",
  264. file.display(),
  265. zip_entry.name()
  266. ));
  267. };
  268. let output_path = target.join(enclosed_name);
  269. if zip_entry.is_dir() {
  270. std::fs::create_dir_all(output_path)?;
  271. } else {
  272. // create parent dirs if needed
  273. if let Some(parent) = output_path.parent() {
  274. std::fs::create_dir_all(parent)?;
  275. }
  276. // extract file
  277. let mut target_file = if !output_path.exists() {
  278. std::fs::File::create(output_path)?
  279. } else {
  280. std::fs::File::open(output_path)?
  281. };
  282. let _num = std::io::copy(&mut zip_entry, &mut target_file)?;
  283. }
  284. }
  285. Ok(())
  286. }
  287. #[cfg(test)]
  288. mod test {
  289. use super::*;
  290. use tempfile::tempdir;
  291. #[test]
  292. fn test_extract_zip() -> anyhow::Result<()> {
  293. let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
  294. .join("tests/fixtures/test.zip");
  295. let temp_dir = tempdir()?;
  296. let temp_path = temp_dir.path();
  297. extract_zip(path.as_path(), temp_path)?;
  298. let expected_files = vec!["file1.txt", "file2.txt", "dir/file3.txt"];
  299. for file in expected_files {
  300. let path = temp_path.join(file);
  301. assert!(path.exists(), "File not found: {:?}", path);
  302. }
  303. Ok(())
  304. }
  305. #[test]
  306. fn test_extract_zip_dangerous_path() -> anyhow::Result<()> {
  307. let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
  308. .join("tests/fixtures/dangerous.zip");
  309. let temp_dir = tempdir()?;
  310. let temp_path = temp_dir.path();
  311. let result = extract_zip(path.as_path(), temp_path);
  312. let err = result.unwrap_err();
  313. assert!(err
  314. .to_string()
  315. .contains("Refusing to unpack zip entry with potentially dangerous path: zip="));
  316. assert!(err.to_string().contains("entry=\"/etc/passwd\""));
  317. Ok(())
  318. }
  319. }