use crate::commands;
use crate::core::{find_repo_root, Object, ObjectHash};
use crate::response::{CommandResponse, ServeResponse};
use crate::storage::ObjectStore;
use std::collections::{HashMap, HashSet};
use std::io::{BufRead, Write};
use std::net::{IpAddr, TcpListener};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tiny_http::{Header, Method, Response, Server, StatusCode};
const MAX_BODY_SIZE: usize = 1_048_576;
const RATE_LIMIT_MAX_REQUESTS: u32 = 100;
const RATE_LIMIT_WINDOW_SECS: u64 = 60;
pub(crate) struct RateLimiter {
clients: HashMap<IpAddr, (Instant, u32)>,
}
impl RateLimiter {
pub(crate) fn new() -> Self {
RateLimiter {
clients: HashMap::new(),
}
}
pub(crate) fn check(&mut self, ip: IpAddr) -> bool {
let now = Instant::now();
let window = std::time::Duration::from_secs(RATE_LIMIT_WINDOW_SECS);
let entry = self.clients.entry(ip).or_insert((now, 0));
if now.duration_since(entry.0) >= window {
*entry = (now, 1);
true
} else if entry.1 < RATE_LIMIT_MAX_REQUESTS {
entry.1 += 1;
true
} else {
false
}
}
}
pub fn execute(port: u16, token: Option<String>) -> Result<ServeResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
execute_at(port, token, repo_root)
}
pub fn execute_at(
port: u16,
token: Option<String>,
repo_root: std::path::PathBuf,
) -> Result<ServeResponse, crate::errors::LitError> {
let bind_addr = format!("127.0.0.1:{}", port);
let server = Server::http(&bind_addr)
.map_err(|e| format!("Failed to start server on {}: {}", bind_addr, e))?;
eprintln!("Lit API server listening on http://{}", bind_addr);
eprintln!("Repository: {}", repo_root.display());
if token.is_some() {
eprintln!("Authentication: Bearer token required");
}
eprintln!("Press Ctrl+C to stop");
let mut rate_limiter = RateLimiter::new();
for mut request in server.incoming_requests() {
if let Some(ip) = request.remote_addr().map(|a| a.ip()) {
if !rate_limiter.check(ip) {
let body = r#"{"status":"error","error":{"message":"Rate limit exceeded"}}"#;
let resp = Response::from_string(body)
.with_status_code(StatusCode(429))
.with_header(json_content_type());
let _ = request.respond(resp);
continue;
}
}
if let Some(ref expected_token) = token {
let auth_header = request.headers().iter().find(|h| {
let name = h.field.as_str().to_string();
name.eq_ignore_ascii_case("authorization")
});
let authorized = match auth_header {
Some(h) => {
let val = h.value.as_str();
val.starts_with("Bearer ")
&& subtle::ConstantTimeEq::ct_eq(
&val.as_bytes()[7..],
expected_token.as_bytes(),
)
.into()
}
None => false,
};
if !authorized {
let body = r#"{"status":"error","error":{"message":"Unauthorized"}}"#;
let resp = Response::from_string(body)
.with_status_code(StatusCode(401))
.with_header(json_content_type());
let _ = request.respond(resp);
continue;
}
}
let method = request.method().clone();
let url = request.url().to_string();
let body_str = read_body(&mut request).unwrap_or_default();
let result = route_request(method, &url, &body_str, &repo_root);
match result {
Ok((status, body)) => {
let resp = Response::from_string(body)
.with_status_code(StatusCode(status))
.with_header(json_content_type());
let _ = request.respond(resp);
}
Err(e) => {
eprintln!("API error: {}", e.internal_message());
let body = serde_json::json!({
"status": "error",
"error": {"message": e.user_message()}
})
.to_string();
let resp = Response::from_string(body)
.with_status_code(StatusCode(500))
.with_header(json_content_type());
let _ = request.respond(resp);
}
}
}
Ok(ServeResponse {
message: "Server stopped".to_string(),
})
}
pub fn execute_stdio() -> Result<ServeResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let reader = stdin.lock();
let mut writer = stdout.lock();
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => break, };
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let req: serde_json::Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
let resp =
serde_json::json!({"status": 400, "body": format!("Invalid JSON: {}", e)});
let _ = writeln!(writer, "{}", resp);
let _ = writer.flush();
continue;
}
};
let method_str = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
let path = req.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let body = req.get("body").and_then(|v| v.as_str()).unwrap_or("");
let method = match method_str.to_uppercase().as_str() {
"GET" => Method::Get,
"POST" => Method::Post,
"PUT" => Method::Put,
"DELETE" => Method::Delete,
_ => Method::Get,
};
let (status, response_body) = match route_request(method, path, body, &repo_root) {
Ok((s, b)) => (s, b),
Err(e) => {
eprintln!("Stdio API error: {}", e.internal_message());
let err_body = serde_json::json!({
"status": "error",
"error": {"message": e.user_message()}
})
.to_string();
(500, err_body)
}
};
let resp = serde_json::json!({"status": status, "body": response_body});
let _ = writeln!(writer, "{}", resp);
let _ = writer.flush();
}
Ok(ServeResponse {
message: "Stdio server stopped".to_string(),
})
}
pub fn execute_daemon(port: u16) -> Result<ServeResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
let bind_addr = format!("127.0.0.1:{}", port);
let listener = TcpListener::bind(&bind_addr)
.map_err(|e| format!("Failed to bind lit:// daemon on {}: {}", bind_addr, e))?;
eprintln!("Lit daemon listening on lit://127.0.0.1:{}", port);
eprintln!("Repository: {}", repo_root.display());
eprintln!("Press Ctrl+C to stop");
let rate_limiter = Arc::new(Mutex::new(RateLimiter::new()));
for stream in listener.incoming() {
let stream = match stream {
Ok(s) => s,
Err(e) => {
eprintln!("Accept error: {}", e);
continue;
}
};
if let Ok(addr) = stream.peer_addr() {
if let Ok(mut rl) = rate_limiter.lock() {
if !rl.check(addr.ip()) {
continue;
}
}
}
let repo = repo_root.clone();
std::thread::spawn(move || {
handle_daemon_connection(stream, &repo);
});
}
Ok(ServeResponse {
message: "Daemon stopped".to_string(),
})
}
fn handle_daemon_connection(stream: std::net::TcpStream, repo_root: &std::path::Path) {
let reader_stream = match stream.try_clone() {
Ok(s) => s,
Err(_) => return,
};
let reader = std::io::BufReader::new(reader_stream);
let mut writer = std::io::BufWriter::new(stream);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let req: serde_json::Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
let resp =
serde_json::json!({"status": 400, "body": format!("Invalid JSON: {}", e)});
let _ = writeln!(writer, "{}", resp);
let _ = writer.flush();
continue;
}
};
let method_str = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
let path = req.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let body = req.get("body").and_then(|v| v.as_str()).unwrap_or("");
let method = match method_str.to_uppercase().as_str() {
"GET" => Method::Get,
"POST" => Method::Post,
"PUT" => Method::Put,
"DELETE" => Method::Delete,
_ => Method::Get,
};
let (status, response_body) = match route_request(method, path, body, repo_root) {
Ok((s, b)) => (s, b),
Err(e) => {
eprintln!("Daemon API error: {}", e.internal_message());
let err_body = serde_json::json!({
"status": "error",
"error": {"message": e.user_message()}
})
.to_string();
(500, err_body)
}
};
let resp = serde_json::json!({"status": status, "body": response_body});
if writeln!(writer, "{}", resp).is_err() {
break;
}
if writer.flush().is_err() {
break;
}
}
}
fn json_content_type() -> Header {
Header::from_bytes("Content-Type", "application/json").unwrap()
}
fn read_body(request: &mut tiny_http::Request) -> Result<String, crate::errors::LitError> {
let content_length = request.body_length().unwrap_or(0);
if content_length > MAX_BODY_SIZE {
return Err("Request body too large".into());
}
let mut body = String::new();
request
.as_reader()
.read_to_string(&mut body)
.map_err(|e| format!("Failed to read request body: {}", e))?;
Ok(body)
}
fn route_request(
method: Method,
url: &str,
body: &str,
repo_root: &std::path::Path,
) -> Result<(u16, String), crate::errors::LitError> {
let path = url.split('?').next().unwrap_or(url);
match (method, path) {
(Method::Get, "/api/v1") | (Method::Get, "/api/v1/") => {
let info = serde_json::json!({
"name": "lit",
"version": env!("CARGO_PKG_VERSION"),
"api_version": "v1",
"endpoints": [
"GET /api/v1/status",
"GET /api/v1/log?count=N",
"GET /api/v1/branches",
"GET /api/v1/diff?staged=bool",
"GET /api/v1/show/:ref",
"GET /api/v1/tags",
"GET /api/v1/remotes",
"GET /api/v1/config",
"GET /api/v1/search?q=query&messages=bool",
"GET /api/v1/verify",
"GET /api/v1/ontology",
"POST /api/v1/add",
"POST /api/v1/commit",
"POST /api/v1/snapshot",
"POST /api/v1/checkout",
"POST /api/v1/merge",
"POST /api/v1/branch",
]
});
Ok((200, serde_json::to_string_pretty(&info).unwrap()))
}
(Method::Get, "/api/v1/status") => {
let resp = commands::status::execute()?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/log") => {
let count = parse_query_param(url, "count")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(10);
let oneline = parse_query_param(url, "oneline")
.map(|s| s == "true")
.unwrap_or(false);
let resp = commands::log::execute(count, oneline)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/branches") => {
let resp = commands::branch::execute(None, false, true)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/diff") => {
let staged = parse_query_param(url, "staged")
.map(|s| s == "true")
.unwrap_or(false);
let stat = parse_query_param(url, "stat")
.map(|s| s == "true")
.unwrap_or(false);
let resp = commands::diff::execute(staged, stat, false, None, None)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, p) if p.starts_with("/api/v1/show/") => {
let object = &p["/api/v1/show/".len()..];
if object.is_empty() {
return Ok((400, r#"{"status":"error","error":{"message":"Missing object ref"}}"#.to_string()));
}
if !is_valid_ref(object) {
return Ok((400, r#"{"status":"error","error":{"message":"Invalid object ref"}}"#.to_string()));
}
let resp = commands::show::execute(object.to_string())?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/tags") => {
let resp = commands::tag::execute(None, None, false, false, false, false, true, None)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/remotes") => {
let resp = commands::remote::execute(Some(crate::RemoteCommands::List { verbose: true }))?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/config") => {
let resp = commands::config::execute(Some(crate::ConfigCommands::Show))?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/search") => {
let query = parse_query_param(url, "q").unwrap_or_default();
if query.is_empty() {
return Ok((400, r#"{"status":"error","error":{"message":"Missing query parameter 'q'"}}"#.to_string()));
}
let messages = parse_query_param(url, "messages")
.map(|s| s == "true")
.unwrap_or(false);
let metadata = parse_query_param(url, "metadata");
let max = parse_query_param(url, "max")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(100);
let resp = commands::search::execute(query, messages, metadata, max)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/verify") => {
let resp = commands::verify::execute()?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/ontology") => {
let resp = crate::ontology::get_ontology();
Ok((200, serde_json::to_string_pretty(&resp).unwrap()))
}
(Method::Post, "/api/v1/add") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let files: Vec<String> = payload
.get("files")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
if files.is_empty() {
return Ok((400, r#"{"status":"error","error":{"message":"Missing 'files' array"}}"#.to_string()));
}
let resp = commands::add::execute(files)?;
Ok((200, resp.to_json_output()))
}
(Method::Post, "/api/v1/commit") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let message = payload
.get("message")
.and_then(|v| v.as_str())
.ok_or("Missing 'message' field")?
.to_string();
let author = payload
.get("author")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let resp = commands::commit::execute(message, author)?;
Ok((200, resp.to_json_output()))
}
(Method::Post, "/api/v1/snapshot") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let message = payload
.get("message")
.and_then(|v| v.as_str())
.ok_or("Missing 'message' field")?
.to_string();
let author = payload
.get("author")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let metadata = payload.get("metadata").cloned();
let resp = commands::snapshot::execute(message, author, metadata)?;
Ok((200, resp.to_json_output()))
}
(Method::Post, "/api/v1/checkout") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let target = payload
.get("target")
.and_then(|v| v.as_str())
.ok_or("Missing 'target' field")?
.to_string();
let create = payload
.get("create")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let resp = commands::checkout::execute(target, create)?;
Ok((200, resp.to_json_output()))
}
(Method::Post, "/api/v1/merge") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let branch = payload
.get("branch")
.and_then(|v| v.as_str())
.ok_or("Missing 'branch' field")?
.to_string();
let strategy = payload
.get("strategy")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let resp = commands::merge::execute(branch, strategy)?;
Ok((200, resp.to_json_output()))
}
(Method::Post, "/api/v1/branch") => {
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let name = payload
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let delete = payload
.get("delete")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let resp = commands::branch::execute(name, delete, false)?;
Ok((200, resp.to_json_output()))
}
(Method::Get, "/api/v1/transport/refs") => {
let kind = parse_query_param(url, "kind").unwrap_or_else(|| "all".to_string());
let mut refs = Vec::new();
if kind == "all" || kind == "heads" {
if let Ok(head_refs) = crate::core::refs::list_refs(repo_root, "heads") {
for r in head_refs {
refs.push(serde_json::json!({"kind": "heads", "name": r.name, "hash": r.hash}));
}
}
}
if kind == "all" || kind == "tags" {
if let Ok(tag_refs) = crate::core::refs::list_refs(repo_root, "tags") {
for r in tag_refs {
refs.push(serde_json::json!({"kind": "tags", "name": r.name, "hash": r.hash}));
}
}
}
Ok((200, serde_json::json!({"refs": refs}).to_string()))
}
(Method::Get, "/api/v1/transport/head") => {
let head = std::fs::read_to_string(repo_root.join(".lit").join("HEAD"))
.map_err(|e| format!("Failed to read HEAD: {}", e))?;
Ok((200, serde_json::json!({"head": head.trim()}).to_string()))
}
(Method::Get, p) if p.starts_with("/api/v1/transport/refs/heads/") => {
let branch = &p["/api/v1/transport/refs/heads/".len()..];
if !is_valid_ref(branch) {
return Ok((400, r#"{"status":"error","error":{"message":"Invalid branch name"}}"#.to_string()));
}
let hash = crate::core::refs::read_ref(repo_root, &format!("heads/{}", branch))?;
Ok((200, serde_json::json!({"branch": branch, "hash": hash}).to_string()))
}
(Method::Get, p) if p.starts_with("/api/v1/transport/objects/") && p.ends_with("/exists") => {
let hash_str = &p["/api/v1/transport/objects/".len()..p.len() - "/exists".len()];
if !is_valid_hex_hash(hash_str) {
return Ok((400, r#"{"status":"error","error":{"message":"Invalid object hash"}}"#.to_string()));
}
let store = ObjectStore::new(repo_root);
let exists = store.exists(&ObjectHash::from_hex(hash_str.to_string()));
Ok((200, serde_json::json!({"hash": hash_str, "exists": exists}).to_string()))
}
(Method::Get, p) if p.starts_with("/api/v1/transport/objects/") => {
let hash_str = &p["/api/v1/transport/objects/".len()..];
if !is_valid_hex_hash(hash_str) {
return Ok((400, r#"{"status":"error","error":{"message":"Invalid object hash"}}"#.to_string()));
}
let store = ObjectStore::new(repo_root);
let hash = ObjectHash::from_hex(hash_str.to_string());
let obj = store.read(&hash)?;
let data = obj.to_bytes();
use std::io::Write as _;
let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
encoder.write_all(&data).map_err(|e| format!("Compress error: {}", e))?;
let compressed = encoder.finish().map_err(|e| format!("Compress error: {}", e))?;
let b64 = base64_encode(&compressed);
Ok((200, serde_json::json!({"hash": hash_str, "data": b64}).to_string()))
}
(Method::Post, "/api/v1/transport/objects") => {
let store = ObjectStore::new(repo_root);
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let objects = payload.get("objects")
.and_then(|v| v.as_array())
.ok_or("Missing 'objects' array")?;
let mut written = 0;
for entry in objects {
let b64_data = entry.get("data")
.and_then(|v| v.as_str())
.ok_or("Missing 'data' field in object entry")?;
let compressed = base64_decode(b64_data)?;
use std::io::Read as _;
let mut decoder = flate2::read::ZlibDecoder::new(&compressed[..]);
let mut raw = Vec::new();
decoder.read_to_end(&mut raw)
.map_err(|e| format!("Decompress error: {}", e))?;
let obj = Object::from_bytes(&raw)?;
store.write(&obj)?;
written += 1;
}
Ok((200, serde_json::json!({"written": written}).to_string()))
}
(Method::Put, p) if p.starts_with("/api/v1/transport/refs/heads/") => {
let branch = &p["/api/v1/transport/refs/heads/".len()..];
if !is_valid_ref(branch) {
return Ok((400, r#"{"status":"error","error":{"message":"Invalid branch name"}}"#.to_string()));
}
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let hash = payload.get("hash")
.and_then(|v| v.as_str())
.ok_or("Missing 'hash' field")?;
let force = payload.get("force")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !force {
if let Ok(current) = crate::core::refs::read_ref(repo_root, &format!("heads/{}", branch)) {
let store = ObjectStore::new(repo_root);
let old_hash = ObjectHash::from_hex(current);
let new_hash = ObjectHash::from_hex(hash.to_string());
let is_ff = crate::core::merge::is_ancestor(&store, &old_hash, &new_hash)?;
if !is_ff {
return Ok((409, serde_json::json!({
"status": "error",
"error": {"message": "Non-fast-forward update rejected. Use force=true."}
}).to_string()));
}
}
}
crate::core::refs::write_ref(repo_root, &format!("heads/{}", branch), hash)?;
Ok((200, serde_json::json!({"branch": branch, "hash": hash, "updated": true}).to_string()))
}
(Method::Post, "/api/v1/transport/negotiate") => {
let store = ObjectStore::new(repo_root);
let payload: serde_json::Value = serde_json::from_str(body)
.map_err(|e| format!("Invalid JSON: {}", e))?;
let wants: Vec<String> = payload.get("wants")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
let haves: Vec<String> = payload.get("haves")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
let known: HashSet<String> = haves.into_iter().collect();
let mut all_needed = Vec::new();
for want in &wants {
let hash = ObjectHash::from_hex(want.clone());
let needed = crate::network::transport::walk_commit_graph(&store, &hash, &known)?;
for h in needed {
let s = h.as_str().to_string();
if !all_needed.contains(&s) {
all_needed.push(s);
}
}
}
Ok((200, serde_json::json!({"needed": all_needed}).to_string()))
}
_ => Ok((
404,
r#"{"status":"error","error":{"message":"Not found. GET /api/v1 for available endpoints."}}"#
.to_string(),
)),
}
}
fn url_decode(s: &str) -> String {
let mut result = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let Ok(byte) =
u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16)
{
result.push(byte);
i += 3;
continue;
}
}
if bytes[i] == b'+' {
result.push(b' ');
} else {
result.push(bytes[i]);
}
i += 1;
}
String::from_utf8(result).unwrap_or_default()
}
fn parse_query_param(url: &str, key: &str) -> Option<String> {
let query = url.split('?').nth(1)?;
for pair in query.split('&') {
let mut parts = pair.splitn(2, '=');
if let (Some(k), Some(v)) = (parts.next(), parts.next()) {
if k == key {
return Some(url_decode(v));
}
}
}
None
}
fn is_valid_ref(name: &str) -> bool {
if name.is_empty() || name.len() > 256 {
return false;
}
if name.contains("..") || name.contains("//") || name.starts_with('/') || name.ends_with('/') {
return false;
}
name.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/')
}
fn is_valid_hex_hash(s: &str) -> bool {
!s.is_empty() && s.len() <= 192 && s.chars().all(|c| c.is_ascii_hexdigit())
}
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
out.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
fn base64_decode(input: &str) -> Result<Vec<u8>, crate::errors::LitError> {
fn val(c: u8) -> Result<u32, crate::errors::LitError> {
match c {
b'A'..=b'Z' => Ok((c - b'A') as u32),
b'a'..=b'z' => Ok((c - b'a' + 26) as u32),
b'0'..=b'9' => Ok((c - b'0' + 52) as u32),
b'+' => Ok(62),
b'/' => Ok(63),
b'=' => Ok(0),
_ => Err(format!("Invalid base64 character: {}", c as char).into()),
}
}
let bytes: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
for chunk in bytes.chunks(4) {
if chunk.len() < 4 {
return Err("Invalid base64 length".into());
}
let a = val(chunk[0])?;
let b = val(chunk[1])?;
let c = val(chunk[2])?;
let d = val(chunk[3])?;
let triple = (a << 18) | (b << 12) | (c << 6) | d;
out.push(((triple >> 16) & 0xFF) as u8);
if chunk[2] != b'=' {
out.push(((triple >> 8) & 0xFF) as u8);
}
if chunk[3] != b'=' {
out.push((triple & 0xFF) as u8);
}
}
Ok(out)
}