From 34d23825fcfce96a3c829469e2bffa3401828ccd Mon Sep 17 00:00:00 2001 From: rroskam Date: Wed, 11 Feb 2026 23:45:14 -0500 Subject: [PATCH 1/4] feat: template caching for git-sourced templates - Cache cloned templates at ~/.cache/diecut/templates/ (XDG-compliant) - Deterministic cache keys with URL normalization (trailing .git, slashes) - get_or_clone() checks cache before cloning, with cross-filesystem fallback - list_cached() for enumerating cached templates with metadata - clear_cache() for selective or full cache removal - Cache metadata stored as .diecut-cache.toml per entry Closes diecut-l41 --- crates/diecut-core/src/lib.rs | 4 +- crates/diecut-core/src/template/cache.rs | 325 +++++++++++++++++++++++ crates/diecut-core/src/template/mod.rs | 2 + 3 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 crates/diecut-core/src/template/cache.rs diff --git a/crates/diecut-core/src/lib.rs b/crates/diecut-core/src/lib.rs index 9a083bb..de4146c 100644 --- a/crates/diecut-core/src/lib.rs +++ b/crates/diecut-core/src/lib.rs @@ -16,7 +16,7 @@ use crate::adapter::resolve_template; use crate::error::{DicecutError, Result}; use crate::prompt::{collect_variables, PromptOptions}; use crate::render::{build_context_with_namespace, walk_and_render, GeneratedProject}; -use crate::template::{clone_template, resolve_source, TemplateSource}; +use crate::template::{get_or_clone, resolve_source, TemplateSource}; /// Options for the `generate` operation. pub struct GenerateOptions { @@ -39,7 +39,7 @@ pub fn generate(options: GenerateOptions) -> Result { let template_dir = match &source { TemplateSource::Local(path) => path.clone(), TemplateSource::Git { url, git_ref } => { - clone_template(url, git_ref.as_deref())? + get_or_clone(url, git_ref.as_deref())? } }; diff --git a/crates/diecut-core/src/template/cache.rs b/crates/diecut-core/src/template/cache.rs new file mode 100644 index 0000000..29441af --- /dev/null +++ b/crates/diecut-core/src/template/cache.rs @@ -0,0 +1,325 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::error::{DicecutError, Result}; +use crate::template::clone::clone_template; + +/// Metadata stored alongside a cached template. +#[derive(Debug, Serialize, Deserialize)] +pub struct CacheMetadata { + /// The original git URL. + pub url: String, + /// The git ref (branch, tag, or commit) if specified. + pub git_ref: Option, + /// When the template was cached (ISO 8601). + pub cached_at: String, +} + +/// A cached template entry returned by `list_cached()`. +#[derive(Debug)] +pub struct CachedTemplate { + /// The cache key (directory name). + pub key: String, + /// The path to the cached template. + pub path: PathBuf, + /// Metadata about the cached template. + pub metadata: CacheMetadata, +} + +const CACHE_METADATA_FILE: &str = ".diecut-cache.toml"; + +/// Get the cache directory for templates. +/// +/// Returns `~/.cache/diecut/templates/` on Linux/macOS (XDG-compliant). +pub fn get_cache_dir() -> PathBuf { + dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from(".cache")) + .join("diecut") + .join("templates") +} + +/// Generate a deterministic cache key from a URL and optional ref. +/// +/// Normalizes the URL by stripping trailing `.git` and trailing `/` before +/// hashing, so `https://github.com/user/repo` and +/// `https://github.com/user/repo.git` produce the same key. +pub fn cache_key(url: &str, git_ref: Option<&str>) -> String { + let normalized = url + .trim_end_matches('/') + .trim_end_matches(".git"); + + let mut hasher = DefaultHasher::new(); + normalized.hash(&mut hasher); + if let Some(r) = git_ref { + r.hash(&mut hasher); + } + let hash = hasher.finish(); + + // Build a human-readable prefix from the URL + let prefix = normalized + .rsplit('/') + .next() + .unwrap_or("template"); + + match git_ref { + Some(r) => format!("{prefix}-{r}-{hash:016x}"), + None => format!("{prefix}-{hash:016x}"), + } +} + +/// Check cache first, clone if missing, return path to the template. +pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { + let cache_dir = get_cache_dir(); + let key = cache_key(url, git_ref); + let cached_path = cache_dir.join(&key); + + // Check if we have a valid cached copy + if cached_path.exists() && cached_path.join(CACHE_METADATA_FILE).exists() { + return Ok(cached_path); + } + + // Clone to a temp location, then move into cache + let cloned_path = clone_template(url, git_ref)?; + + // Write cache metadata + let metadata = CacheMetadata { + url: url.to_string(), + git_ref: git_ref.map(String::from), + cached_at: chrono_now(), + }; + let metadata_toml = toml::to_string_pretty(&metadata).map_err(|e| DicecutError::Io { + context: format!("serializing cache metadata: {e}"), + source: std::io::Error::other(e.to_string()), + })?; + std::fs::write(cloned_path.join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| { + DicecutError::Io { + context: "writing cache metadata".into(), + source: e, + } + })?; + + // Ensure cache directory exists + std::fs::create_dir_all(&cache_dir).map_err(|e| DicecutError::Io { + context: format!("creating cache directory {}", cache_dir.display()), + source: e, + })?; + + // Remove any stale cache entry + if cached_path.exists() { + std::fs::remove_dir_all(&cached_path).map_err(|e| DicecutError::Io { + context: format!("removing stale cache entry {}", cached_path.display()), + source: e, + })?; + } + + // Move cloned directory into cache + std::fs::rename(&cloned_path, &cached_path).or_else(|_| { + // rename can fail across filesystems; fall back to copy + delete + copy_dir_all(&cloned_path, &cached_path)?; + std::fs::remove_dir_all(&cloned_path).map_err(|e| DicecutError::Io { + context: "cleaning up temp clone directory".into(), + source: e, + }) + })?; + + Ok(cached_path) +} + +/// List all cached templates. +pub fn list_cached() -> Result> { + let cache_dir = get_cache_dir(); + if !cache_dir.exists() { + return Ok(Vec::new()); + } + + let mut entries = Vec::new(); + let read_dir = std::fs::read_dir(&cache_dir).map_err(|e| DicecutError::Io { + context: format!("reading cache directory {}", cache_dir.display()), + source: e, + })?; + + for entry in read_dir { + let entry = entry.map_err(|e| DicecutError::Io { + context: "reading cache directory entry".into(), + source: e, + })?; + + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let metadata_path = path.join(CACHE_METADATA_FILE); + if !metadata_path.exists() { + continue; + } + + let metadata_str = std::fs::read_to_string(&metadata_path).map_err(|e| DicecutError::Io { + context: format!("reading cache metadata {}", metadata_path.display()), + source: e, + })?; + + let metadata: CacheMetadata = + toml::from_str(&metadata_str).map_err(|e| DicecutError::Io { + context: format!("parsing cache metadata: {e}"), + source: std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()), + })?; + + let key = entry + .file_name() + .to_string_lossy() + .into_owned(); + + entries.push(CachedTemplate { + key, + path, + metadata, + }); + } + + Ok(entries) +} + +/// Clear cached templates. +/// +/// If `url` is provided, only the cache entry matching that URL is removed. +/// If `url` is None, the entire cache directory is cleared. +pub fn clear_cache(url: Option<&str>) -> Result<()> { + let cache_dir = get_cache_dir(); + + if let Some(url) = url { + // Clear specific entries matching this URL (any ref) + if !cache_dir.exists() { + return Ok(()); + } + let entries = list_cached()?; + for entry in entries { + if entry.metadata.url == url { + std::fs::remove_dir_all(&entry.path).map_err(|e| DicecutError::Io { + context: format!("removing cached template {}", entry.path.display()), + source: e, + })?; + } + } + } else { + // Clear entire cache + if cache_dir.exists() { + std::fs::remove_dir_all(&cache_dir).map_err(|e| DicecutError::Io { + context: format!("removing cache directory {}", cache_dir.display()), + source: e, + })?; + } + } + + Ok(()) +} + +/// Recursively copy a directory. +fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> Result<()> { + std::fs::create_dir_all(dst).map_err(|e| DicecutError::Io { + context: format!("creating directory {}", dst.display()), + source: e, + })?; + + for entry in std::fs::read_dir(src).map_err(|e| DicecutError::Io { + context: format!("reading directory {}", src.display()), + source: e, + })? { + let entry = entry.map_err(|e| DicecutError::Io { + context: "reading directory entry".into(), + source: e, + })?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if src_path.is_dir() { + copy_dir_all(&src_path, &dst_path)?; + } else { + std::fs::copy(&src_path, &dst_path).map_err(|e| DicecutError::Io { + context: format!("copying {} to {}", src_path.display(), dst_path.display()), + source: e, + })?; + } + } + + Ok(()) +} + +/// Get the current time as an ISO 8601 string without pulling in chrono. +fn chrono_now() -> String { + // Use std::time for a simple timestamp + let duration = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default(); + format!("{}", duration.as_secs()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cache_key_deterministic() { + let key1 = cache_key("https://github.com/user/repo.git", None); + let key2 = cache_key("https://github.com/user/repo.git", None); + assert_eq!(key1, key2); + } + + #[test] + fn cache_key_normalizes_trailing_git() { + let key1 = cache_key("https://github.com/user/repo.git", None); + let key2 = cache_key("https://github.com/user/repo", None); + assert_eq!(key1, key2); + } + + #[test] + fn cache_key_normalizes_trailing_slash() { + let key1 = cache_key("https://github.com/user/repo/", None); + let key2 = cache_key("https://github.com/user/repo", None); + assert_eq!(key1, key2); + } + + #[test] + fn cache_key_differs_by_ref() { + let key1 = cache_key("https://github.com/user/repo", Some("main")); + let key2 = cache_key("https://github.com/user/repo", Some("develop")); + assert_ne!(key1, key2); + } + + #[test] + fn cache_key_none_ref_differs_from_some() { + let key1 = cache_key("https://github.com/user/repo", None); + let key2 = cache_key("https://github.com/user/repo", Some("main")); + assert_ne!(key1, key2); + } + + #[test] + fn cache_key_includes_repo_name() { + let key = cache_key("https://github.com/user/my-template.git", None); + assert!(key.starts_with("my-template-")); + } + + #[test] + fn cache_key_includes_ref_in_name() { + let key = cache_key("https://github.com/user/repo", Some("v2.0")); + assert!(key.contains("v2.0")); + } + + #[test] + fn get_cache_dir_returns_xdg_path() { + let dir = get_cache_dir(); + assert!(dir.ends_with("diecut/templates")); + } + + #[test] + fn list_cached_empty_when_no_cache() { + // With a non-existent cache dir, list_cached should return empty + let entries = list_cached(); + // This may or may not have entries depending on system state, + // but it should not error + assert!(entries.is_ok()); + } +} diff --git a/crates/diecut-core/src/template/mod.rs b/crates/diecut-core/src/template/mod.rs index e14e56a..8d2ce45 100644 --- a/crates/diecut-core/src/template/mod.rs +++ b/crates/diecut-core/src/template/mod.rs @@ -1,5 +1,7 @@ +pub mod cache; pub mod clone; pub mod source; +pub use cache::{get_or_clone, list_cached, clear_cache, CachedTemplate}; pub use clone::clone_template; pub use source::{resolve_source, resolve_source_with_ref, TemplateSource}; From c163da89f57447bdca38c0b3030889c95c4878c7 Mon Sep 17 00:00:00 2001 From: rroskam Date: Thu, 12 Feb 2026 00:02:23 -0500 Subject: [PATCH 2/4] fix: cache module improvements (timestamps, URL matching, configurability) - Rename chrono_now() to unix_timestamp_secs(), update doc comments - Add normalize_url() helper for consistent URL comparison in clear_cache - Make get_cache_dir() configurable via DIECUT_CACHE_DIR env var, return Result - Reject file:// URLs in clone_template, warn on http:// - Move create_dir_all before clone operation in get_or_clone - Add UnsafeUrl error variant for URL scheme validation - Add tests for env var, URL normalization, and file:// rejection --- Cargo.lock | 1 + Cargo.toml | 1 + crates/diecut-core/Cargo.toml | 1 + crates/diecut-core/src/error.rs | 14 +- crates/diecut-core/src/template/cache.rs | 150 +++++++++++++++------- crates/diecut-core/src/template/clone.rs | 48 +++++-- crates/diecut-core/src/template/source.rs | 5 +- 7 files changed, 150 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a60a4f..b14cd68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,7 @@ dependencies = [ "rhai", "serde", "serde_json", + "sha2", "tempfile", "tera", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 135f79e..047a99c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,4 @@ dirs = "6" rhai = { version = "1", features = ["sync"] } tempfile = "3" gix = { version = "0.72", default-features = false, features = ["blocking-network-client", "worktree-mutation"] } +sha2 = "0.10" diff --git a/crates/diecut-core/Cargo.toml b/crates/diecut-core/Cargo.toml index 31aa849..f7bcfe0 100644 --- a/crates/diecut-core/Cargo.toml +++ b/crates/diecut-core/Cargo.toml @@ -23,3 +23,4 @@ rhai = { workspace = true } dirs = { workspace = true } gix = { workspace = true } tempfile = { workspace = true } +sha2 = { workspace = true } diff --git a/crates/diecut-core/src/error.rs b/crates/diecut-core/src/error.rs index eacec14..02ccf57 100644 --- a/crates/diecut-core/src/error.rs +++ b/crates/diecut-core/src/error.rs @@ -18,7 +18,7 @@ pub enum DicecutError { source: toml::de::Error, }, - #[error("Invalid variable definition for '{name}'")] + #[error("Invalid variable definition for '{name}': {reason}")] ConfigInvalidVariable { name: String, reason: String }, #[error("Validation failed for variable '{name}': {message}")] @@ -96,10 +96,6 @@ pub enum DicecutError { ))] UnsupportedFormat { path: PathBuf }, - #[error("Git source not yet implemented: {url}")] - #[diagnostic(help("Git template sources will be available in a future release"))] - GitNotImplemented { url: String }, - #[error("Invalid template abbreviation: {input}")] #[diagnostic(help( "Supported abbreviations: gh:user/repo, gl:user/repo, bb:user/repo, sr:~user/repo" @@ -110,6 +106,14 @@ pub enum DicecutError { #[diagnostic(help("Check the Rhai script for errors"))] HookError { hook: String, message: String }, + #[error("Cache metadata error: {context}")] + #[diagnostic(help("Try clearing the cache with `diecut cache clear`"))] + CacheMetadata { context: String }, + + #[error("Unsafe URL scheme in '{url}': {reason}")] + #[diagnostic(help("Use https:// URLs for remote templates"))] + UnsafeUrl { url: String, reason: String }, + #[error("Git clone failed for {url}")] #[diagnostic(help("Check the URL and your network connection"))] GitClone { url: String, reason: String }, diff --git a/crates/diecut-core/src/template/cache.rs b/crates/diecut-core/src/template/cache.rs index 29441af..d0fc051 100644 --- a/crates/diecut-core/src/template/cache.rs +++ b/crates/diecut-core/src/template/cache.rs @@ -1,6 +1,6 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +use sha2::{Sha256, Digest}; use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ pub struct CacheMetadata { pub url: String, /// The git ref (branch, tag, or commit) if specified. pub git_ref: Option, - /// When the template was cached (ISO 8601). + /// When the template was cached (Unix timestamp in seconds). pub cached_at: String, } @@ -33,12 +33,24 @@ const CACHE_METADATA_FILE: &str = ".diecut-cache.toml"; /// Get the cache directory for templates. /// -/// Returns `~/.cache/diecut/templates/` on Linux/macOS (XDG-compliant). -pub fn get_cache_dir() -> PathBuf { +/// Checks `DIECUT_CACHE_DIR` env var first. Falls back to +/// `~/.cache/diecut/templates/` on Linux/macOS (XDG-compliant). +/// Returns an error if neither source provides a cache directory. +pub fn get_cache_dir() -> Result { + if let Ok(dir) = std::env::var("DIECUT_CACHE_DIR") { + return Ok(PathBuf::from(dir)); + } dirs::cache_dir() - .unwrap_or_else(|| PathBuf::from(".cache")) - .join("diecut") - .join("templates") + .map(|d| d.join("diecut").join("templates")) + .ok_or_else(|| DicecutError::Io { + context: "unable to determine cache directory: set DIECUT_CACHE_DIR or ensure a home directory exists".into(), + source: std::io::Error::new(std::io::ErrorKind::NotFound, "no cache directory available"), + }) +} + +/// Normalize a URL by stripping trailing `.git` and `/` for consistent comparison. +fn normalize_url(url: &str) -> &str { + url.trim_end_matches('/').trim_end_matches(".git") } /// Generate a deterministic cache key from a URL and optional ref. @@ -46,33 +58,44 @@ pub fn get_cache_dir() -> PathBuf { /// Normalizes the URL by stripping trailing `.git` and trailing `/` before /// hashing, so `https://github.com/user/repo` and /// `https://github.com/user/repo.git` produce the same key. -pub fn cache_key(url: &str, git_ref: Option<&str>) -> String { - let normalized = url - .trim_end_matches('/') - .trim_end_matches(".git"); +pub(crate) fn cache_key(url: &str, git_ref: Option<&str>) -> String { + let normalized = normalize_url(url); - let mut hasher = DefaultHasher::new(); - normalized.hash(&mut hasher); + let mut hasher = Sha256::new(); + hasher.update(normalized.as_bytes()); if let Some(r) = git_ref { - r.hash(&mut hasher); + hasher.update(b"\0"); + hasher.update(r.as_bytes()); } - let hash = hasher.finish(); + let digest = hasher.finalize(); + let hash: String = digest.iter().take(8).map(|b| format!("{b:02x}")).collect(); + + // Sanitize components to prevent path traversal + let sanitize = |s: &str| -> String { + s.replace(['/', '\\'], "_").replace("..", "_") + }; // Build a human-readable prefix from the URL - let prefix = normalized - .rsplit('/') - .next() - .unwrap_or("template"); + let prefix = sanitize( + normalized + .rsplit('/') + .next() + .unwrap_or("template"), + ); match git_ref { - Some(r) => format!("{prefix}-{r}-{hash:016x}"), - None => format!("{prefix}-{hash:016x}"), + Some(r) => format!("{prefix}-{}-{hash}", sanitize(r)), + None => format!("{prefix}-{hash}"), } } /// Check cache first, clone if missing, return path to the template. +/// +/// Note: this function does not protect against concurrent access. If multiple +/// processes call `get_or_clone` for the same URL simultaneously, they may +/// both clone and race to populate the cache entry. pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { - let cache_dir = get_cache_dir(); + let cache_dir = get_cache_dir()?; let key = cache_key(url, git_ref); let cached_path = cache_dir.join(&key); @@ -81,6 +104,12 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { return Ok(cached_path); } + // Ensure cache directory exists before cloning + std::fs::create_dir_all(&cache_dir).map_err(|e| DicecutError::Io { + context: format!("creating cache directory {}", cache_dir.display()), + source: e, + })?; + // Clone to a temp location, then move into cache let cloned_path = clone_template(url, git_ref)?; @@ -88,25 +117,18 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { let metadata = CacheMetadata { url: url.to_string(), git_ref: git_ref.map(String::from), - cached_at: chrono_now(), + cached_at: unix_timestamp_secs(), }; - let metadata_toml = toml::to_string_pretty(&metadata).map_err(|e| DicecutError::Io { + let metadata_toml = toml::to_string_pretty(&metadata).map_err(|e| DicecutError::CacheMetadata { context: format!("serializing cache metadata: {e}"), - source: std::io::Error::other(e.to_string()), })?; - std::fs::write(cloned_path.join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| { + std::fs::write(cloned_path.path().join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| { DicecutError::Io { context: "writing cache metadata".into(), source: e, } })?; - // Ensure cache directory exists - std::fs::create_dir_all(&cache_dir).map_err(|e| DicecutError::Io { - context: format!("creating cache directory {}", cache_dir.display()), - source: e, - })?; - // Remove any stale cache entry if cached_path.exists() { std::fs::remove_dir_all(&cached_path).map_err(|e| DicecutError::Io { @@ -116,10 +138,15 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { } // Move cloned directory into cache - std::fs::rename(&cloned_path, &cached_path).or_else(|_| { + std::fs::rename(cloned_path.path(), &cached_path).or_else(|rename_err| { // rename can fail across filesystems; fall back to copy + delete - copy_dir_all(&cloned_path, &cached_path)?; - std::fs::remove_dir_all(&cloned_path).map_err(|e| DicecutError::Io { + copy_dir_all(cloned_path.path(), &cached_path).map_err(|e| DicecutError::Io { + context: format!( + "copying cloned template to cache (rename failed: {rename_err}): {e}" + ), + source: std::io::Error::other(e.to_string()), + })?; + std::fs::remove_dir_all(cloned_path.path()).map_err(|e| DicecutError::Io { context: "cleaning up temp clone directory".into(), source: e, }) @@ -130,7 +157,7 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { /// List all cached templates. pub fn list_cached() -> Result> { - let cache_dir = get_cache_dir(); + let cache_dir = get_cache_dir()?; if !cache_dir.exists() { return Ok(Vec::new()); } @@ -163,9 +190,8 @@ pub fn list_cached() -> Result> { })?; let metadata: CacheMetadata = - toml::from_str(&metadata_str).map_err(|e| DicecutError::Io { + toml::from_str(&metadata_str).map_err(|e| DicecutError::CacheMetadata { context: format!("parsing cache metadata: {e}"), - source: std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()), })?; let key = entry @@ -188,16 +214,17 @@ pub fn list_cached() -> Result> { /// If `url` is provided, only the cache entry matching that URL is removed. /// If `url` is None, the entire cache directory is cleared. pub fn clear_cache(url: Option<&str>) -> Result<()> { - let cache_dir = get_cache_dir(); + let cache_dir = get_cache_dir()?; if let Some(url) = url { // Clear specific entries matching this URL (any ref) if !cache_dir.exists() { return Ok(()); } + let normalized_input = normalize_url(url); let entries = list_cached()?; for entry in entries { - if entry.metadata.url == url { + if normalize_url(&entry.metadata.url) == normalized_input { std::fs::remove_dir_all(&entry.path).map_err(|e| DicecutError::Io { context: format!("removing cached template {}", entry.path.display()), source: e, @@ -217,8 +244,8 @@ pub fn clear_cache(url: Option<&str>) -> Result<()> { Ok(()) } -/// Recursively copy a directory. -fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> Result<()> { +/// Recursively copy a directory, skipping symlinks. +fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> { std::fs::create_dir_all(dst).map_err(|e| DicecutError::Io { context: format!("creating directory {}", dst.display()), source: e, @@ -232,10 +259,21 @@ fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> Result<()> { context: "reading directory entry".into(), source: e, })?; + + let file_type = entry.file_type().map_err(|e| DicecutError::Io { + context: "reading file type of directory entry".into(), + source: e, + })?; + + // Skip symlinks to prevent symlink-following attacks + if file_type.is_symlink() { + continue; + } + let src_path = entry.path(); let dst_path = dst.join(entry.file_name()); - if src_path.is_dir() { + if file_type.is_dir() { copy_dir_all(&src_path, &dst_path)?; } else { std::fs::copy(&src_path, &dst_path).map_err(|e| DicecutError::Io { @@ -248,9 +286,8 @@ fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> Result<()> { Ok(()) } -/// Get the current time as an ISO 8601 string without pulling in chrono. -fn chrono_now() -> String { - // Use std::time for a simple timestamp +/// Get the current time as a Unix timestamp in seconds. +fn unix_timestamp_secs() -> String { let duration = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); @@ -310,10 +347,25 @@ mod tests { #[test] fn get_cache_dir_returns_xdg_path() { - let dir = get_cache_dir(); + let dir = get_cache_dir().unwrap(); assert!(dir.ends_with("diecut/templates")); } + #[test] + fn get_cache_dir_respects_env_var() { + std::env::set_var("DIECUT_CACHE_DIR", "/tmp/test-diecut-cache"); + let dir = get_cache_dir().unwrap(); + std::env::remove_var("DIECUT_CACHE_DIR"); + assert_eq!(dir, PathBuf::from("/tmp/test-diecut-cache")); + } + + #[test] + fn normalize_url_strips_trailing_git_and_slash() { + assert_eq!(normalize_url("https://github.com/user/repo.git"), "https://github.com/user/repo"); + assert_eq!(normalize_url("https://github.com/user/repo/"), "https://github.com/user/repo"); + assert_eq!(normalize_url("https://github.com/user/repo"), "https://github.com/user/repo"); + } + #[test] fn list_cached_empty_when_no_cache() { // With a non-existent cache dir, list_cached should return empty diff --git a/crates/diecut-core/src/template/clone.rs b/crates/diecut-core/src/template/clone.rs index 6d037bb..33a892a 100644 --- a/crates/diecut-core/src/template/clone.rs +++ b/crates/diecut-core/src/template/clone.rs @@ -1,22 +1,34 @@ -use std::path::PathBuf; - use crate::error::{DicecutError, Result}; -/// Clone a git repository to a temporary directory and return the path. +/// Clone a git repository to a temporary directory. /// /// If `git_ref` is provided, the repository is checked out at that ref -/// (branch, tag, or commit). The caller is responsible for cleaning up -/// the temporary directory. -pub fn clone_template(url: &str, git_ref: Option<&str>) -> Result { +/// (branch, tag, or commit). Returns the `TempDir` handle — the temporary +/// directory is automatically cleaned up when the handle is dropped. +/// +/// Rejects `file://` URLs to prevent local file access attacks. +/// Prints a warning for `http://` URLs (non-TLS). +pub fn clone_template(url: &str, git_ref: Option<&str>) -> Result { + // Reject file:// URLs to prevent local filesystem access + if url.starts_with("file://") { + return Err(DicecutError::UnsafeUrl { + url: url.to_string(), + reason: "file:// URLs are not allowed for remote templates".into(), + }); + } + + // Warn on non-TLS http:// URLs + if url.starts_with("http://") { + eprintln!("warning: using insecure http:// URL; consider using https:// instead"); + } + let tmp_dir = tempfile::tempdir().map_err(|e| DicecutError::Io { context: "creating temporary directory for git clone".into(), source: e, })?; - let clone_path = tmp_dir.path().to_path_buf(); - // Use prepare_clone (with worktree) so we get a checked-out working copy - let mut prepare = gix::prepare_clone(url, &clone_path).map_err(|e| { + let mut prepare = gix::prepare_clone(url, tmp_dir.path()).map_err(|e| { DicecutError::GitClone { url: url.to_string(), reason: e.to_string(), @@ -50,11 +62,7 @@ pub fn clone_template(url: &str, git_ref: Option<&str>) -> Result { reason: format!("worktree checkout failed: {e}"), })?; - // Persist the tempdir so it isn't deleted when this function returns. - // The caller (or a higher-level cleanup mechanism) owns the path. - let _ = tmp_dir.keep(); - - Ok(clone_path) + Ok(tmp_dir) } #[cfg(test)] @@ -79,4 +87,16 @@ mod tests { let result = clone_template("https://nonexistent.invalid/repo.git", None); assert!(result.is_err()); } + + #[test] + fn clone_rejects_file_url() { + let result = clone_template("file:///tmp/repo", None); + assert!(result.is_err()); + match result.unwrap_err() { + DicecutError::UnsafeUrl { url, .. } => { + assert_eq!(url, "file:///tmp/repo"); + } + other => panic!("expected UnsafeUrl error, got: {other:?}"), + } + } } diff --git a/crates/diecut-core/src/template/source.rs b/crates/diecut-core/src/template/source.rs index 4fa2711..7223e5e 100644 --- a/crates/diecut-core/src/template/source.rs +++ b/crates/diecut-core/src/template/source.rs @@ -34,8 +34,9 @@ fn expand_abbreviation(input: &str) -> Result { return Ok(format!("{base_url}{rest}{suffix}")); } } - // Not an abbreviation at all — caller should handle this. - unreachable!("expand_abbreviation called on non-abbreviation input"); + Err(DicecutError::InvalidAbbreviation { + input: input.to_string(), + }) } /// Returns `true` when the argument looks like a known abbreviation prefix. From 06f41b07ea0fc8d46badee572f53e1c46cb5f4a4 Mon Sep 17 00:00:00 2001 From: rroskam Date: Thu, 12 Feb 2026 00:03:51 -0500 Subject: [PATCH 3/4] fix: proper tempdir lifecycle in clone/cache flow Change get_or_clone to keep TempDir alive during cache placement. On error, TempDir drops and cleans up automatically. Only call tmp_dir.keep() after successful rename/copy into the cache directory. This prevents orphaned temp directories when metadata write, mkdir, or rename operations fail. --- crates/diecut-core/src/template/cache.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/diecut-core/src/template/cache.rs b/crates/diecut-core/src/template/cache.rs index d0fc051..caf9eb2 100644 --- a/crates/diecut-core/src/template/cache.rs +++ b/crates/diecut-core/src/template/cache.rs @@ -110,8 +110,9 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { source: e, })?; - // Clone to a temp location, then move into cache - let cloned_path = clone_template(url, git_ref)?; + // Clone to a temp location, then move into cache. + // tmp_dir is kept alive so the temp directory is cleaned up on error. + let tmp_dir = clone_template(url, git_ref)?; // Write cache metadata let metadata = CacheMetadata { @@ -122,7 +123,7 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { let metadata_toml = toml::to_string_pretty(&metadata).map_err(|e| DicecutError::CacheMetadata { context: format!("serializing cache metadata: {e}"), })?; - std::fs::write(cloned_path.path().join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| { + std::fs::write(tmp_dir.path().join(CACHE_METADATA_FILE), metadata_toml).map_err(|e| { DicecutError::Io { context: "writing cache metadata".into(), source: e, @@ -137,21 +138,23 @@ pub fn get_or_clone(url: &str, git_ref: Option<&str>) -> Result { })?; } - // Move cloned directory into cache - std::fs::rename(cloned_path.path(), &cached_path).or_else(|rename_err| { + // Move cloned directory into cache. Only persist (leak) the tempdir + // after successful placement — on error, drop cleans it up. + std::fs::rename(tmp_dir.path(), &cached_path).or_else(|rename_err| { // rename can fail across filesystems; fall back to copy + delete - copy_dir_all(cloned_path.path(), &cached_path).map_err(|e| DicecutError::Io { + copy_dir_all(tmp_dir.path(), &cached_path).map_err(|e| DicecutError::Io { context: format!( "copying cloned template to cache (rename failed: {rename_err}): {e}" ), source: std::io::Error::other(e.to_string()), })?; - std::fs::remove_dir_all(cloned_path.path()).map_err(|e| DicecutError::Io { - context: "cleaning up temp clone directory".into(), - source: e, - }) + Ok(()) })?; + // Successfully placed in cache — prevent TempDir from cleaning up + // the source (it may already be gone after a successful rename). + let _ = tmp_dir.keep(); + Ok(cached_path) } From 5c70a2de99b0452f4c2a4e300eef6052f713fec4 Mon Sep 17 00:00:00 2001 From: rroskam Date: Thu, 12 Feb 2026 00:11:14 -0500 Subject: [PATCH 4/4] fix: enable HTTPS transport for gix git cloning Add blocking-http-transport-reqwest-rust-tls feature to gix dependency. Without this, only file://, git://, and ssh:// transports were available, causing HTTPS clones (gh: abbreviations, https:// URLs) to fail silently. --- Cargo.lock | 712 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 +- 2 files changed, 706 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b14cd68..d82e369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -144,6 +150,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -188,6 +200,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cc" version = "1.2.55" @@ -204,6 +222,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -322,6 +346,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -380,7 +414,7 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -594,6 +628,56 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -629,8 +713,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -640,9 +726,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -877,6 +965,7 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" dependencies = [ + "bytes", "crc32fast", "flate2", "gix-path", @@ -1319,13 +1408,16 @@ version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfe22ba26d4b65c17879f12b9882eafe65d3c8611c933b272fce2c10f546f59" dependencies = [ + "base64", "bstr", "gix-command", + "gix-credentials", "gix-features", "gix-packetline", "gix-quote", "gix-sec", "gix-url", + "reqwest", "thiserror", ] @@ -1443,6 +1535,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1484,6 +1595,45 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "humansize" version = "2.1.3" @@ -1493,6 +1643,70 @@ dependencies = [ "libm", ] +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1685,6 +1899,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -1819,6 +2049,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "maybe-async" version = "0.2.10" @@ -1875,6 +2111,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1897,6 +2139,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "newline-converter" version = "0.3.0" @@ -2073,7 +2326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -2085,6 +2338,18 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -2137,6 +2402,61 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.44" @@ -2159,8 +2479,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2170,7 +2500,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2182,6 +2522,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2246,6 +2595,49 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "rhai" version = "1.24.0" @@ -2275,12 +2667,32 @@ dependencies = [ "syn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.3" @@ -2294,12 +2706,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2367,6 +2820,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2428,7 +2893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -2454,6 +2919,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "slug" version = "0.1.6" @@ -2481,6 +2952,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.5.2" @@ -2505,6 +2986,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "3.0.2" @@ -2537,6 +3024,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2548,6 +3044,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2575,7 +3092,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -2672,6 +3189,43 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio 1.1.1", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -2713,6 +3267,76 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -2770,6 +3394,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2810,6 +3440,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2838,6 +3477,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -2870,6 +3523,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2880,6 +3543,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2952,6 +3624,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2979,6 +3662,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -3277,6 +3969,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 047a99c..864f788 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,5 +26,5 @@ console = "0.15" dirs = "6" rhai = { version = "1", features = ["sync"] } tempfile = "3" -gix = { version = "0.72", default-features = false, features = ["blocking-network-client", "worktree-mutation"] } +gix = { version = "0.72", default-features = false, features = ["blocking-network-client", "blocking-http-transport-reqwest-rust-tls", "worktree-mutation"] } sha2 = "0.10"