929 lines
30 KiB
Rust
929 lines
30 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use std::cmp::Reverse;
|
||
use std::fs::{self, OpenOptions};
|
||
use std::io::Write;
|
||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||
use std::path::{Component, Path, PathBuf};
|
||
use std::process::Command;
|
||
use std::time::{SystemTime, UNIX_EPOCH};
|
||
use tauri::{AppHandle, Manager};
|
||
use tauri_plugin_dialog::DialogExt;
|
||
use url::Url;
|
||
use uuid::Uuid;
|
||
|
||
const SNAPSHOT_SCHEMA: &str = "hololake.code-channel/v1";
|
||
const REGISTRY_SCHEMA: &str = "hololake.code-channel-registry/v1";
|
||
const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com"];
|
||
const MAX_TREE_ENTRIES: usize = 1_000;
|
||
const MAX_CODE_FILE_BYTES: u64 = 2 * 1024 * 1024;
|
||
|
||
#[derive(Clone, Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||
pub struct CloneCodeChannelInput {
|
||
pub url: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CodeChannelEntry {
|
||
pub channel_id: String,
|
||
pub name: String,
|
||
pub source_kind: String,
|
||
pub local_path: String,
|
||
pub remote_url: Option<String>,
|
||
pub git_head: String,
|
||
pub branch: String,
|
||
pub repository_clean: bool,
|
||
pub registered_at_unix_ms: u128,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CodeChannelSnapshot {
|
||
pub schema: &'static str,
|
||
pub state: &'static str,
|
||
pub storage_root: String,
|
||
pub channels: Vec<CodeChannelEntry>,
|
||
pub authority: &'static str,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||
pub struct BrowseCodeChannelInput {
|
||
pub channel_id: String,
|
||
pub path: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CodeTreeEntry {
|
||
pub path: String,
|
||
pub name: String,
|
||
pub kind: &'static str,
|
||
pub size_bytes: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CodeTreeSnapshot {
|
||
pub schema: &'static str,
|
||
pub channel_id: String,
|
||
pub path: String,
|
||
pub entries: Vec<CodeTreeEntry>,
|
||
pub truncated: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Deserialize)]
|
||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||
pub struct ReadCodeChannelFileInput {
|
||
pub channel_id: String,
|
||
pub path: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CodeFileProjection {
|
||
pub schema: &'static str,
|
||
pub channel_id: String,
|
||
pub path: String,
|
||
pub format: String,
|
||
pub source: String,
|
||
pub human_markdown: String,
|
||
pub size_bytes: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||
struct CodeChannelRegistry {
|
||
schema: String,
|
||
channels: Vec<CodeChannelEntry>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
struct ValidatedCloneUrl {
|
||
normalized: String,
|
||
owner: String,
|
||
repository: String,
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn get_code_channel_snapshot(app: AppHandle) -> Result<CodeChannelSnapshot, String> {
|
||
let root = code_channel_root(&app)?;
|
||
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root))
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn clone_code_channel(
|
||
app: AppHandle,
|
||
input: CloneCodeChannelInput,
|
||
) -> Result<CodeChannelSnapshot, String> {
|
||
let root = code_channel_root(&app)?;
|
||
tauri::async_runtime::spawn_blocking(move || clone_at(&root, input))
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn select_local_code_channel(
|
||
app: AppHandle,
|
||
) -> Result<Option<CodeChannelSnapshot>, String> {
|
||
let picker = app.clone();
|
||
let selected = tauri::async_runtime::spawn_blocking(move || {
|
||
picker
|
||
.dialog()
|
||
.file()
|
||
.set_title("选择本地 Git 代码频道")
|
||
.blocking_pick_folder()
|
||
})
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PICKER_JOIN_FAILED: {error}"))?;
|
||
let Some(selected) = selected else {
|
||
return Ok(None);
|
||
};
|
||
let selected = selected
|
||
.into_path()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PICKER_PATH_INVALID: {error}"))?;
|
||
let root = code_channel_root(&app)?;
|
||
tauri::async_runtime::spawn_blocking(move || register_local_at(&root, &selected).map(Some))
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn browse_code_channel(
|
||
app: AppHandle,
|
||
input: BrowseCodeChannelInput,
|
||
) -> Result<CodeTreeSnapshot, String> {
|
||
let root = code_channel_root(&app)?;
|
||
tauri::async_runtime::spawn_blocking(move || browse_at(&root, input))
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
|
||
}
|
||
|
||
#[tauri::command]
|
||
pub async fn read_code_channel_file(
|
||
app: AppHandle,
|
||
input: ReadCodeChannelFileInput,
|
||
) -> Result<CodeFileProjection, String> {
|
||
let root = code_channel_root(&app)?;
|
||
tauri::async_runtime::spawn_blocking(move || read_code_file_at(&root, input))
|
||
.await
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_JOIN_FAILED: {error}"))?
|
||
}
|
||
|
||
fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||
let root = app
|
||
.path()
|
||
.app_data_dir()
|
||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
||
.join("code-channel-v1");
|
||
ensure_root(&root)?;
|
||
Ok(root)
|
||
}
|
||
|
||
fn ensure_root(root: &Path) -> Result<(), String> {
|
||
fs::create_dir_all(root.join("repositories"))
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;
|
||
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PERMISSION_FAILED: {error}"))?;
|
||
Ok(())
|
||
}
|
||
|
||
fn snapshot_at(root: &Path) -> Result<CodeChannelSnapshot, String> {
|
||
ensure_root(root)?;
|
||
let mut registry = read_registry(root)?;
|
||
for channel in &mut registry.channels {
|
||
if let Ok(inspection) = inspect_repository(Path::new(&channel.local_path)) {
|
||
channel.git_head = inspection.git_head;
|
||
channel.branch = inspection.branch;
|
||
channel.repository_clean = inspection.repository_clean;
|
||
}
|
||
}
|
||
registry
|
||
.channels
|
||
.sort_by_key(|entry| Reverse(entry.registered_at_unix_ms));
|
||
Ok(CodeChannelSnapshot {
|
||
schema: SNAPSHOT_SCHEMA,
|
||
state: "READY",
|
||
storage_root: root.to_string_lossy().into_owned(),
|
||
channels: registry.channels,
|
||
authority: "LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY",
|
||
})
|
||
}
|
||
|
||
fn browse_at(root: &Path, input: BrowseCodeChannelInput) -> Result<CodeTreeSnapshot, String> {
|
||
let (channel, repository) = registered_repository(root, &input.channel_id)?;
|
||
let relative = input.path.unwrap_or_default();
|
||
let directory = safe_repository_path(&repository, &relative)?;
|
||
if !directory.is_dir() {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_DIRECTORY_INVALID".into());
|
||
}
|
||
let mut entries = fs::read_dir(&directory)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?;
|
||
entries.sort_by_key(|entry| {
|
||
let is_file = entry.file_type().map(|kind| kind.is_file()).unwrap_or(true);
|
||
(is_file, entry.file_name())
|
||
});
|
||
let mut projected = Vec::new();
|
||
for entry in entries {
|
||
if projected.len() >= MAX_TREE_ENTRIES {
|
||
break;
|
||
}
|
||
let name = entry.file_name().to_string_lossy().into_owned();
|
||
if name == ".git" || name == ".DS_Store" || name.starts_with("._") {
|
||
continue;
|
||
}
|
||
let kind = entry
|
||
.file_type()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?;
|
||
if kind.is_symlink() || (!kind.is_dir() && !kind.is_file()) {
|
||
continue;
|
||
}
|
||
let entry_path = entry.path();
|
||
let relative_path = relative_posix(&repository, &entry_path)?;
|
||
let size_bytes = if kind.is_file() {
|
||
entry
|
||
.metadata()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_TREE_UNAVAILABLE: {error}"))?
|
||
.len()
|
||
} else {
|
||
0
|
||
};
|
||
projected.push(CodeTreeEntry {
|
||
path: relative_path,
|
||
name,
|
||
kind: if kind.is_dir() { "directory" } else { "file" },
|
||
size_bytes,
|
||
});
|
||
}
|
||
Ok(CodeTreeSnapshot {
|
||
schema: "hololake.code-channel-tree/v1",
|
||
channel_id: channel.channel_id,
|
||
path: relative,
|
||
truncated: projected.len() >= MAX_TREE_ENTRIES,
|
||
entries: projected,
|
||
})
|
||
}
|
||
|
||
fn read_code_file_at(
|
||
root: &Path,
|
||
input: ReadCodeChannelFileInput,
|
||
) -> Result<CodeFileProjection, String> {
|
||
let (channel, repository) = registered_repository(root, &input.channel_id)?;
|
||
let file = safe_repository_path(&repository, &input.path)?;
|
||
let metadata = fs::metadata(&file)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_FILE_UNAVAILABLE: {error}"))?;
|
||
if !metadata.is_file() || metadata.len() > MAX_CODE_FILE_BYTES || !is_supported_text_file(&file)
|
||
{
|
||
return Err("HOLOLAKE_CODE_CHANNEL_FILE_UNSUPPORTED".into());
|
||
}
|
||
let source = fs::read_to_string(&file)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_FILE_INVALID_UTF8: {error}"))?;
|
||
let format = file_format(&file);
|
||
let human_markdown = human_projection(&input.path, &format, &source);
|
||
Ok(CodeFileProjection {
|
||
schema: "hololake.code-channel-file-projection/v1",
|
||
channel_id: channel.channel_id,
|
||
path: input.path,
|
||
format,
|
||
source,
|
||
human_markdown,
|
||
size_bytes: metadata.len(),
|
||
})
|
||
}
|
||
|
||
fn registered_repository(
|
||
root: &Path,
|
||
channel_id: &str,
|
||
) -> Result<(CodeChannelEntry, PathBuf), String> {
|
||
let registry = read_registry(root)?;
|
||
let channel = registry
|
||
.channels
|
||
.into_iter()
|
||
.find(|entry| entry.channel_id == channel_id)
|
||
.ok_or("HOLOLAKE_CODE_CHANNEL_NOT_REGISTERED")?;
|
||
let repository = PathBuf::from(&channel.local_path)
|
||
.canonicalize()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REPOSITORY_UNAVAILABLE: {error}"))?;
|
||
if !repository.is_dir() {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_REPOSITORY_UNAVAILABLE".into());
|
||
}
|
||
Ok((channel, repository))
|
||
}
|
||
|
||
fn safe_repository_path(root: &Path, relative: &str) -> Result<PathBuf, String> {
|
||
let relative_path = Path::new(relative);
|
||
if relative_path.is_absolute()
|
||
|| relative_path
|
||
.components()
|
||
.any(|component| !matches!(component, Component::Normal(_)))
|
||
{
|
||
if relative.is_empty() {
|
||
return Ok(root.to_path_buf());
|
||
}
|
||
return Err("HOLOLAKE_CODE_CHANNEL_PATH_INVALID".into());
|
||
}
|
||
if relative.is_empty() {
|
||
return Ok(root.to_path_buf());
|
||
}
|
||
let candidate = root.join(relative_path);
|
||
let metadata = fs::symlink_metadata(&candidate)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PATH_UNAVAILABLE: {error}"))?;
|
||
if metadata.file_type().is_symlink() {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_SYMLINK_DENIED".into());
|
||
}
|
||
let canonical = candidate
|
||
.canonicalize()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PATH_UNAVAILABLE: {error}"))?;
|
||
if !canonical.starts_with(root) {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_PATH_ESCAPE".into());
|
||
}
|
||
Ok(canonical)
|
||
}
|
||
|
||
fn relative_posix(root: &Path, path: &Path) -> Result<String, String> {
|
||
Ok(path
|
||
.strip_prefix(root)
|
||
.map_err(|_| "HOLOLAKE_CODE_CHANNEL_PATH_INVALID".to_string())?
|
||
.to_string_lossy()
|
||
.replace('\\', "/"))
|
||
}
|
||
|
||
fn is_supported_text_file(path: &Path) -> bool {
|
||
let name = path
|
||
.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.unwrap_or("");
|
||
if ["README", "LICENSE", "Dockerfile", "Makefile", "Procfile"].contains(&name) {
|
||
return true;
|
||
}
|
||
matches!(
|
||
path.extension()
|
||
.and_then(|value| value.to_str())
|
||
.unwrap_or("")
|
||
.to_ascii_lowercase()
|
||
.as_str(),
|
||
"md" | "markdown"
|
||
| "txt"
|
||
| "json"
|
||
| "jsonl"
|
||
| "yaml"
|
||
| "yml"
|
||
| "toml"
|
||
| "hdlp"
|
||
| "rs"
|
||
| "ts"
|
||
| "tsx"
|
||
| "js"
|
||
| "jsx"
|
||
| "mjs"
|
||
| "cjs"
|
||
| "py"
|
||
| "sh"
|
||
| "zsh"
|
||
| "bash"
|
||
| "css"
|
||
| "scss"
|
||
| "html"
|
||
| "xml"
|
||
| "sql"
|
||
| "graphql"
|
||
| "go"
|
||
| "java"
|
||
| "kt"
|
||
| "swift"
|
||
| "c"
|
||
| "h"
|
||
| "cpp"
|
||
| "hpp"
|
||
| "proto"
|
||
| "ini"
|
||
| "conf"
|
||
| "env"
|
||
)
|
||
}
|
||
|
||
fn file_format(path: &Path) -> String {
|
||
path.extension()
|
||
.and_then(|value| value.to_str())
|
||
.filter(|value| !value.is_empty())
|
||
.unwrap_or("text")
|
||
.to_ascii_lowercase()
|
||
}
|
||
|
||
fn human_projection(path: &str, format: &str, source: &str) -> String {
|
||
if matches!(format, "md" | "markdown") {
|
||
return source.to_string();
|
||
}
|
||
let title = Path::new(path)
|
||
.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.unwrap_or("代码文件");
|
||
if format == "json" {
|
||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(source) {
|
||
return format!(
|
||
"# {title}\n\n> 来自代码频道的 JSON 结构,已转换为可读层级。\n\n{}",
|
||
json_as_markdown(&value, 2)
|
||
);
|
||
}
|
||
}
|
||
if matches!(format, "hdlp" | "yaml" | "yml" | "toml" | "ini" | "conf") {
|
||
let rows = source
|
||
.lines()
|
||
.filter_map(|line| {
|
||
let trimmed = line.trim();
|
||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||
return None;
|
||
}
|
||
trimmed
|
||
.split_once(':')
|
||
.or_else(|| trimmed.split_once('='))
|
||
.map(|(key, value)| format!("| {} | {} |", key.trim(), value.trim()))
|
||
})
|
||
.take(120)
|
||
.collect::<Vec<_>>();
|
||
if !rows.is_empty() {
|
||
return format!(
|
||
"# {title}\n\n> 来自代码频道的结构化文件。原始机器字段已转成阅读表格。\n\n| 字段 | 内容 |\n| --- | --- |\n{}",
|
||
rows.join("\n")
|
||
);
|
||
}
|
||
}
|
||
format!(
|
||
"# {title}\n\n> 来自代码频道的 `{format}` 文件。下方保留原始内容,便于核验。\n\n```{format}\n{source}\n```"
|
||
)
|
||
}
|
||
|
||
fn json_as_markdown(value: &serde_json::Value, level: usize) -> String {
|
||
match value {
|
||
serde_json::Value::Object(map) => map
|
||
.iter()
|
||
.map(|(key, value)| match value {
|
||
serde_json::Value::Object(_) | serde_json::Value::Array(_) => format!(
|
||
"{} {key}\n\n{}",
|
||
"#".repeat(level.min(5)),
|
||
json_as_markdown(value, level + 1)
|
||
),
|
||
_ => format!("- **{key}**:{}", scalar_json(value)),
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join("\n\n"),
|
||
serde_json::Value::Array(values) => values
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(index, value)| match value {
|
||
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
|
||
format!("{}. {}", index + 1, json_as_markdown(value, level + 1))
|
||
}
|
||
_ => format!("- {}", scalar_json(value)),
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join("\n"),
|
||
_ => scalar_json(value),
|
||
}
|
||
}
|
||
|
||
fn scalar_json(value: &serde_json::Value) -> String {
|
||
value
|
||
.as_str()
|
||
.map(ToOwned::to_owned)
|
||
.unwrap_or_else(|| value.to_string())
|
||
}
|
||
|
||
fn clone_at(root: &Path, input: CloneCodeChannelInput) -> Result<CodeChannelSnapshot, String> {
|
||
ensure_root(root)?;
|
||
let validated = validate_clone_url(&input.url)?;
|
||
let destination = root.join("repositories").join(format!(
|
||
"{}--{}",
|
||
safe_segment(&validated.owner),
|
||
safe_segment(&validated.repository)
|
||
));
|
||
if destination.exists() {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_DESTINATION_EXISTS".into());
|
||
}
|
||
let output = Command::new("/usr/bin/git")
|
||
.env("GIT_TERMINAL_PROMPT", "0")
|
||
.env("GIT_ASKPASS", "/usr/bin/false")
|
||
.env("SSH_ASKPASS", "/usr/bin/false")
|
||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||
.args([
|
||
"-c",
|
||
"credential.helper=",
|
||
"-c",
|
||
"core.askPass=",
|
||
"clone",
|
||
"--origin",
|
||
"origin",
|
||
"--no-tags",
|
||
])
|
||
.arg(&validated.normalized)
|
||
.arg(&destination)
|
||
.output()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_CLONE_FAILED: {error}"))?;
|
||
if !output.status.success() {
|
||
if destination.starts_with(root.join("repositories")) {
|
||
let _ = fs::remove_dir_all(&destination);
|
||
}
|
||
return Err(format!(
|
||
"HOLOLAKE_CODE_CHANNEL_CLONE_FAILED: {}",
|
||
String::from_utf8_lossy(&output.stderr).trim()
|
||
));
|
||
}
|
||
let entry = entry_for_repository(
|
||
&destination,
|
||
"CLONED_HTTPS",
|
||
Some(validated.normalized),
|
||
Some(format!("{}/{}", validated.owner, validated.repository)),
|
||
)?;
|
||
upsert_entry(root, entry)?;
|
||
snapshot_at(root)
|
||
}
|
||
|
||
fn register_local_at(root: &Path, selected: &Path) -> Result<CodeChannelSnapshot, String> {
|
||
ensure_root(root)?;
|
||
let canonical = selected
|
||
.canonicalize()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_LOCAL_UNAVAILABLE: {error}"))?;
|
||
if !canonical.is_dir() {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_LOCAL_INVALID".into());
|
||
}
|
||
let remote = git_optional(&canonical, &["remote", "get-url", "origin"])
|
||
.and_then(|value| redact_remote_url(value.trim()));
|
||
let entry = entry_for_repository(&canonical, "LOCAL_GIT", remote, None)?;
|
||
upsert_entry(root, entry)?;
|
||
snapshot_at(root)
|
||
}
|
||
|
||
struct RepositoryInspection {
|
||
git_head: String,
|
||
branch: String,
|
||
repository_clean: bool,
|
||
}
|
||
|
||
fn inspect_repository(path: &Path) -> Result<RepositoryInspection, String> {
|
||
let inside = git(path, &["rev-parse", "--is-inside-work-tree"], "INSPECT")?;
|
||
if inside.trim() != "true" {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_NOT_GIT".into());
|
||
}
|
||
let git_head = git(path, &["rev-parse", "HEAD"], "READ_HEAD")?
|
||
.trim()
|
||
.to_string();
|
||
let branch = git_optional(path, &["symbolic-ref", "--short", "HEAD"])
|
||
.map(|value| value.trim().to_string())
|
||
.filter(|value| !value.is_empty())
|
||
.unwrap_or_else(|| "DETACHED".into());
|
||
let repository_clean = git(path, &["status", "--porcelain"], "READ_STATUS")?
|
||
.trim()
|
||
.is_empty();
|
||
Ok(RepositoryInspection {
|
||
git_head,
|
||
branch,
|
||
repository_clean,
|
||
})
|
||
}
|
||
|
||
fn entry_for_repository(
|
||
path: &Path,
|
||
source_kind: &str,
|
||
remote_url: Option<String>,
|
||
explicit_name: Option<String>,
|
||
) -> Result<CodeChannelEntry, String> {
|
||
let inspection = inspect_repository(path)?;
|
||
let name = explicit_name.unwrap_or_else(|| {
|
||
path.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.unwrap_or("代码频道")
|
||
.to_string()
|
||
});
|
||
Ok(CodeChannelEntry {
|
||
channel_id: format!("channel-{}", Uuid::new_v4()),
|
||
name,
|
||
source_kind: source_kind.into(),
|
||
local_path: path.to_string_lossy().into_owned(),
|
||
remote_url,
|
||
git_head: inspection.git_head,
|
||
branch: inspection.branch,
|
||
repository_clean: inspection.repository_clean,
|
||
registered_at_unix_ms: now_unix_ms()?,
|
||
})
|
||
}
|
||
|
||
fn validate_clone_url(raw: &str) -> Result<ValidatedCloneUrl, String> {
|
||
let raw = raw.trim();
|
||
if raw.len() > 2_048 {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
|
||
}
|
||
let mut parsed = Url::parse(raw).map_err(|_| "HOLOLAKE_CODE_CHANNEL_URL_INVALID")?;
|
||
if parsed.scheme() != "https"
|
||
|| !parsed.username().is_empty()
|
||
|| parsed.password().is_some()
|
||
|| parsed.query().is_some()
|
||
|| parsed.fragment().is_some()
|
||
|| parsed.port().is_some()
|
||
{
|
||
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
|
||
}
|
||
let host = parsed
|
||
.host_str()
|
||
.ok_or("HOLOLAKE_CODE_CHANNEL_URL_INVALID")?
|
||
.to_lowercase();
|
||
if !ALLOWED_HOSTS.contains(&host.as_str()) {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_HOST_NOT_TRUSTED".into());
|
||
}
|
||
let segments = parsed
|
||
.path_segments()
|
||
.ok_or("HOLOLAKE_CODE_CHANNEL_URL_INVALID")?
|
||
.filter(|segment| !segment.is_empty())
|
||
.collect::<Vec<_>>();
|
||
if segments.len() != 3 || segments[0] != "code" {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
|
||
}
|
||
let owner = segments[1].to_string();
|
||
let repository = segments[2]
|
||
.strip_suffix(".git")
|
||
.unwrap_or(segments[2])
|
||
.to_string();
|
||
if !valid_git_segment(&owner) || !valid_git_segment(&repository) {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_URL_INVALID".into());
|
||
}
|
||
parsed.set_path(&format!("/code/{owner}/{repository}.git"));
|
||
Ok(ValidatedCloneUrl {
|
||
normalized: parsed.to_string(),
|
||
owner,
|
||
repository,
|
||
})
|
||
}
|
||
|
||
fn valid_git_segment(value: &str) -> bool {
|
||
!value.is_empty()
|
||
&& value.len() <= 100
|
||
&& value != "."
|
||
&& value != ".."
|
||
&& value
|
||
.chars()
|
||
.all(|character| character.is_ascii_alphanumeric() || "-_.".contains(character))
|
||
}
|
||
|
||
fn redact_remote_url(raw: &str) -> Option<String> {
|
||
let mut parsed = Url::parse(raw).ok()?;
|
||
if parsed.scheme() != "https" {
|
||
return None;
|
||
}
|
||
parsed.set_username("").ok()?;
|
||
parsed.set_password(None).ok()?;
|
||
parsed.set_query(None);
|
||
parsed.set_fragment(None);
|
||
Some(parsed.to_string())
|
||
}
|
||
|
||
fn safe_segment(value: &str) -> String {
|
||
value
|
||
.chars()
|
||
.map(|character| {
|
||
if character.is_ascii_alphanumeric() || "-_.".contains(character) {
|
||
character
|
||
} else {
|
||
'-'
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn upsert_entry(root: &Path, entry: CodeChannelEntry) -> Result<(), String> {
|
||
let mut registry = read_registry(root)?;
|
||
registry
|
||
.channels
|
||
.retain(|existing| existing.local_path != entry.local_path);
|
||
registry.channels.push(entry);
|
||
write_registry(root, ®istry)
|
||
}
|
||
|
||
fn read_registry(root: &Path) -> Result<CodeChannelRegistry, String> {
|
||
let path = root.join("channels.json");
|
||
if !path.exists() {
|
||
return Ok(CodeChannelRegistry {
|
||
schema: REGISTRY_SCHEMA.into(),
|
||
channels: Vec::new(),
|
||
});
|
||
}
|
||
let bytes = fs::read(&path)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_UNAVAILABLE: {error}"))?;
|
||
let registry: CodeChannelRegistry = serde_json::from_slice(&bytes)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_INVALID: {error}"))?;
|
||
if registry.schema != REGISTRY_SCHEMA {
|
||
return Err("HOLOLAKE_CODE_CHANNEL_REGISTRY_SCHEMA_INVALID".into());
|
||
}
|
||
Ok(registry)
|
||
}
|
||
|
||
fn write_registry(root: &Path, registry: &CodeChannelRegistry) -> Result<(), String> {
|
||
let bytes = serde_json::to_vec_pretty(registry)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_INVALID: {error}"))?;
|
||
let temporary = root.join(format!("channels.json.tmp-{}", Uuid::new_v4()));
|
||
let final_path = root.join("channels.json");
|
||
let mut file = OpenOptions::new()
|
||
.create_new(true)
|
||
.write(true)
|
||
.mode(0o600)
|
||
.open(&temporary)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
|
||
file.write_all(&bytes)
|
||
.and_then(|_| file.sync_all())
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
|
||
fs::rename(&temporary, &final_path)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_REGISTRY_WRITE_FAILED: {error}"))?;
|
||
Ok(())
|
||
}
|
||
|
||
fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
|
||
let output = Command::new("/usr/bin/git")
|
||
.current_dir(root)
|
||
.env("GIT_TERMINAL_PROMPT", "0")
|
||
.args(args)
|
||
.output()
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_GIT_{operation}_FAILED: {error}"))?;
|
||
if !output.status.success() {
|
||
return Err(format!(
|
||
"HOLOLAKE_CODE_CHANNEL_GIT_{operation}_FAILED: {}",
|
||
String::from_utf8_lossy(&output.stderr).trim()
|
||
));
|
||
}
|
||
String::from_utf8(output.stdout)
|
||
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_GIT_{operation}_INVALID_UTF8: {error}"))
|
||
}
|
||
|
||
fn git_optional(root: &Path, args: &[&str]) -> Option<String> {
|
||
let output = Command::new("/usr/bin/git")
|
||
.current_dir(root)
|
||
.env("GIT_TERMINAL_PROMPT", "0")
|
||
.args(args)
|
||
.output()
|
||
.ok()?;
|
||
output
|
||
.status
|
||
.success()
|
||
.then(|| String::from_utf8_lossy(&output.stdout).into_owned())
|
||
}
|
||
|
||
fn now_unix_ms() -> Result<u128, String> {
|
||
SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map(|duration| duration.as_millis())
|
||
.map_err(|error| format!("HOLOLAKE_CLOCK_INVALID: {error}"))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use tempfile::tempdir;
|
||
|
||
#[test]
|
||
fn clone_url_is_normalized_to_official_https_git_endpoint() {
|
||
let parsed =
|
||
validate_clone_url("https://guanghulab.com/code/bingshuo/guanghu-ice-heart").unwrap();
|
||
assert_eq!(
|
||
parsed.normalized,
|
||
"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"
|
||
);
|
||
assert_eq!(parsed.owner, "bingshuo");
|
||
assert_eq!(parsed.repository, "guanghu-ice-heart");
|
||
}
|
||
|
||
#[test]
|
||
fn clone_url_rejects_credentials_queries_and_unknown_hosts() {
|
||
assert!(
|
||
validate_clone_url("https://user:secret@guanghulab.com/code/bingshuo/repo").is_err()
|
||
);
|
||
assert!(
|
||
validate_clone_url("https://guanghulab.com/code/bingshuo/repo?token=secret").is_err()
|
||
);
|
||
assert!(validate_clone_url("https://example.com/code/bingshuo/repo").is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn remote_redaction_removes_credentials_and_fragments() {
|
||
assert_eq!(
|
||
redact_remote_url("https://user:secret@example.com/a/b.git?x=1#readme"),
|
||
Some("https://example.com/a/b.git".into())
|
||
);
|
||
assert_eq!(redact_remote_url("git@example.com:a/b.git"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn local_repository_registration_is_persistent_and_deduplicated() {
|
||
let root = tempdir().unwrap();
|
||
let repository = tempdir().unwrap();
|
||
ensure_root(root.path()).unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["init", "--initial-branch=main"],
|
||
"TEST_INIT",
|
||
)
|
||
.unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["config", "user.name", "Test"],
|
||
"TEST_CONFIG",
|
||
)
|
||
.unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["config", "user.email", "test@example.invalid"],
|
||
"TEST_CONFIG",
|
||
)
|
||
.unwrap();
|
||
fs::write(repository.path().join("README.md"), "# test\n").unwrap();
|
||
git(repository.path(), &["add", "README.md"], "TEST_ADD").unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["commit", "-m", "initial"],
|
||
"TEST_COMMIT",
|
||
)
|
||
.unwrap();
|
||
|
||
register_local_at(root.path(), repository.path()).unwrap();
|
||
register_local_at(root.path(), repository.path()).unwrap();
|
||
let snapshot = snapshot_at(root.path()).unwrap();
|
||
assert_eq!(snapshot.channels.len(), 1);
|
||
assert_eq!(snapshot.channels[0].branch, "main");
|
||
assert!(snapshot.channels[0].repository_clean);
|
||
}
|
||
|
||
#[test]
|
||
fn registered_repository_can_be_browsed_and_projected_as_knowledge() {
|
||
let root = tempdir().unwrap();
|
||
let repository = tempdir().unwrap();
|
||
ensure_root(root.path()).unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["init", "--initial-branch=main"],
|
||
"TEST_INIT",
|
||
)
|
||
.unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["config", "user.name", "Test"],
|
||
"TEST_CONFIG",
|
||
)
|
||
.unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["config", "user.email", "test@example.invalid"],
|
||
"TEST_CONFIG",
|
||
)
|
||
.unwrap();
|
||
fs::create_dir(repository.path().join("config")).unwrap();
|
||
fs::write(
|
||
repository.path().join("config/system.json"),
|
||
r#"{"name":"光湖","state":"ready","routes":["native","mcp"]}"#,
|
||
)
|
||
.unwrap();
|
||
git(repository.path(), &["add", "."], "TEST_ADD").unwrap();
|
||
git(
|
||
repository.path(),
|
||
&["commit", "-m", "initial"],
|
||
"TEST_COMMIT",
|
||
)
|
||
.unwrap();
|
||
|
||
let snapshot = register_local_at(root.path(), repository.path()).unwrap();
|
||
let channel_id = snapshot.channels[0].channel_id.clone();
|
||
let tree = browse_at(
|
||
root.path(),
|
||
BrowseCodeChannelInput {
|
||
channel_id: channel_id.clone(),
|
||
path: None,
|
||
},
|
||
)
|
||
.unwrap();
|
||
assert!(tree
|
||
.entries
|
||
.iter()
|
||
.any(|entry| entry.name == "config" && entry.kind == "directory"));
|
||
|
||
let projection = read_code_file_at(
|
||
root.path(),
|
||
ReadCodeChannelFileInput {
|
||
channel_id,
|
||
path: "config/system.json".into(),
|
||
},
|
||
)
|
||
.unwrap();
|
||
assert_eq!(projection.format, "json");
|
||
assert!(projection
|
||
.human_markdown
|
||
.contains("来自代码频道的 JSON 结构"));
|
||
assert!(projection.human_markdown.contains("**state**:ready"));
|
||
}
|
||
|
||
#[test]
|
||
fn code_tree_denies_parent_escape() {
|
||
let repository = tempdir().unwrap();
|
||
assert!(safe_repository_path(repository.path(), "../secret").is_err());
|
||
}
|
||
}
|