1use anyhow::{bail, Context, Result};
26use nostr_sdk::ToBech32;
27use std::io::{BufRead, Write};
28use tracing::{debug, info, warn};
29
30mod git;
31mod helper;
32mod nostr_client;
33
34use hashtree_config::Config;
35use helper::RemoteHelper;
36use nostr_client::resolve_identity;
37
38pub fn main_entry() {
41 let _ = rustls::crypto::ring::default_provider().install_default();
43
44 if let Err(e) = run() {
45 eprintln!("Error: {:#}", e);
46 std::process::exit(1);
47 }
48}
49
50fn run() -> Result<()> {
51 #[cfg(unix)]
53 {
54 unsafe {
55 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
56 }
57 }
58
59 tracing_subscriber::fmt()
62 .with_env_filter(
63 tracing_subscriber::EnvFilter::from_default_env()
64 .add_directive("git_remote_htree=error".parse().unwrap())
65 .add_directive("nostr_relay_pool=off".parse().unwrap()),
66 )
67 .with_writer(std::io::stderr)
68 .init();
69
70 let args: Vec<String> = std::env::args().collect();
71 debug!("git-remote-htree called with args: {:?}", args);
72
73 if args.len() < 3 {
75 bail!("Usage: git-remote-htree <remote-name> <url>");
76 }
77
78 let remote_name = &args[1];
79 let url = &args[2];
80
81 info!("Remote: {}, URL: {}", remote_name, url);
82
83 let parsed = parse_htree_url(url)?;
85 let identifier = parsed.identifier;
86 let repo_name = parsed.repo_name;
87 let url_secret = parsed.secret_key; if url_secret.is_some() {
90 info!("Private repo mode: using secret key from URL");
91 }
92
93 let (pubkey, signing_key) = match resolve_identity(&identifier) {
96 Ok(result) => result,
97 Err(e) => {
98 warn!("Failed to resolve identity '{}': {}", identifier, e);
100 info!("Tip: Use htree://self/<repo> to auto-generate identity on first use");
101 return Err(e);
102 }
103 };
104
105 if signing_key.is_some() {
106 debug!("Found signing key for {}", identifier);
107 } else {
108 debug!("No signing key for {} (read-only)", identifier);
109 }
110
111 if let Ok(pk_bytes) = hex::decode(&pubkey) {
113 if pk_bytes.len() == 32 {
114 if let Ok(pk) = nostr_sdk::PublicKey::from_slice(&pk_bytes) {
115 if let Ok(npub) = pk.to_bech32() {
116 info!("Using identity: {}", npub);
117 }
118 }
119 }
120 }
121
122 let config = Config::load_or_default();
124 debug!("Loaded config with {} read servers, {} write servers",
125 config.blossom.read_servers.len(),
126 config.blossom.write_servers.len());
127
128 let mut helper = RemoteHelper::new(&pubkey, &repo_name, signing_key, url_secret, config)?;
130
131 let stdin = std::io::stdin();
133 let stdout = std::io::stdout();
134 let mut stdout = stdout.lock();
135
136 for line in stdin.lock().lines() {
137 let line = match line {
138 Ok(l) => l,
139 Err(e) => {
140 if e.kind() == std::io::ErrorKind::BrokenPipe {
141 break;
142 }
143 return Err(e.into());
144 }
145 };
146
147 debug!("< {}", line);
148
149 match helper.handle_command(&line) {
150 Ok(Some(responses)) => {
151 for response in responses {
152 debug!("> {}", response);
153 if let Err(e) = writeln!(stdout, "{}", response) {
154 if e.kind() == std::io::ErrorKind::BrokenPipe {
155 break;
156 }
157 return Err(e.into());
158 }
159 }
160 if let Err(e) = stdout.flush() {
161 if e.kind() == std::io::ErrorKind::BrokenPipe {
162 break;
163 }
164 return Err(e.into());
165 }
166 }
167 Ok(None) => {}
168 Err(e) => {
169 warn!("Command error: {}", e);
170 return Err(e);
172 }
173 }
174
175 if helper.should_exit() {
176 break;
177 }
178 }
179
180 Ok(())
181}
182
183pub struct ParsedUrl {
185 pub identifier: String,
186 pub repo_name: String,
187 pub secret_key: Option<[u8; 32]>,
189}
190
191fn parse_htree_url(url: &str) -> Result<ParsedUrl> {
194 let url = url
195 .strip_prefix("htree://")
196 .context("URL must start with htree://")?;
197
198 let (url_path, secret_key) = if let Some((path, fragment)) = url.split_once('#') {
200 let key = if let Some(key_hex) = fragment.strip_prefix("k=") {
201 let bytes = hex::decode(key_hex)
202 .context("Invalid secret key hex in URL fragment")?;
203 if bytes.len() != 32 {
204 bail!("Secret key must be 32 bytes (64 hex chars)");
205 }
206 let mut key = [0u8; 32];
207 key.copy_from_slice(&bytes);
208 Some(key)
209 } else {
210 None
211 };
212 (path, key)
213 } else {
214 (url, None)
215 };
216
217 let (identifier, repo) = url_path
219 .split_once('/')
220 .context("URL must be htree://<identifier>/<repo>")?;
221
222 let repo_name = repo.to_string();
224
225 if identifier.is_empty() {
226 bail!("Identifier cannot be empty");
227 }
228 if repo_name.is_empty() {
229 bail!("Repository name cannot be empty");
230 }
231
232 Ok(ParsedUrl {
233 identifier: identifier.to_string(),
234 repo_name,
235 secret_key,
236 })
237}
238
239pub fn generate_secret_key() -> [u8; 32] {
241 let mut key = [0u8; 32];
242 getrandom::fill(&mut key).expect("Failed to generate random bytes");
243 key
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_parse_htree_url_pubkey() {
252 let parsed = parse_htree_url(
253 "htree://a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8/myrepo",
254 )
255 .unwrap();
256 assert_eq!(
257 parsed.identifier,
258 "a9a91ed5f1c405618f63fdd393f9055ab8bac281102cff6b1ac3c74094562dd8"
259 );
260 assert_eq!(parsed.repo_name, "myrepo");
261 assert!(parsed.secret_key.is_none());
262 }
263
264 #[test]
265 fn test_parse_htree_url_npub() {
266 let parsed =
267 parse_htree_url("htree://npub1qvmu0aru530g6yu3kmlhw33fh68r75wf3wuml3vk4ekg0p4m4t6s7fuhxx/test")
268 .unwrap();
269 assert!(parsed.identifier.starts_with("npub1"));
270 assert_eq!(parsed.repo_name, "test");
271 assert!(parsed.secret_key.is_none());
272 }
273
274 #[test]
275 fn test_parse_htree_url_petname() {
276 let parsed = parse_htree_url("htree://alice/project").unwrap();
277 assert_eq!(parsed.identifier, "alice");
278 assert_eq!(parsed.repo_name, "project");
279 assert!(parsed.secret_key.is_none());
280 }
281
282 #[test]
283 fn test_parse_htree_url_self() {
284 let parsed = parse_htree_url("htree://self/myrepo").unwrap();
285 assert_eq!(parsed.identifier, "self");
286 assert_eq!(parsed.repo_name, "myrepo");
287 assert!(parsed.secret_key.is_none());
288 }
289
290 #[test]
291 fn test_parse_htree_url_with_subpath() {
292 let parsed = parse_htree_url("htree://test/repo/some/path").unwrap();
293 assert_eq!(parsed.identifier, "test");
294 assert_eq!(parsed.repo_name, "repo/some/path");
295 assert!(parsed.secret_key.is_none());
296 }
297
298 #[test]
299 fn test_parse_htree_url_with_secret() {
300 let secret_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
301 let url = format!("htree://test/repo#k={}", secret_hex);
302 let parsed = parse_htree_url(&url).unwrap();
303 assert_eq!(parsed.identifier, "test");
304 assert_eq!(parsed.repo_name, "repo");
305 assert!(parsed.secret_key.is_some());
306 let key = parsed.secret_key.unwrap();
307 assert_eq!(hex::encode(key), secret_hex);
308 }
309
310 #[test]
311 fn test_parse_htree_url_invalid_secret_length() {
312 let url = "htree://test/repo#k=0123456789abcdef";
314 assert!(parse_htree_url(url).is_err());
315 }
316
317 #[test]
318 fn test_parse_htree_url_invalid_secret_hex() {
319 let url = "htree://test/repo#k=ghij456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
321 assert!(parse_htree_url(url).is_err());
322 }
323
324 #[test]
325 fn test_parse_htree_url_invalid_scheme() {
326 assert!(parse_htree_url("https://example.com/repo").is_err());
327 }
328
329 #[test]
330 fn test_parse_htree_url_no_repo() {
331 assert!(parse_htree_url("htree://pubkey").is_err());
332 }
333
334 #[test]
335 fn test_parse_htree_url_empty_identifier() {
336 assert!(parse_htree_url("htree:///repo").is_err());
337 }
338
339 #[test]
340 fn test_parse_htree_url_colon() {
341 let result = parse_htree_url("htree://test:repo");
343 assert!(result.is_err()); }
345}