1use 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
15pub const DEFAULT_PORT: u16 = 9418;
17
18pub fn is_lit_url(url: &str) -> bool {
20 url.starts_with("lit://")
21}
22
23#[derive(Debug, Clone)]
25pub struct LitUrl {
26 pub host: String,
27 pub port: u16,
28 pub path: String,
29}
30
31pub 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
61pub struct LitConnection {
63 reader: BufReader<TcpStream>,
64 writer: BufWriter<TcpStream>,
65}
66
67impl LitConnection {
68 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 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 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
141fn 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
156pub 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
187pub 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
198pub 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
208pub 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
221pub 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
243pub 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
279pub 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
312fn 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}