Skip to main content

lit/network/
lit_protocol.rs

1//! Native Lit protocol transport (`lit://`) for remote Lit repositories
2//!
3//! The `lit://` protocol is Lit's native transport using TCP connections
4//! with newline-delimited JSON framing. The server side is provided by
5//! `lit serve --daemon`, which accepts TCP connections on the configured
6//! port (default 9418) and processes the same JSON API as the stdio and
7//! HTTP modes.
8
9use crate::core::{Object, ObjectHash};
10use crate::network::transport::RemoteRef;
11use crate::storage::ObjectStore;
12use std::io::{BufRead, BufReader, BufWriter, Write};
13use std::net::TcpStream;
14
15/// Default port for the lit:// protocol
16pub const DEFAULT_PORT: u16 = 9418;
17
18/// Check whether a URL uses the lit:// transport
19pub fn is_lit_url(url: &str) -> bool {
20    url.starts_with("lit://")
21}
22
23/// Parsed lit:// URL components
24#[derive(Debug, Clone)]
25pub struct LitUrl {
26    pub host: String,
27    pub port: u16,
28    pub path: String,
29}
30
31/// Parse a `lit://host[:port]/path` URL into its components
32pub fn parse_lit_url(url: &str) -> Result<LitUrl, String> {
33    let rest = url
34        .strip_prefix("lit://")
35        .ok_or_else(|| format!("Not a lit:// URL: {}", url))?;
36
37    let (hostport, path) = rest
38        .split_once('/')
39        .ok_or_else(|| format!("Invalid lit:// URL (missing path): {}", url))?;
40
41    let (host, port) = if let Some((h, p)) = hostport.split_once(':') {
42        let port_num = p
43            .parse::<u16>()
44            .map_err(|_| format!("Invalid port in lit:// URL: {}", p))?;
45        (h.to_string(), port_num)
46    } else {
47        (hostport.to_string(), DEFAULT_PORT)
48    };
49
50    if host.is_empty() {
51        return Err(format!("Empty host in lit:// URL: {}", url));
52    }
53
54    Ok(LitUrl {
55        host,
56        port,
57        path: format!("/{}", path),
58    })
59}
60
61/// A TCP connection to a remote `lit serve --daemon` instance
62pub struct LitConnection {
63    reader: BufReader<TcpStream>,
64    writer: BufWriter<TcpStream>,
65}
66
67impl LitConnection {
68    /// Connect to a remote lit:// daemon
69    pub fn open(parsed: &LitUrl) -> Result<Self, String> {
70        let addr = format!("{}:{}", parsed.host, parsed.port);
71        let stream = TcpStream::connect(&addr)
72            .map_err(|e| format!("Failed to connect to lit://{}: {}", addr, e))?;
73
74        let reader_stream = stream
75            .try_clone()
76            .map_err(|e| format!("Failed to clone TCP stream: {}", e))?;
77
78        Ok(LitConnection {
79            reader: BufReader::new(reader_stream),
80            writer: BufWriter::new(stream),
81        })
82    }
83
84    /// Connect to a local daemon (for testing)
85    pub fn open_local(port: u16) -> Result<Self, String> {
86        let addr = format!("127.0.0.1:{}", port);
87        let stream = TcpStream::connect(&addr)
88            .map_err(|e| format!("Failed to connect to lit daemon at {}: {}", addr, e))?;
89
90        let reader_stream = stream
91            .try_clone()
92            .map_err(|e| format!("Failed to clone TCP stream: {}", e))?;
93
94        Ok(LitConnection {
95            reader: BufReader::new(reader_stream),
96            writer: BufWriter::new(stream),
97        })
98    }
99
100    /// Send a request and read the response
101    fn request(
102        &mut self,
103        method: &str,
104        path: &str,
105        body: &str,
106    ) -> Result<(u16, serde_json::Value), String> {
107        let req = serde_json::json!({
108            "method": method,
109            "path": path,
110            "body": body,
111        });
112        writeln!(self.writer, "{}", req)
113            .map_err(|e| format!("Failed to write to lit:// connection: {}", e))?;
114        self.writer
115            .flush()
116            .map_err(|e| format!("Failed to flush lit:// connection: {}", e))?;
117
118        let mut line = String::new();
119        self.reader
120            .read_line(&mut line)
121            .map_err(|e| format!("Failed to read from lit:// connection: {}", e))?;
122
123        if line.is_empty() {
124            return Err("lit:// connection closed unexpectedly".to_string());
125        }
126
127        let resp: serde_json::Value = serde_json::from_str(line.trim())
128            .map_err(|e| format!("Invalid JSON from lit:// connection: {}", e))?;
129
130        let status = resp.get("status").and_then(|v| v.as_u64()).unwrap_or(500) as u16;
131
132        let body_str = resp.get("body").and_then(|v| v.as_str()).unwrap_or("{}");
133
134        let body_json: serde_json::Value =
135            serde_json::from_str(body_str).unwrap_or_else(|_| serde_json::json!({"raw": body_str}));
136
137        Ok((status, body_json))
138    }
139}
140
141/// Check response status and extract error messages
142fn check_status(status: u16, body: &serde_json::Value) -> Result<(), String> {
143    if status >= 400 {
144        let msg = body
145            .get("error")
146            .and_then(|e| e.get("message"))
147            .and_then(|m| m.as_str())
148            .or_else(|| body.get("raw").and_then(|v| v.as_str()))
149            .unwrap_or("Unknown error");
150        Err(format!("lit:// transport error ({}): {}", status, msg))
151    } else {
152        Ok(())
153    }
154}
155
156/// List refs from a remote server via lit:// connection
157pub fn list_refs_lit(conn: &mut LitConnection, kind: &str) -> Result<Vec<RemoteRef>, String> {
158    let path = format!("/api/v1/transport/refs?kind={}", kind);
159    let (status, body) = conn.request("GET", &path, "")?;
160    check_status(status, &body)?;
161
162    let refs = body
163        .get("refs")
164        .and_then(|v| v.as_array())
165        .ok_or("Invalid refs response from lit://")?;
166
167    let mut result = Vec::new();
168    for r in refs {
169        let kind = r.get("kind").and_then(|v| v.as_str()).unwrap_or("heads");
170        let name = r
171            .get("name")
172            .and_then(|v| v.as_str())
173            .ok_or("Missing ref name")?;
174        let hash = r
175            .get("hash")
176            .and_then(|v| v.as_str())
177            .ok_or("Missing ref hash")?;
178        result.push(RemoteRef {
179            kind: kind.to_string(),
180            name: name.to_string(),
181            hash: hash.to_string(),
182        });
183    }
184    Ok(result)
185}
186
187/// Read a branch ref from a remote server via lit:// connection
188pub fn read_ref_lit(conn: &mut LitConnection, branch: &str) -> Result<String, String> {
189    let path = format!("/api/v1/transport/refs/heads/{}", branch);
190    let (status, body) = conn.request("GET", &path, "")?;
191    check_status(status, &body)?;
192    body.get("hash")
193        .and_then(|v| v.as_str())
194        .map(|s| s.to_string())
195        .ok_or("Missing hash in lit:// response".to_string())
196}
197
198/// Read HEAD from a remote server via lit:// connection
199pub fn read_head_lit(conn: &mut LitConnection) -> Result<String, String> {
200    let (status, body) = conn.request("GET", "/api/v1/transport/head", "")?;
201    check_status(status, &body)?;
202    body.get("head")
203        .and_then(|v| v.as_str())
204        .map(|s| s.to_string())
205        .ok_or("Missing head in lit:// response".to_string())
206}
207
208/// Update a branch ref on a remote server via lit:// connection
209pub fn update_ref_lit(
210    conn: &mut LitConnection,
211    branch: &str,
212    hash: &str,
213    force: bool,
214) -> Result<(), String> {
215    let path = format!("/api/v1/transport/refs/heads/{}", branch);
216    let body = serde_json::json!({"hash": hash, "force": force}).to_string();
217    let (status, resp) = conn.request("PUT", &path, &body)?;
218    check_status(status, &resp)
219}
220
221/// Negotiate which objects are needed via lit:// connection
222pub fn negotiate_lit(
223    conn: &mut LitConnection,
224    wants: &[String],
225    haves: &[String],
226) -> Result<Vec<ObjectHash>, String> {
227    let body = serde_json::json!({"wants": wants, "haves": haves}).to_string();
228    let (status, resp) = conn.request("POST", "/api/v1/transport/negotiate", &body)?;
229    check_status(status, &resp)?;
230
231    let needed = resp
232        .get("needed")
233        .and_then(|v| v.as_array())
234        .ok_or("Invalid negotiate response from lit://")?;
235
236    Ok(needed
237        .iter()
238        .filter_map(|v| v.as_str())
239        .map(|s| ObjectHash::from_hex(s.to_string()))
240        .collect())
241}
242
243/// Download objects from a remote server via lit:// connection
244pub fn download_objects_lit(
245    conn: &mut LitConnection,
246    local_store: &ObjectStore,
247    hashes: &[ObjectHash],
248) -> Result<usize, String> {
249    let mut count = 0;
250    for hash in hashes {
251        if local_store.exists(hash) {
252            continue;
253        }
254        let path = format!("/api/v1/transport/objects/{}", hash.as_str());
255        let (status, body) = conn.request("GET", &path, "")?;
256        check_status(status, &body)?;
257
258        let b64_data = body
259            .get("data")
260            .and_then(|v| v.as_str())
261            .ok_or("Missing object data in lit:// response")?;
262
263        let compressed = base64_decode(b64_data)?;
264
265        use std::io::Read as _;
266        let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
267        let mut raw = Vec::new();
268        decoder
269            .read_to_end(&mut raw)
270            .map_err(|e| format!("Decompress error: {}", e))?;
271
272        let obj = Object::from_bytes(&raw)?;
273        local_store.write(&obj)?;
274        count += 1;
275    }
276    Ok(count)
277}
278
279/// Upload objects from a local store to a remote server via lit:// connection
280pub fn upload_objects_lit(
281    conn: &mut LitConnection,
282    local_store: &ObjectStore,
283    hashes: &[ObjectHash],
284) -> Result<usize, String> {
285    let mut total = 0;
286    for chunk in hashes.chunks(50) {
287        let mut objects_json = Vec::new();
288        for hash in chunk {
289            let obj = local_store.read(hash)?;
290            let data = obj.to_bytes();
291            use std::io::Write as _;
292            let mut encoder =
293                flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
294            encoder
295                .write_all(&data)
296                .map_err(|e| format!("Compress error: {}", e))?;
297            let compressed = encoder
298                .finish()
299                .map_err(|e| format!("Compress error: {}", e))?;
300            let b64 = base64_encode(&compressed);
301            objects_json.push(serde_json::json!({"hash": hash.as_str(), "data": b64}));
302        }
303
304        let body = serde_json::json!({"objects": objects_json}).to_string();
305        let (status, resp) = conn.request("POST", "/api/v1/transport/objects", &body)?;
306        check_status(status, &resp)?;
307        total += resp.get("written").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
308    }
309    Ok(total)
310}
311
312// ── Base64 helpers ──
313
314fn base64_encode(data: &[u8]) -> String {
315    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
316    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
317    for chunk in data.chunks(3) {
318        let b0 = chunk[0] as u32;
319        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
320        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
321        let triple = (b0 << 16) | (b1 << 8) | b2;
322        out.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
323        out.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
324        if chunk.len() > 1 {
325            out.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
326        } else {
327            out.push('=');
328        }
329        if chunk.len() > 2 {
330            out.push(CHARS[(triple & 0x3F) as usize] as char);
331        } else {
332            out.push('=');
333        }
334    }
335    out
336}
337
338fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
339    fn val(c: u8) -> Result<u32, String> {
340        match c {
341            b'A'..=b'Z' => Ok((c - b'A') as u32),
342            b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
343            b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
344            b'+' => Ok(62),
345            b'/' => Ok(63),
346            b'=' => Ok(0),
347            _ => Err(format!("Invalid base64 character: {}", c as char)),
348        }
349    }
350    let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
351    let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
352    for chunk in bytes.chunks(4) {
353        if chunk.len() < 4 {
354            return Err("Invalid base64 length".to_string());
355        }
356        let a = val(chunk[0])?;
357        let b = val(chunk[1])?;
358        let c = val(chunk[2])?;
359        let d = val(chunk[3])?;
360        let triple = (a << 18) | (b << 12) | (c << 6) | d;
361        out.push(((triple >> 16) & 0xFF) as u8);
362        if chunk[2] != b'=' {
363            out.push(((triple >> 8) & 0xFF) as u8);
364        }
365        if chunk[3] != b'=' {
366            out.push((triple & 0xFF) as u8);
367        }
368    }
369    Ok(out)
370}