Skip to main content

lit/commands/
serve.rs

1use crate::commands;
2use crate::core::{find_repo_root, Object, ObjectHash};
3use crate::response::{CommandResponse, ServeResponse};
4use crate::storage::ObjectStore;
5use std::collections::{HashMap, HashSet};
6use std::io::{BufRead, Write};
7use std::net::{IpAddr, TcpListener};
8use std::sync::{Arc, Mutex};
9use std::time::Instant;
10use tiny_http::{Header, Method, Response, Server, StatusCode};
11
12/// Maximum request body size (1 MB)
13const MAX_BODY_SIZE: usize = 1_048_576;
14
15/// Maximum requests per IP per window
16const RATE_LIMIT_MAX_REQUESTS: u32 = 100;
17
18/// Rate limit window duration in seconds
19const RATE_LIMIT_WINDOW_SECS: u64 = 60;
20
21/// Per-IP rate limiter using a sliding window counter
22pub(crate) struct RateLimiter {
23    clients: HashMap<IpAddr, (Instant, u32)>,
24}
25
26impl RateLimiter {
27    pub(crate) fn new() -> Self {
28        RateLimiter {
29            clients: HashMap::new(),
30        }
31    }
32
33    /// Check whether a request from `ip` should be allowed.
34    /// Returns `true` if allowed, `false` if rate-limited.
35    pub(crate) fn check(&mut self, ip: IpAddr) -> bool {
36        let now = Instant::now();
37        let window = std::time::Duration::from_secs(RATE_LIMIT_WINDOW_SECS);
38
39        let entry = self.clients.entry(ip).or_insert((now, 0));
40        if now.duration_since(entry.0) >= window {
41            // Reset window
42            *entry = (now, 1);
43            true
44        } else if entry.1 < RATE_LIMIT_MAX_REQUESTS {
45            entry.1 += 1;
46            true
47        } else {
48            false
49        }
50    }
51}
52
53pub fn execute(port: u16, token: Option<String>) -> Result<ServeResponse, crate::errors::LitError> {
54    let repo_root = find_repo_root()?;
55    execute_at(port, token, repo_root)
56}
57
58/// Like `execute`, but takes an explicit repo root instead of searching
59/// from the current directory. Useful for tests that cannot rely on cwd.
60pub fn execute_at(
61    port: u16,
62    token: Option<String>,
63    repo_root: std::path::PathBuf,
64) -> Result<ServeResponse, crate::errors::LitError> {
65    let bind_addr = format!("127.0.0.1:{}", port);
66    let server = Server::http(&bind_addr)
67        .map_err(|e| format!("Failed to start server on {}: {}", bind_addr, e))?;
68
69    eprintln!("Lit API server listening on http://{}", bind_addr);
70    eprintln!("Repository: {}", repo_root.display());
71    if token.is_some() {
72        eprintln!("Authentication: Bearer token required");
73    }
74    eprintln!("Press Ctrl+C to stop");
75
76    let mut rate_limiter = RateLimiter::new();
77
78    for mut request in server.incoming_requests() {
79        // Rate limiting
80        if let Some(ip) = request.remote_addr().map(|a| a.ip()) {
81            if !rate_limiter.check(ip) {
82                let body = r#"{"status":"error","error":{"message":"Rate limit exceeded"}}"#;
83                let resp = Response::from_string(body)
84                    .with_status_code(StatusCode(429))
85                    .with_header(json_content_type());
86                let _ = request.respond(resp);
87                continue;
88            }
89        }
90
91        // Authenticate if token is set
92        if let Some(ref expected_token) = token {
93            let auth_header = request.headers().iter().find(|h| {
94                let name = h.field.as_str().to_string();
95                name.eq_ignore_ascii_case("authorization")
96            });
97
98            let authorized = match auth_header {
99                Some(h) => {
100                    let val = h.value.as_str();
101                    val.starts_with("Bearer ")
102                        && subtle::ConstantTimeEq::ct_eq(
103                            &val.as_bytes()[7..],
104                            expected_token.as_bytes(),
105                        )
106                        .into()
107                }
108                None => false,
109            };
110
111            if !authorized {
112                let body = r#"{"status":"error","error":{"message":"Unauthorized"}}"#;
113                let resp = Response::from_string(body)
114                    .with_status_code(StatusCode(401))
115                    .with_header(json_content_type());
116                let _ = request.respond(resp);
117                continue;
118            }
119        }
120
121        let method = request.method().clone();
122        let url = request.url().to_string();
123
124        // Read body before routing so request is available for respond
125        let body_str = read_body(&mut request).unwrap_or_default();
126        let result = route_request(method, &url, &body_str, &repo_root);
127
128        match result {
129            Ok((status, body)) => {
130                let resp = Response::from_string(body)
131                    .with_status_code(StatusCode(status))
132                    .with_header(json_content_type());
133                let _ = request.respond(resp);
134            }
135            Err(e) => {
136                // SECURITY: Log internal message server-side, return generic message to client
137                eprintln!("API error: {}", e.internal_message());
138                let body = serde_json::json!({
139                    "status": "error",
140                    "error": {"message": e.user_message()}
141                })
142                .to_string();
143                let resp = Response::from_string(body)
144                    .with_status_code(StatusCode(500))
145                    .with_header(json_content_type());
146                let _ = request.respond(resp);
147            }
148        }
149    }
150
151    Ok(ServeResponse {
152        message: "Server stopped".to_string(),
153    })
154}
155
156/// Execute the server in stdio pipe mode.
157/// Reads newline-delimited JSON requests from stdin, routes them through
158/// `route_request`, and writes JSON responses to stdout. Used by SSH transport.
159pub fn execute_stdio() -> Result<ServeResponse, crate::errors::LitError> {
160    let repo_root = find_repo_root()?;
161    let stdin = std::io::stdin();
162    let stdout = std::io::stdout();
163    let reader = stdin.lock();
164    let mut writer = stdout.lock();
165
166    for line in reader.lines() {
167        let line = match line {
168            Ok(l) => l,
169            Err(_) => break, // EOF or pipe closed
170        };
171        let line = line.trim().to_string();
172        if line.is_empty() {
173            continue;
174        }
175
176        let req: serde_json::Value = match serde_json::from_str(&line) {
177            Ok(v) => v,
178            Err(e) => {
179                let resp =
180                    serde_json::json!({"status": 400, "body": format!("Invalid JSON: {}", e)});
181                let _ = writeln!(writer, "{}", resp);
182                let _ = writer.flush();
183                continue;
184            }
185        };
186
187        let method_str = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
188        let path = req.get("path").and_then(|v| v.as_str()).unwrap_or("/");
189        let body = req.get("body").and_then(|v| v.as_str()).unwrap_or("");
190
191        let method = match method_str.to_uppercase().as_str() {
192            "GET" => Method::Get,
193            "POST" => Method::Post,
194            "PUT" => Method::Put,
195            "DELETE" => Method::Delete,
196            _ => Method::Get,
197        };
198
199        let (status, response_body) = match route_request(method, path, body, &repo_root) {
200            Ok((s, b)) => (s, b),
201            Err(e) => {
202                // SECURITY: Log internal message server-side, return generic message to client
203                eprintln!("Stdio API error: {}", e.internal_message());
204                let err_body = serde_json::json!({
205                    "status": "error",
206                    "error": {"message": e.user_message()}
207                })
208                .to_string();
209                (500, err_body)
210            }
211        };
212
213        let resp = serde_json::json!({"status": status, "body": response_body});
214        let _ = writeln!(writer, "{}", resp);
215        let _ = writer.flush();
216    }
217
218    Ok(ServeResponse {
219        message: "Stdio server stopped".to_string(),
220    })
221}
222
223/// Execute the server as a lit:// protocol TCP daemon.
224/// Accepts TCP connections and handles each with the same newline-delimited
225/// JSON protocol as stdio mode. Used by the `lit://` native transport.
226pub fn execute_daemon(port: u16) -> Result<ServeResponse, crate::errors::LitError> {
227    let repo_root = find_repo_root()?;
228    // SECURITY: Bind to localhost only — network exposure requires explicit reverse proxy
229    let bind_addr = format!("127.0.0.1:{}", port);
230    let listener = TcpListener::bind(&bind_addr)
231        .map_err(|e| format!("Failed to bind lit:// daemon on {}: {}", bind_addr, e))?;
232
233    eprintln!("Lit daemon listening on lit://127.0.0.1:{}", port);
234    eprintln!("Repository: {}", repo_root.display());
235    eprintln!("Press Ctrl+C to stop");
236
237    let rate_limiter = Arc::new(Mutex::new(RateLimiter::new()));
238
239    for stream in listener.incoming() {
240        let stream = match stream {
241            Ok(s) => s,
242            Err(e) => {
243                eprintln!("Accept error: {}", e);
244                continue;
245            }
246        };
247
248        // Rate limit per peer IP
249        if let Ok(addr) = stream.peer_addr() {
250            if let Ok(mut rl) = rate_limiter.lock() {
251                if !rl.check(addr.ip()) {
252                    // Silently drop over-limit connections
253                    continue;
254                }
255            }
256        }
257
258        let repo = repo_root.clone();
259        std::thread::spawn(move || {
260            handle_daemon_connection(stream, &repo);
261        });
262    }
263
264    Ok(ServeResponse {
265        message: "Daemon stopped".to_string(),
266    })
267}
268
269/// Handle a single lit:// daemon TCP connection
270fn handle_daemon_connection(stream: std::net::TcpStream, repo_root: &std::path::Path) {
271    let reader_stream = match stream.try_clone() {
272        Ok(s) => s,
273        Err(_) => return,
274    };
275    let reader = std::io::BufReader::new(reader_stream);
276    let mut writer = std::io::BufWriter::new(stream);
277
278    for line in reader.lines() {
279        let line = match line {
280            Ok(l) => l,
281            Err(_) => break,
282        };
283        let line = line.trim().to_string();
284        if line.is_empty() {
285            continue;
286        }
287
288        let req: serde_json::Value = match serde_json::from_str(&line) {
289            Ok(v) => v,
290            Err(e) => {
291                let resp =
292                    serde_json::json!({"status": 400, "body": format!("Invalid JSON: {}", e)});
293                let _ = writeln!(writer, "{}", resp);
294                let _ = writer.flush();
295                continue;
296            }
297        };
298
299        let method_str = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
300        let path = req.get("path").and_then(|v| v.as_str()).unwrap_or("/");
301        let body = req.get("body").and_then(|v| v.as_str()).unwrap_or("");
302
303        let method = match method_str.to_uppercase().as_str() {
304            "GET" => Method::Get,
305            "POST" => Method::Post,
306            "PUT" => Method::Put,
307            "DELETE" => Method::Delete,
308            _ => Method::Get,
309        };
310
311        let (status, response_body) = match route_request(method, path, body, repo_root) {
312            Ok((s, b)) => (s, b),
313            Err(e) => {
314                // SECURITY: Log internal message server-side, return sanitized message to client (FINDING-002)
315                eprintln!("Daemon API error: {}", e.internal_message());
316                let err_body = serde_json::json!({
317                    "status": "error",
318                    "error": {"message": e.user_message()}
319                })
320                .to_string();
321                (500, err_body)
322            }
323        };
324
325        let resp = serde_json::json!({"status": status, "body": response_body});
326        if writeln!(writer, "{}", resp).is_err() {
327            break;
328        }
329        if writer.flush().is_err() {
330            break;
331        }
332    }
333}
334
335fn json_content_type() -> Header {
336    Header::from_bytes("Content-Type", "application/json").unwrap()
337}
338
339fn read_body(request: &mut tiny_http::Request) -> Result<String, crate::errors::LitError> {
340    let content_length = request.body_length().unwrap_or(0);
341    if content_length > MAX_BODY_SIZE {
342        return Err("Request body too large".into());
343    }
344    let mut body = String::new();
345    request
346        .as_reader()
347        .read_to_string(&mut body)
348        .map_err(|e| format!("Failed to read request body: {}", e))?;
349    Ok(body)
350}
351
352fn route_request(
353    method: Method,
354    url: &str,
355    body: &str,
356    repo_root: &std::path::Path,
357) -> Result<(u16, String), crate::errors::LitError> {
358    let path = url.split('?').next().unwrap_or(url);
359
360    match (method, path) {
361        // Discovery
362        (Method::Get, "/api/v1") | (Method::Get, "/api/v1/") => {
363            let info = serde_json::json!({
364                "name": "lit",
365                "version": env!("CARGO_PKG_VERSION"),
366                "api_version": "v1",
367                "endpoints": [
368                    "GET  /api/v1/status",
369                    "GET  /api/v1/log?count=N",
370                    "GET  /api/v1/branches",
371                    "GET  /api/v1/diff?staged=bool",
372                    "GET  /api/v1/show/:ref",
373                    "GET  /api/v1/tags",
374                    "GET  /api/v1/remotes",
375                    "GET  /api/v1/config",
376                    "GET  /api/v1/search?q=query&messages=bool",
377                    "GET  /api/v1/verify",
378                    "GET  /api/v1/ontology",
379                    "POST /api/v1/add",
380                    "POST /api/v1/commit",
381                    "POST /api/v1/snapshot",
382                    "POST /api/v1/checkout",
383                    "POST /api/v1/merge",
384                    "POST /api/v1/branch",
385                ]
386            });
387            Ok((200, serde_json::to_string_pretty(&info).unwrap()))
388        }
389
390        // GET endpoints
391        (Method::Get, "/api/v1/status") => {
392            let resp = commands::status::execute()?;
393            Ok((200, resp.to_json_output()))
394        }
395
396        (Method::Get, "/api/v1/log") => {
397            let count = parse_query_param(url, "count")
398                .and_then(|s| s.parse::<usize>().ok())
399                .unwrap_or(10);
400            let oneline = parse_query_param(url, "oneline")
401                .map(|s| s == "true")
402                .unwrap_or(false);
403            let resp = commands::log::execute(count, oneline)?;
404            Ok((200, resp.to_json_output()))
405        }
406
407        (Method::Get, "/api/v1/branches") => {
408            let resp = commands::branch::execute(None, false, true)?;
409            Ok((200, resp.to_json_output()))
410        }
411
412        (Method::Get, "/api/v1/diff") => {
413            let staged = parse_query_param(url, "staged")
414                .map(|s| s == "true")
415                .unwrap_or(false);
416            let stat = parse_query_param(url, "stat")
417                .map(|s| s == "true")
418                .unwrap_or(false);
419            let resp = commands::diff::execute(staged, stat, false, None, None)?;
420            Ok((200, resp.to_json_output()))
421        }
422
423        (Method::Get, p) if p.starts_with("/api/v1/show/") => {
424            let object = &p["/api/v1/show/".len()..];
425            if object.is_empty() {
426                return Ok((400, r#"{"status":"error","error":{"message":"Missing object ref"}}"#.to_string()));
427            }
428            if !is_valid_ref(object) {
429                return Ok((400, r#"{"status":"error","error":{"message":"Invalid object ref"}}"#.to_string()));
430            }
431            let resp = commands::show::execute(object.to_string())?;
432            Ok((200, resp.to_json_output()))
433        }
434
435        (Method::Get, "/api/v1/tags") => {
436            let resp = commands::tag::execute(None, None, false, false, false, false, true, None)?;
437            Ok((200, resp.to_json_output()))
438        }
439
440        (Method::Get, "/api/v1/remotes") => {
441            let resp = commands::remote::execute(Some(crate::RemoteCommands::List { verbose: true }))?;
442            Ok((200, resp.to_json_output()))
443        }
444
445        (Method::Get, "/api/v1/config") => {
446            let resp = commands::config::execute(Some(crate::ConfigCommands::Show))?;
447            Ok((200, resp.to_json_output()))
448        }
449
450        (Method::Get, "/api/v1/search") => {
451            let query = parse_query_param(url, "q").unwrap_or_default();
452            if query.is_empty() {
453                return Ok((400, r#"{"status":"error","error":{"message":"Missing query parameter 'q'"}}"#.to_string()));
454            }
455            let messages = parse_query_param(url, "messages")
456                .map(|s| s == "true")
457                .unwrap_or(false);
458            let metadata = parse_query_param(url, "metadata");
459            let max = parse_query_param(url, "max")
460                .and_then(|s| s.parse::<usize>().ok())
461                .unwrap_or(100);
462            let resp = commands::search::execute(query, messages, metadata, max)?;
463            Ok((200, resp.to_json_output()))
464        }
465
466        (Method::Get, "/api/v1/verify") => {
467            let resp = commands::verify::execute()?;
468            Ok((200, resp.to_json_output()))
469        }
470
471        (Method::Get, "/api/v1/ontology") => {
472            let resp = crate::ontology::get_ontology();
473            Ok((200, serde_json::to_string_pretty(&resp).unwrap()))
474        }
475
476        // POST endpoints
477        (Method::Post, "/api/v1/add") => {
478            let payload: serde_json::Value = serde_json::from_str(body)
479                .map_err(|e| format!("Invalid JSON: {}", e))?;
480            let files: Vec<String> = payload
481                .get("files")
482                .and_then(|v| serde_json::from_value(v.clone()).ok())
483                .unwrap_or_default();
484            if files.is_empty() {
485                return Ok((400, r#"{"status":"error","error":{"message":"Missing 'files' array"}}"#.to_string()));
486            }
487            let resp = commands::add::execute(files)?;
488            Ok((200, resp.to_json_output()))
489        }
490
491        (Method::Post, "/api/v1/commit") => {
492            let payload: serde_json::Value = serde_json::from_str(body)
493                .map_err(|e| format!("Invalid JSON: {}", e))?;
494            let message = payload
495                .get("message")
496                .and_then(|v| v.as_str())
497                .ok_or("Missing 'message' field")?
498                .to_string();
499            let author = payload
500                .get("author")
501                .and_then(|v| v.as_str())
502                .map(|s| s.to_string());
503            let resp = commands::commit::execute(message, author)?;
504            Ok((200, resp.to_json_output()))
505        }
506
507        (Method::Post, "/api/v1/snapshot") => {
508            let payload: serde_json::Value = serde_json::from_str(body)
509                .map_err(|e| format!("Invalid JSON: {}", e))?;
510            let message = payload
511                .get("message")
512                .and_then(|v| v.as_str())
513                .ok_or("Missing 'message' field")?
514                .to_string();
515            let author = payload
516                .get("author")
517                .and_then(|v| v.as_str())
518                .map(|s| s.to_string());
519            let metadata = payload.get("metadata").cloned();
520            let resp = commands::snapshot::execute(message, author, metadata)?;
521            Ok((200, resp.to_json_output()))
522        }
523
524        (Method::Post, "/api/v1/checkout") => {
525            let payload: serde_json::Value = serde_json::from_str(body)
526                .map_err(|e| format!("Invalid JSON: {}", e))?;
527            let target = payload
528                .get("target")
529                .and_then(|v| v.as_str())
530                .ok_or("Missing 'target' field")?
531                .to_string();
532            let create = payload
533                .get("create")
534                .and_then(|v| v.as_bool())
535                .unwrap_or(false);
536            let resp = commands::checkout::execute(target, create)?;
537            Ok((200, resp.to_json_output()))
538        }
539
540        (Method::Post, "/api/v1/merge") => {
541            let payload: serde_json::Value = serde_json::from_str(body)
542                .map_err(|e| format!("Invalid JSON: {}", e))?;
543            let branch = payload
544                .get("branch")
545                .and_then(|v| v.as_str())
546                .ok_or("Missing 'branch' field")?
547                .to_string();
548            let strategy = payload
549                .get("strategy")
550                .and_then(|v| v.as_str())
551                .map(|s| s.to_string());
552            let resp = commands::merge::execute(branch, strategy)?;
553            Ok((200, resp.to_json_output()))
554        }
555
556        (Method::Post, "/api/v1/branch") => {
557            let payload: serde_json::Value = serde_json::from_str(body)
558                .map_err(|e| format!("Invalid JSON: {}", e))?;
559            let name = payload
560                .get("name")
561                .and_then(|v| v.as_str())
562                .map(|s| s.to_string());
563            let delete = payload
564                .get("delete")
565                .and_then(|v| v.as_bool())
566                .unwrap_or(false);
567            let resp = commands::branch::execute(name, delete, false)?;
568            Ok((200, resp.to_json_output()))
569        }
570
571        // ── Transport API endpoints ──
572        // List refs (branches + tags)
573        (Method::Get, "/api/v1/transport/refs") => {
574            let kind = parse_query_param(url, "kind").unwrap_or_else(|| "all".to_string());
575            let mut refs = Vec::new();
576            if kind == "all" || kind == "heads" {
577                if let Ok(head_refs) = crate::core::refs::list_refs(repo_root, "heads") {
578                    for r in head_refs {
579                        refs.push(serde_json::json!({"kind": "heads", "name": r.name, "hash": r.hash}));
580                    }
581                }
582            }
583            if kind == "all" || kind == "tags" {
584                if let Ok(tag_refs) = crate::core::refs::list_refs(repo_root, "tags") {
585                    for r in tag_refs {
586                        refs.push(serde_json::json!({"kind": "tags", "name": r.name, "hash": r.hash}));
587                    }
588                }
589            }
590            Ok((200, serde_json::json!({"refs": refs}).to_string()))
591        }
592
593        // Read HEAD
594        (Method::Get, "/api/v1/transport/head") => {
595            let head = std::fs::read_to_string(repo_root.join(".lit").join("HEAD"))
596                .map_err(|e| format!("Failed to read HEAD: {}", e))?;
597            Ok((200, serde_json::json!({"head": head.trim()}).to_string()))
598        }
599
600        // Read a specific ref
601        (Method::Get, p) if p.starts_with("/api/v1/transport/refs/heads/") => {
602            let branch = &p["/api/v1/transport/refs/heads/".len()..];
603            if !is_valid_ref(branch) {
604                return Ok((400, r#"{"status":"error","error":{"message":"Invalid branch name"}}"#.to_string()));
605            }
606            let hash = crate::core::refs::read_ref(repo_root, &format!("heads/{}", branch))?;
607            Ok((200, serde_json::json!({"branch": branch, "hash": hash}).to_string()))
608        }
609
610        // Check if object exists
611        (Method::Get, p) if p.starts_with("/api/v1/transport/objects/") && p.ends_with("/exists") => {
612            let hash_str = &p["/api/v1/transport/objects/".len()..p.len() - "/exists".len()];
613            if !is_valid_hex_hash(hash_str) {
614                return Ok((400, r#"{"status":"error","error":{"message":"Invalid object hash"}}"#.to_string()));
615            }
616            let store = ObjectStore::new(repo_root);
617            let exists = store.exists(&ObjectHash::from_hex(hash_str.to_string()));
618            Ok((200, serde_json::json!({"hash": hash_str, "exists": exists}).to_string()))
619        }
620
621        // Download a single object (serialized bytes, base64 encoded)
622        (Method::Get, p) if p.starts_with("/api/v1/transport/objects/") => {
623            let hash_str = &p["/api/v1/transport/objects/".len()..];
624            if !is_valid_hex_hash(hash_str) {
625                return Ok((400, r#"{"status":"error","error":{"message":"Invalid object hash"}}"#.to_string()));
626            }
627            let store = ObjectStore::new(repo_root);
628            let hash = ObjectHash::from_hex(hash_str.to_string());
629            let obj = store.read(&hash)?;
630            let data = obj.to_bytes();
631            use std::io::Write as _;
632            let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
633            encoder.write_all(&data).map_err(|e| format!("Compress error: {}", e))?;
634            let compressed = encoder.finish().map_err(|e| format!("Compress error: {}", e))?;
635            let b64 = base64_encode(&compressed);
636            Ok((200, serde_json::json!({"hash": hash_str, "data": b64}).to_string()))
637        }
638
639        // Batch upload objects
640        (Method::Post, "/api/v1/transport/objects") => {
641            let store = ObjectStore::new(repo_root);
642            let payload: serde_json::Value = serde_json::from_str(body)
643                .map_err(|e| format!("Invalid JSON: {}", e))?;
644            let objects = payload.get("objects")
645                .and_then(|v| v.as_array())
646                .ok_or("Missing 'objects' array")?;
647            let mut written = 0;
648            for entry in objects {
649                let b64_data = entry.get("data")
650                    .and_then(|v| v.as_str())
651                    .ok_or("Missing 'data' field in object entry")?;
652                let compressed = base64_decode(b64_data)?;
653                use std::io::Read as _;
654                let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
655                let mut raw = Vec::new();
656                decoder.read_to_end(&mut raw)
657                    .map_err(|e| format!("Decompress error: {}", e))?;
658                let obj = Object::from_bytes(&raw)?;
659                store.write(&obj)?;
660                written += 1;
661            }
662            Ok((200, serde_json::json!({"written": written}).to_string()))
663        }
664
665        // Update a branch ref
666        (Method::Put, p) if p.starts_with("/api/v1/transport/refs/heads/") => {
667            let branch = &p["/api/v1/transport/refs/heads/".len()..];
668            if !is_valid_ref(branch) {
669                return Ok((400, r#"{"status":"error","error":{"message":"Invalid branch name"}}"#.to_string()));
670            }
671            let payload: serde_json::Value = serde_json::from_str(body)
672                .map_err(|e| format!("Invalid JSON: {}", e))?;
673            let hash = payload.get("hash")
674                .and_then(|v| v.as_str())
675                .ok_or("Missing 'hash' field")?;
676            let force = payload.get("force")
677                .and_then(|v| v.as_bool())
678                .unwrap_or(false);
679
680            // Fast-forward check unless force
681            if !force {
682                if let Ok(current) = crate::core::refs::read_ref(repo_root, &format!("heads/{}", branch)) {
683                    let store = ObjectStore::new(repo_root);
684                    let old_hash = ObjectHash::from_hex(current);
685                    let new_hash = ObjectHash::from_hex(hash.to_string());
686                    let is_ff = crate::core::merge::is_ancestor(&store, &old_hash, &new_hash)?;
687                    if !is_ff {
688                        return Ok((409, serde_json::json!({
689                            "status": "error",
690                            "error": {"message": "Non-fast-forward update rejected. Use force=true."}
691                        }).to_string()));
692                    }
693                }
694            }
695
696            crate::core::refs::write_ref(repo_root, &format!("heads/{}", branch), hash)?;
697            Ok((200, serde_json::json!({"branch": branch, "hash": hash, "updated": true}).to_string()))
698        }
699
700        // Server-side graph walk (negotiate)
701        (Method::Post, "/api/v1/transport/negotiate") => {
702            let store = ObjectStore::new(repo_root);
703            let payload: serde_json::Value = serde_json::from_str(body)
704                .map_err(|e| format!("Invalid JSON: {}", e))?;
705            let wants: Vec<String> = payload.get("wants")
706                .and_then(|v| serde_json::from_value(v.clone()).ok())
707                .unwrap_or_default();
708            let haves: Vec<String> = payload.get("haves")
709                .and_then(|v| serde_json::from_value(v.clone()).ok())
710                .unwrap_or_default();
711            let known: HashSet<String> = haves.into_iter().collect();
712            let mut all_needed = Vec::new();
713            for want in &wants {
714                let hash = ObjectHash::from_hex(want.clone());
715                let needed = crate::network::transport::walk_commit_graph(&store, &hash, &known)?;
716                for h in needed {
717                    let s = h.as_str().to_string();
718                    if !all_needed.contains(&s) {
719                        all_needed.push(s);
720                    }
721                }
722            }
723            Ok((200, serde_json::json!({"needed": all_needed}).to_string()))
724        }
725
726        _ => Ok((
727            404,
728            r#"{"status":"error","error":{"message":"Not found. GET /api/v1 for available endpoints."}}"#
729                .to_string(),
730        )),
731    }
732}
733
734fn url_decode(s: &str) -> String {
735    let mut result = Vec::new();
736    let bytes = s.as_bytes();
737    let mut i = 0;
738    while i < bytes.len() {
739        if bytes[i] == b'%' && i + 2 < bytes.len() {
740            if let Ok(byte) =
741                u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
742            {
743                result.push(byte);
744                i += 3;
745                continue;
746            }
747        }
748        if bytes[i] == b'+' {
749            result.push(b' ');
750        } else {
751            result.push(bytes[i]);
752        }
753        i += 1;
754    }
755    String::from_utf8(result).unwrap_or_default()
756}
757
758fn parse_query_param(url: &str, key: &str) -> Option<String> {
759    let query = url.split('?').nth(1)?;
760    for pair in query.split('&') {
761        let mut parts = pair.splitn(2, '=');
762        if let (Some(k), Some(v)) = (parts.next(), parts.next()) {
763            if k == key {
764                return Some(url_decode(v));
765            }
766        }
767    }
768    None
769}
770
771/// Validate a ref name (branch, tag) — alphanumeric, hyphens, underscores, dots, slashes.
772/// Rejects empty strings, leading/trailing slashes, double dots, and path traversal.
773fn is_valid_ref(name: &str) -> bool {
774    if name.is_empty() || name.len() > 256 {
775        return false;
776    }
777    if name.contains("..") || name.contains("//") || name.starts_with('/') || name.ends_with('/') {
778        return false;
779    }
780    name.chars()
781        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/')
782}
783
784/// Validate a hex-encoded object hash (up to 192 hex characters for SHA3-512+BLAKE3 composite).
785fn is_valid_hex_hash(s: &str) -> bool {
786    !s.is_empty() && s.len() <= 192 && s.chars().all(|c| c.is_ascii_hexdigit())
787}
788
789/// Base64 encode bytes (standard alphabet, no padding)
790fn base64_encode(data: &[u8]) -> String {
791    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
792    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
793    for chunk in data.chunks(3) {
794        let b0 = chunk[0] as u32;
795        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
796        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
797        let triple = (b0 << 16) | (b1 << 8) | b2;
798        out.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
799        out.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
800        if chunk.len() > 1 {
801            out.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
802        } else {
803            out.push('=');
804        }
805        if chunk.len() > 2 {
806            out.push(CHARS[(triple & 0x3F) as usize] as char);
807        } else {
808            out.push('=');
809        }
810    }
811    out
812}
813
814/// Base64 decode string to bytes
815fn base64_decode(input: &str) -> Result<Vec<u8>, crate::errors::LitError> {
816    fn val(c: u8) -> Result<u32, crate::errors::LitError> {
817        match c {
818            b'A'..=b'Z' => Ok((c - b'A') as u32),
819            b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
820            b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
821            b'+' => Ok(62),
822            b'/' => Ok(63),
823            b'=' => Ok(0),
824            _ => Err(format!("Invalid base64 character: {}", c as char).into()),
825        }
826    }
827    let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
828    let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
829    for chunk in bytes.chunks(4) {
830        if chunk.len() < 4 {
831            return Err("Invalid base64 length".into());
832        }
833        let a = val(chunk[0])?;
834        let b = val(chunk[1])?;
835        let c = val(chunk[2])?;
836        let d = val(chunk[3])?;
837        let triple = (a << 18) | (b << 12) | (c << 6) | d;
838        out.push(((triple >> 16) & 0xFF) as u8);
839        if chunk[2] != b'=' {
840            out.push(((triple >> 8) & 0xFF) as u8);
841        }
842        if chunk[3] != b'=' {
843            out.push((triple & 0xFF) as u8);
844        }
845    }
846    Ok(out)
847}