use std::{
fs::{create_dir_all, File},
io::{ErrorKind, Read, Write},
path::{Path, PathBuf},
process::Command,
};
use anyhow::Context;
use flate2::read::GzDecoder;
use futures_util::StreamExt;
use tar::Archive;
use tokio::io::AsyncWriteExt;
#[derive(Debug, PartialEq, Eq)]
pub enum Tool {
Binaryen,
Sass,
Tailwind,
}
pub fn app_path() -> PathBuf {
let data_local = dirs::data_local_dir().unwrap();
let dioxus_dir = data_local.join("dioxus");
if !dioxus_dir.is_dir() {
create_dir_all(&dioxus_dir).unwrap();
}
dioxus_dir
}
pub fn temp_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("temp");
if !temp_path.is_dir() {
create_dir_all(&temp_path).unwrap();
}
temp_path
}
pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
let target_dir = dir.parent().unwrap();
let dir_name = dir.file_name().unwrap();
let mut cmd = Command::new("git");
let cmd = cmd.current_dir(target_dir);
let res = cmd.arg("clone").arg(url).arg(dir_name).output();
if let Err(err) = res {
if ErrorKind::NotFound == err.kind() {
tracing::warn!("Git program not found. Hint: Install git or check $PATH.");
return Err(err.into());
}
}
Ok(())
}
pub fn tools_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("tools");
if !temp_path.is_dir() {
create_dir_all(&temp_path).unwrap();
}
temp_path
}
#[allow(clippy::should_implement_trait)]
impl Tool {
pub fn from_str(name: &str) -> Option<Self> {
match name {
"binaryen" => Some(Self::Binaryen),
"sass" => Some(Self::Sass),
"tailwindcss" => Some(Self::Tailwind),
_ => None,
}
}
pub fn name(&self) -> &str {
match self {
Self::Binaryen => "binaryen",
Self::Sass => "sass",
Self::Tailwind => "tailwindcss",
}
}
pub fn bin_path(&self) -> &str {
match self {
Self::Binaryen => "bin",
Self::Sass => ".",
Self::Tailwind => ".",
}
}
pub fn target_platform(&self) -> &str {
match self {
Self::Binaryen => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
Self::Sass => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
Self::Tailwind => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
}
}
pub fn tool_version(&self) -> &str {
match self {
Self::Binaryen => "version_105",
Self::Sass => "1.51.0",
Self::Tailwind => "v3.1.6",
}
}
pub fn download_url(&self) -> String {
match self {
Self::Binaryen => {
format!(
"https://github.com/WebAssembly/binaryen/releases/download/{version}/binaryen-{version}-x86_64-{target}.tar.gz",
version = self.tool_version(),
target = self.target_platform()
)
}
Self::Sass => {
format!(
"https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
version = self.tool_version(),
target = self.target_platform(),
extension = self.extension()
)
}
Self::Tailwind => {
let windows_extension = match self.target_platform() {
"windows" => ".exe",
_ => "",
};
format!(
"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
version = self.tool_version(),
target = self.target_platform(),
optional_ext = windows_extension
)
}
}
}
pub fn extension(&self) -> &str {
match self {
Self::Binaryen => "tar.gz",
Self::Sass => {
if cfg!(target_os = "windows") {
"zip"
} else {
"tar.gz"
}
}
Self::Tailwind => "bin",
}
}
pub fn is_installed(&self) -> bool {
tools_path().join(self.name()).is_dir()
}
pub fn temp_out_path(&self) -> PathBuf {
temp_path().join(format!("{}-tool.tmp", self.name()))
}
pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
let download_url = self.download_url();
let temp_out = self.temp_out_path();
let mut file = tokio::fs::File::create(&temp_out)
.await
.context("failed creating temporary output file")?;
let resp = reqwest::get(download_url).await.unwrap();
let mut res_bytes = resp.bytes_stream();
while let Some(chunk_res) = res_bytes.next().await {
let chunk = chunk_res.context("error reading chunk from download")?;
let _ = file.write(chunk.as_ref()).await;
}
Ok(temp_out)
}
pub async fn install_package(&self) -> anyhow::Result<()> {
let temp_path = self.temp_out_path();
let tool_path = tools_path();
let dir_name = match self {
Self::Binaryen => format!("binaryen-{}", self.tool_version()),
Self::Sass => "dart-sass".to_string(),
Self::Tailwind => self.name().to_string(),
};
if self.extension() == "tar.gz" {
let tar_gz = File::open(temp_path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(&tool_path)?;
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "zip" {
extract_zip(&temp_path, &tool_path)?;
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "bin" {
let bin_path = match self.target_platform() {
"windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
_ => tool_path.join(&dir_name).join(self.name()),
};
std::fs::create_dir(tool_path.join(dir_name))?;
let mut final_file = std::fs::File::create(&bin_path)?;
let mut temp_file = File::open(&temp_path)?;
let mut content = Vec::new();
temp_file.read_to_end(&mut content)?;
final_file.write_all(&content)?;
if self.target_platform() == "linux" {
let mut command = Command::new("chmod");
let _ = command
.args(vec!["+x", bin_path.to_str().unwrap()])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.output()?;
}
std::fs::remove_file(&temp_path)?;
}
Ok(())
}
pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
let bin_path = tools_path().join(self.name()).join(self.bin_path());
let command_file = match self {
Tool::Binaryen => {
if cfg!(target_os = "windows") {
format!("{}.exe", command)
} else {
command.to_string()
}
}
Tool::Sass => {
if cfg!(target_os = "windows") {
format!("{}.bat", command)
} else {
command.to_string()
}
}
Tool::Tailwind => {
if cfg!(target_os = "windows") {
format!("{}.exe", command)
} else {
command.to_string()
}
}
};
if !bin_path.join(&command_file).is_file() {
return Err(anyhow::anyhow!("Command file not found."));
}
let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
let output = command
.args(&args[..])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.output()?;
Ok(output.stdout)
}
}
pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
let zip_file = std::fs::File::open(file)?;
let mut zip = zip::ZipArchive::new(zip_file)?;
if !target.exists() {
std::fs::create_dir_all(target)?;
}
for i in 0..zip.len() {
let mut zip_entry = zip.by_index(i)?;
let Some(enclosed_name) = zip_entry.enclosed_name() else {
return Err(anyhow::anyhow!(
"Refusing to unpack zip entry with potentially dangerous path: zip={} entry={:?}",
file.display(),
zip_entry.name()
));
};
let output_path = target.join(enclosed_name);
if zip_entry.is_dir() {
std::fs::create_dir_all(output_path)?;
} else {
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut target_file = if !output_path.exists() {
std::fs::File::create(output_path)?
} else {
std::fs::File::open(output_path)?
};
let _num = std::io::copy(&mut zip_entry, &mut target_file)?;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::tempdir;
#[test]
fn test_extract_zip() -> anyhow::Result<()> {
let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/fixtures/test.zip");
let temp_dir = tempdir()?;
let temp_path = temp_dir.path();
extract_zip(path.as_path(), temp_path)?;
let expected_files = vec!["file1.txt", "file2.txt", "dir/file3.txt"];
for file in expected_files {
let path = temp_path.join(file);
assert!(path.exists(), "File not found: {:?}", path);
}
Ok(())
}
#[test]
fn test_extract_zip_dangerous_path() -> anyhow::Result<()> {
let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("tests/fixtures/dangerous.zip");
let temp_dir = tempdir()?;
let temp_path = temp_dir.path();
let result = extract_zip(path.as_path(), temp_path);
let err = result.unwrap_err();
assert!(err
.to_string()
.contains("Refusing to unpack zip entry with potentially dangerous path: zip="));
assert!(err.to_string().contains("entry=\"/etc/passwd\""));
Ok(())
}
}