app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.3.3 source + crate build: BOM exported from the SOLIDWORKS assembly, build log §1–11, release script
477e64f
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
//! A minimal blocking HTTP/1.1 client — loopback only, no dependencies.
//!
//! Used for exactly one thing in production: calling ab back on its direct API
//! (`swx_screenshot` delegates to ab's generic `desktop_screenshot_window`).
//! That is one POST to 127.0.0.1 with a couple of headers, which does not
//! justify pulling an HTTP client crate into a bridge whose whole selling point
//! is being one small static binary.
//!
//! The integration tests reuse it to drive the real server exactly as ab does.
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
#[derive(Debug)]
pub struct Response {
pub status: u16,
pub body: String,
}
impl Response {
/// The body parsed as JSON (an error string if it is not JSON).
pub fn json(&self) -> Result<serde_json::Value, String> {
serde_json::from_str(&self.body).map_err(|e| format!("not JSON ({e}): {}", self.body))
}
}
pub fn get(url: &str, timeout: Duration) -> Result<Response, String> {
request("GET", url, &[], None, timeout)
}
pub fn post_json(
url: &str,
headers: &[(String, String)],
body: &str,
timeout: Duration,
) -> Result<Response, String> {
request("POST", url, headers, Some(body), timeout)
}
fn request(
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<&str>,
timeout: Duration,
) -> Result<Response, String> {
let (host_port, path) = split_url(url)?;
let stream = TcpStream::connect(&host_port).map_err(|e| format!("connect {host_port}: {e}"))?;
stream.set_read_timeout(Some(timeout)).ok();
stream.set_write_timeout(Some(timeout)).ok();
let mut stream = stream;
let mut req = format!("{method} {path} HTTP/1.1\r\nHost: {host_port}\r\nConnection: close\r\n");
for (k, v) in headers {
req.push_str(&format!("{k}: {v}\r\n"));
}
if let Some(b) = body {
req.push_str("Content-Type: application/json\r\n");
req.push_str(&format!("Content-Length: {}\r\n", b.len()));
}
req.push_str("\r\n");
if let Some(b) = body {
req.push_str(b);
}
stream.write_all(req.as_bytes()).map_err(|e| format!("write: {e}"))?;
stream.flush().ok();
let mut raw = Vec::new();
stream.read_to_end(&mut raw).map_err(|e| format!("read: {e}"))?;
parse_response(&raw)
}
fn split_url(url: &str) -> Result<(String, String), String> {
let rest = url
.strip_prefix("http://")
.ok_or_else(|| format!("only plain http:// loopback URLs are supported: {url}"))?;
let (host_port, path) = match rest.find('/') {
Some(i) => (&rest[..i], &rest[i..]),
None => (rest, "/"),
};
let host_port =
if host_port.contains(':') { host_port.to_string() } else { format!("{host_port}:80") };
Ok((host_port, path.to_string()))
}
fn parse_response(raw: &[u8]) -> Result<Response, String> {
let split = raw
.windows(4)
.position(|w| w == b"\r\n\r\n")
.ok_or_else(|| "malformed response: no header terminator".to_string())?;
let head = String::from_utf8_lossy(&raw[..split]).to_string();
let body_bytes = &raw[split + 4..];
let mut lines = head.lines();
let status_line = lines.next().unwrap_or_default();
let status: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.ok_or_else(|| format!("malformed status line: {status_line}"))?;
let chunked = lines.any(|l| {
let l = l.to_ascii_lowercase();
l.starts_with("transfer-encoding:") && l.contains("chunked")
});
let body = if chunked { dechunk(body_bytes) } else { body_bytes.to_vec() };
Ok(Response { status, body: String::from_utf8_lossy(&body).to_string() })
}
fn dechunk(mut input: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
loop {
let nl = match input.windows(2).position(|w| w == b"\r\n") {
Some(i) => i,
None => break,
};
let size_line = String::from_utf8_lossy(&input[..nl]).to_string();
let size = usize::from_str_radix(size_line.trim().split(';').next().unwrap_or("0"), 16)
.unwrap_or(0);
input = &input[nl + 2..];
if size == 0 || input.len() < size {
out.extend_from_slice(&input[..input.len().min(size)]);
break;
}
out.extend_from_slice(&input[..size]);
input = &input[size..];
if input.starts_with(b"\r\n") {
input = &input[2..];
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_split_into_authority_and_path() {
assert_eq!(
split_url("http://127.0.0.1:9000/command").unwrap(),
("127.0.0.1:9000".into(), "/command".into())
);
assert_eq!(split_url("http://localhost").unwrap(), ("localhost:80".into(), "/".into()));
assert!(split_url("https://example.com").is_err());
}
#[test]
fn parses_content_length_and_chunked_bodies() {
let r = parse_response(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi").unwrap();
assert_eq!((r.status, r.body.as_str()), (200, "hi"));
let r = parse_response(
b"HTTP/1.1 404 Not Found\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n",
)
.unwrap();
assert_eq!((r.status, r.body.as_str()), (404, "abc"));
}
}