ZY-TAKEOVER-SNAPSHOT 20260815: 铸渊接管前现场冻结(Codex 未提交改动全部入册)· 冰朔面谕接管+可回退
This commit is contained in:
parent
ebe2ecf262
commit
ffed065841
22 changed files with 4293 additions and 567 deletions
|
|
@ -0,0 +1,929 @@
|
|||
// 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());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue