refactor: rebuild desktop app with Tauri

This commit is contained in:
leookun
2026-08-24 02:49:00 +08:00
parent a3ec2a0dfc
commit 4053a7fb20
808 changed files with 87359 additions and 181491 deletions
+173
View File
@@ -0,0 +1,173 @@
//! Deterministic local file discovery with gitignore and Semble exclusions.
use std::{
fs,
path::{Path, PathBuf},
sync::mpsc,
time::UNIX_EPOCH,
};
use ignore::{WalkBuilder, WalkState};
use serde::{Deserialize, Serialize};
use crate::{language::content_type_for_path, ContentType, Error, Result};
const IGNORED_DIRS: &[&str] = &[
".git",
".hg",
".svn",
"node_modules",
"target",
".venv",
"venv",
".tox",
"__pycache__",
".next",
"dist",
"build",
".cache",
".semble",
];
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FileStamp {
pub modified_ns: u128,
pub size: u64,
}
#[derive(Clone, Debug)]
pub struct SourceFile {
pub absolute_path: PathBuf,
pub relative_path: String,
pub stamp: FileStamp,
pub content_type: ContentType,
}
pub fn discover_files(
root: &Path,
selected: &[ContentType],
max_bytes: u64,
) -> Result<Vec<SourceFile>> {
let mut builder = WalkBuilder::new(root);
builder
.hidden(false)
.follow_links(false)
.git_ignore(true)
.git_exclude(true)
.parents(true)
.add_custom_ignore_filename(".sembleignore")
.filter_entry(|entry| {
entry
.file_name()
.to_str()
.map(|name| {
!entry.file_type().is_some_and(|kind| kind.is_dir())
|| !IGNORED_DIRS.contains(&name)
})
.unwrap_or(true)
});
let (sender, receiver) = mpsc::channel();
builder.build_parallel().run(|| {
let sender = sender.clone();
Box::new(move |entry| {
let result = entry
.map_err(|error| Error::InvalidRequest(error.to_string()))
.and_then(|entry| source_file(root, selected, max_bytes, entry));
let _ = sender.send(result);
WalkState::Continue
})
});
drop(sender);
let mut files = Vec::new();
for result in receiver {
if let Some(file) = result? {
files.push(file);
}
}
files.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
Ok(files)
}
fn source_file(
root: &Path,
selected: &[ContentType],
max_bytes: u64,
entry: ignore::DirEntry,
) -> Result<Option<SourceFile>> {
if !entry.file_type().is_some_and(|kind| kind.is_file()) {
return Ok(None);
}
let path = entry.path();
let Some(content_type) = content_type_for_path(path) else {
return Ok(None);
};
if !selected.contains(&content_type) {
return Ok(None);
}
let metadata = fs::metadata(path).map_err(|error| Error::io(path, error))?;
if metadata.len() == 0 || metadata.len() > max_bytes {
return Ok(None);
}
let relative_path = path
.strip_prefix(root)
.map_err(|_| Error::UnsafePath(path.to_path_buf()))?;
let modified_ns = metadata
.modified()
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map_or(0, |duration| duration.as_nanos());
Ok(Some(SourceFile {
absolute_path: path.to_path_buf(),
relative_path: relative_path.to_string_lossy().replace('\\', "/"),
stamp: FileStamp {
modified_ns,
size: metadata.len(),
},
content_type,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discovery_obeys_content_scope_and_ignore_files() {
let directory = tempfile::tempdir().unwrap();
fs::create_dir_all(directory.path().join("src")).unwrap();
fs::create_dir_all(directory.path().join("target")).unwrap();
fs::write(directory.path().join("src/lib.rs"), "pub fn visible() {}\n").unwrap();
fs::write(
directory.path().join("src/ignored.rs"),
"pub fn ignored() {}\n",
)
.unwrap();
fs::write(directory.path().join("README.md"), "# docs\n").unwrap();
fs::write(
directory.path().join("target/generated.rs"),
"fn generated() {}\n",
)
.unwrap();
fs::write(directory.path().join(".sembleignore"), "src/ignored.rs\n").unwrap();
let code = discover_files(directory.path(), &[ContentType::Code], 1024).unwrap();
assert_eq!(
code.iter()
.map(|file| file.relative_path.as_str())
.collect::<Vec<_>>(),
vec!["src/lib.rs"]
);
let docs = discover_files(directory.path(), &[ContentType::Docs], 1024).unwrap();
assert_eq!(docs[0].relative_path, "README.md");
}
#[test]
fn discovery_skips_empty_and_oversized_files() {
let directory = tempfile::tempdir().unwrap();
fs::write(directory.path().join("empty.rs"), "").unwrap();
fs::write(directory.path().join("large.rs"), "0123456789").unwrap();
assert!(discover_files(directory.path(), &[ContentType::Code], 5)
.unwrap()
.is_empty());
}
}
+9
View File
@@ -0,0 +1,9 @@
//! Source discovery, ignore handling, path safety, and remote Git acquisition.
mod local;
mod remote_git;
mod security;
pub use local::{discover_files, FileStamp, SourceFile};
pub use remote_git::RemoteRepository;
pub use security::canonical_source_root;
@@ -0,0 +1,87 @@
//! Restricted HTTPS Git acquisition for explicitly requested remote repositories.
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
};
use sha2::{Digest, Sha256};
use url::Url;
use crate::{Error, Result};
pub struct RemoteRepository {
pub path: PathBuf,
pub identity: String,
}
impl RemoteRepository {
pub fn acquire(url: &str, cache_root: &Path) -> Result<Self> {
let parsed = Url::parse(url).map_err(|_| Error::UnsupportedUrl(url.to_owned()))?;
if !matches!(parsed.scheme(), "https" | "http") || parsed.host_str().is_none() {
return Err(Error::UnsupportedUrl(url.to_owned()));
}
let key = hex::encode(Sha256::digest(url.as_bytes()));
let repos = cache_root.join("repos");
std::fs::create_dir_all(&repos).map_err(|error| Error::io(&repos, error))?;
let path = repos.join(key);
if !path.join(".git").is_dir() {
let temporary =
repos.join(format!(".clone-{}-{}", std::process::id(), random_suffix()));
let result = Command::new("git")
.args(["clone", "--depth", "1", "--", url])
.arg(&temporary)
.stdin(Stdio::null())
.output()
.map_err(|error| Error::Git(error.to_string()))?;
if !result.status.success() {
let _ = std::fs::remove_dir_all(&temporary);
return Err(Error::Git(
String::from_utf8_lossy(&result.stderr).trim().to_owned(),
));
}
std::fs::rename(&temporary, &path).map_err(|error| Error::io(&path, error))?;
}
let output = Command::new("git")
.args(["-C"])
.arg(&path)
.args(["rev-parse", "HEAD"])
.stdin(Stdio::null())
.output()
.map_err(|error| Error::Git(error.to_string()))?;
if !output.status.success() {
return Err(Error::Git(
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
));
}
let revision = String::from_utf8_lossy(&output.stdout).trim().to_owned();
Ok(Self {
path,
identity: format!("{url}@{revision}"),
})
}
}
fn random_suffix() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |value| value.as_nanos())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remote_sources_reject_non_http_urls_before_git_runs() {
let directory = tempfile::tempdir().unwrap();
assert!(matches!(
RemoteRepository::acquire("file:///tmp/repository", directory.path()),
Err(Error::UnsupportedUrl(_))
));
assert!(matches!(
RemoteRepository::acquire("git@github.com:owner/repo.git", directory.path()),
Err(Error::UnsupportedUrl(_))
));
}
}
+15
View File
@@ -0,0 +1,15 @@
//! Canonical path validation that keeps indexing inside an allowed source root.
use std::path::{Path, PathBuf};
use crate::{Error, Result};
pub fn canonical_source_root(path: &Path) -> Result<PathBuf> {
if !path.exists() {
return Err(Error::SourceMissing(path.to_path_buf()));
}
if !path.is_dir() {
return Err(Error::SourceNotDirectory(path.to_path_buf()));
}
path.canonicalize().map_err(|error| Error::io(path, error))
}