gitserver_core/
dynamic_registry.rs1use std::path::{Path, PathBuf};
8use std::sync::{Arc, RwLock};
9
10use crate::discovery::{RepoInfo, RepoStore};
11use crate::error::{Error, Result};
12
13pub trait RepoResolver: Send + Sync {
14 fn resolve(&self, relative: &str) -> Result<RepoInfo>;
15 fn list(&self) -> Result<Vec<RepoInfo>>;
16}
17
18pub trait MutableRepoRegistry: RepoResolver {
19 fn register(&self, repo: RepoInfo) -> Result<()>;
20 fn unregister(&self, relative: &str) -> Result<()>;
21}
22
23#[derive(Clone, Default)]
24pub struct DynamicRepoRegistry {
25 repos: Arc<RwLock<Vec<RepoInfo>>>,
26}
27
28impl DynamicRepoRegistry {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn from_repos(repos: Vec<RepoInfo>) -> Result<Self> {
34 let registry = Self::new();
35 for repo in repos {
36 registry.register(repo)?;
37 }
38 Ok(registry)
39 }
40}
41
42impl RepoResolver for RepoStore {
43 fn resolve(&self, relative: &str) -> Result<RepoInfo> {
44 RepoStore::resolve(self, relative).cloned()
45 }
46
47 fn list(&self) -> Result<Vec<RepoInfo>> {
48 Ok(RepoStore::list(self).to_vec())
49 }
50}
51
52impl RepoResolver for DynamicRepoRegistry {
53 fn resolve(&self, relative: &str) -> Result<RepoInfo> {
54 let normalized = normalize_relative_repo_path(relative)?;
55 self.repos
56 .read()
57 .expect("dynamic repo registry poisoned")
58 .iter()
59 .find(|repo| repo.relative_path == normalized)
60 .cloned()
61 .ok_or_else(|| Error::RepoNotFound(relative.to_string()))
62 }
63
64 fn list(&self) -> Result<Vec<RepoInfo>> {
65 Ok(self
66 .repos
67 .read()
68 .expect("dynamic repo registry poisoned")
69 .clone())
70 }
71}
72
73impl MutableRepoRegistry for DynamicRepoRegistry {
74 fn register(&self, repo: RepoInfo) -> Result<()> {
75 let relative_path = normalize_relative_repo_path(&repo.relative_path)?;
76 let repo_path = repo.absolute_path.canonicalize()?;
77 let opened = gix::open(&repo_path)?;
78 if !opened.is_bare() {
79 return Err(Error::Protocol(format!(
80 "registered path is not a bare repository: {}",
81 repo_path.display()
82 )));
83 }
84
85 let mut repos = self.repos.write().expect("dynamic repo registry poisoned");
86 if repos
87 .iter()
88 .any(|existing| existing.relative_path == relative_path)
89 {
90 return Err(Error::Protocol(format!(
91 "repository already registered: {}",
92 relative_path
93 )));
94 }
95
96 let mut repo = repo;
97 repo.relative_path = relative_path;
98 repo.absolute_path = repo_path;
99 repos.push(repo);
100 repos.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
101 Ok(())
102 }
103
104 fn unregister(&self, relative: &str) -> Result<()> {
105 let normalized = normalize_relative_repo_path(relative)?;
106 let mut repos = self.repos.write().expect("dynamic repo registry poisoned");
107 let original_len = repos.len();
108 repos.retain(|repo| repo.relative_path != normalized);
109 if repos.len() == original_len {
110 return Err(Error::RepoNotFound(relative.to_string()));
111 }
112 Ok(())
113 }
114}
115
116fn normalize_relative_repo_path(relative: &str) -> Result<String> {
117 let path = Path::new(relative);
118 if path.is_absolute() {
119 return Err(Error::PathTraversal(relative.to_string().into()));
120 }
121
122 let mut normalized = PathBuf::new();
123 for component in path.components() {
124 match component {
125 std::path::Component::Normal(part) => normalized.push(part),
126 std::path::Component::CurDir => {}
127 _ => return Err(Error::PathTraversal(relative.to_string().into())),
128 }
129 }
130
131 if normalized.as_os_str().is_empty() {
132 return Err(Error::RepoNotFound(relative.to_string()));
133 }
134
135 Ok(normalized.to_string_lossy().into_owned())
136}
137
138#[cfg(test)]
139mod tests {
140 use std::path::Path;
141 use std::process::Command;
142
143 use tempfile::TempDir;
144
145 use super::*;
146
147 fn create_bare_repo(path: &Path) {
148 Command::new("git")
149 .args(["init", "--bare", path.to_str().unwrap()])
150 .output()
151 .expect("git init --bare failed");
152 }
153
154 #[test]
155 fn dynamic_registry_registers_and_unregisters() {
156 let dir = TempDir::new().unwrap();
157 let repo_path = dir.path().join("alpha.git");
158 create_bare_repo(&repo_path);
159
160 let registry = DynamicRepoRegistry::new();
161 registry
162 .register(RepoInfo {
163 name: "alpha.git".into(),
164 relative_path: "alpha.git".into(),
165 absolute_path: repo_path.clone(),
166 description: None,
167 })
168 .unwrap();
169
170 assert_eq!(registry.list().unwrap().len(), 1);
171 assert_eq!(
172 registry.resolve("alpha.git").unwrap().absolute_path,
173 repo_path.canonicalize().unwrap()
174 );
175
176 registry.unregister("alpha.git").unwrap();
177 assert!(matches!(
178 registry.resolve("alpha.git"),
179 Err(Error::RepoNotFound(_))
180 ));
181 }
182
183 #[test]
184 fn dynamic_registry_resolve_and_unregister_normalize_paths() {
185 let dir = TempDir::new().unwrap();
186 let repo_path = dir.path().join("alpha.git");
187 create_bare_repo(&repo_path);
188
189 let registry = DynamicRepoRegistry::new();
190 registry
191 .register(RepoInfo {
192 name: "alpha.git".into(),
193 relative_path: "alpha.git".into(),
194 absolute_path: repo_path,
195 description: None,
196 })
197 .unwrap();
198
199 assert!(registry.resolve("./alpha.git").is_ok());
200 registry.unregister("./alpha.git").unwrap();
201 assert!(matches!(
202 registry.resolve("alpha.git"),
203 Err(Error::RepoNotFound(_))
204 ));
205 }
206
207 #[test]
208 fn dynamic_registry_rejects_duplicate_registration() {
209 let dir = TempDir::new().unwrap();
210 let repo_path = dir.path().join("alpha.git");
211 create_bare_repo(&repo_path);
212
213 let registry = DynamicRepoRegistry::new();
214 let repo = RepoInfo {
215 name: "alpha.git".into(),
216 relative_path: "alpha.git".into(),
217 absolute_path: repo_path,
218 description: None,
219 };
220
221 registry.register(repo.clone()).unwrap();
222 let err = registry.register(repo).unwrap_err();
223 assert!(matches!(err, Error::Protocol(_)));
224 }
225
226 #[test]
227 fn dynamic_registry_rejects_parent_relative_paths() {
228 let dir = TempDir::new().unwrap();
229 let repo_path = dir.path().join("alpha.git");
230 create_bare_repo(&repo_path);
231
232 let registry = DynamicRepoRegistry::new();
233 let err = registry
234 .register(RepoInfo {
235 name: "alpha.git".into(),
236 relative_path: "./team/../alpha.git".into(),
237 absolute_path: repo_path,
238 description: None,
239 })
240 .unwrap_err();
241
242 assert!(matches!(err, Error::PathTraversal(_)));
243 }
244
245 #[test]
246 fn dynamic_registry_rejects_absolute_relative_path() {
247 let dir = TempDir::new().unwrap();
248 let repo_path = dir.path().join("alpha.git");
249 create_bare_repo(&repo_path);
250
251 let registry = DynamicRepoRegistry::new();
252 let err = registry
253 .register(RepoInfo {
254 name: "alpha.git".into(),
255 relative_path: "/tmp/alpha.git".into(),
256 absolute_path: repo_path,
257 description: None,
258 })
259 .unwrap_err();
260
261 assert!(matches!(err, Error::PathTraversal(_)));
262 }
263}