1use crate::{default_socket_path, JsonRpcVersion, Request, RpcError};
32use std::path::{Path, PathBuf};
33use std::time::Duration;
34
35#[cfg(unix)]
36use std::io::{BufRead, BufReader, Read as _, Write};
37#[cfg(unix)]
38use std::os::unix::net::UnixStream;
39
40pub const READ_TIMEOUT: Duration = Duration::from_secs(5);
44
45pub const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
48
49pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
55
56const REQUEST_ID: u64 = 1;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum UnavailableKind {
66 NotFound,
68 ConnectionRefused,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
76pub enum EnvelopeError {
77 #[error("missing or unsupported `jsonrpc` version (expected \"2.0\")")]
79 WrongVersion,
80 #[error("missing or non-integer `id`")]
82 MissingId,
83 #[error("both `result` and `error` present")]
85 BothResultAndError,
86 #[error("neither `result` nor `error` present")]
88 NeitherResultNorError,
89 #[error("`error` member is not a valid JSON-RPC error object")]
92 InvalidErrorObject,
93}
94
95#[derive(Debug, thiserror::Error)]
109pub enum ClientError {
110 #[error("no Scrybe running (socket {}: {kind:?})", path.display())]
113 SocketUnavailable {
114 path: PathBuf,
116 kind: UnavailableKind,
118 },
119 #[error("permission denied connecting to Scrybe socket {}", path.display())]
121 PermissionDenied {
122 path: PathBuf,
124 },
125 #[error("timed out connecting to the Scrybe socket")]
128 ConnectTimeout,
129 #[error("timed out writing request to the Scrybe socket")]
131 WriteTimeout,
132 #[error("timed out waiting for a reply from the Scrybe app")]
134 ReadTimeout,
135 #[error("socket I/O error: {0}")]
138 Io(#[source] std::io::Error),
139 #[error("reply frame too large: read {bytes} bytes, limit {limit}")]
142 FrameTooLarge {
143 bytes: usize,
145 limit: usize,
147 },
148 #[error("reply is not valid UTF-8")]
150 InvalidUtf8,
151 #[error("reply is not valid JSON: {0}")]
153 InvalidJson(#[source] serde_json::Error),
154 #[error("reply violates the JSON-RPC envelope: {0}")]
156 InvalidEnvelope(#[from] EnvelopeError),
157 #[error("reply id {actual} does not match request id {expected}")]
160 MismatchedResponseId {
161 expected: u64,
163 actual: u64,
165 },
166 #[error("Scrybe app error {}: {}", .0.code, .0.message)]
170 Remote(RpcError),
171}
172
173impl ClientError {
174 pub fn is_not_running(&self) -> bool {
178 matches!(self, Self::SocketUnavailable { .. })
179 }
180}
181
182pub fn socket_path() -> PathBuf {
186 default_socket_path()
187}
188
189pub fn is_live() -> bool {
192 try_connect().is_ok()
193}
194
195#[cfg(unix)]
198pub fn try_connect() -> Result<UnixStream, ClientError> {
199 try_connect_at(&default_socket_path())
200}
201
202#[cfg(unix)]
205pub fn try_connect_at(path: &Path) -> Result<UnixStream, ClientError> {
206 if !path.exists() {
207 return Err(ClientError::SocketUnavailable {
208 path: path.to_path_buf(),
209 kind: UnavailableKind::NotFound,
210 });
211 }
212 match UnixStream::connect(path) {
213 Ok(s) => {
214 s.set_read_timeout(Some(READ_TIMEOUT))
215 .map_err(ClientError::Io)?;
216 s.set_write_timeout(Some(WRITE_TIMEOUT))
217 .map_err(ClientError::Io)?;
218 Ok(s)
219 }
220 Err(e) => Err(match e.kind() {
221 std::io::ErrorKind::NotFound => ClientError::SocketUnavailable {
222 path: path.to_path_buf(),
223 kind: UnavailableKind::NotFound,
224 },
225 std::io::ErrorKind::ConnectionRefused => ClientError::SocketUnavailable {
226 path: path.to_path_buf(),
227 kind: UnavailableKind::ConnectionRefused,
228 },
229 std::io::ErrorKind::PermissionDenied => ClientError::PermissionDenied {
230 path: path.to_path_buf(),
231 },
232 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => {
233 ClientError::ConnectTimeout
234 }
235 _ => ClientError::Io(e),
236 }),
237 }
238}
239
240#[cfg(not(unix))]
241pub fn try_connect() -> Result<(), ClientError> {
242 try_connect_at(Path::new("(unsupported)"))
243}
244
245#[cfg(not(unix))]
246pub fn try_connect_at(path: &Path) -> Result<(), ClientError> {
247 Err(unsupported(path))
248}
249
250#[cfg(not(unix))]
257fn unsupported(path: &Path) -> ClientError {
258 ClientError::SocketUnavailable {
259 path: path.to_path_buf(),
260 kind: UnavailableKind::NotFound,
261 }
262}
263
264pub fn send(method: &str, params: serde_json::Value) -> Result<serde_json::Value, ClientError> {
270 send_to(&default_socket_path(), method, params)
271}
272
273#[cfg(unix)]
275pub fn send_to(
276 socket: &Path,
277 method: &str,
278 params: serde_json::Value,
279) -> Result<serde_json::Value, ClientError> {
280 let mut stream = try_connect_at(socket)?;
281 let req = Request {
282 jsonrpc: JsonRpcVersion,
283 id: REQUEST_ID,
284 method: method.to_string(),
285 params,
286 };
287 let line = serde_json::to_string(&req)
288 .map_err(|e| ClientError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
289 writeln!(stream, "{line}").map_err(write_error)?;
290 let raw = read_frame(stream)?;
291 validate_envelope(&raw, REQUEST_ID)
292}
293
294#[cfg(not(unix))]
295pub fn send_to(
296 socket: &Path,
297 _method: &str,
298 _params: serde_json::Value,
299) -> Result<serde_json::Value, ClientError> {
300 Err(unsupported(socket))
301}
302
303#[cfg(unix)]
306fn read_frame(stream: UnixStream) -> Result<Vec<u8>, ClientError> {
307 let mut reader = BufReader::new(stream).take(MAX_FRAME_BYTES as u64 + 1);
310 let mut buf = Vec::new();
311 reader.read_until(b'\n', &mut buf).map_err(read_error)?;
312 if buf.len() > MAX_FRAME_BYTES {
313 return Err(ClientError::FrameTooLarge {
314 bytes: buf.len(),
315 limit: MAX_FRAME_BYTES,
316 });
317 }
318 if buf.is_empty() {
319 return Err(ClientError::Io(std::io::Error::new(
320 std::io::ErrorKind::UnexpectedEof,
321 "server closed connection without responding",
322 )));
323 }
324 if buf.last() != Some(&b'\n') {
325 return Err(ClientError::Io(std::io::Error::new(
326 std::io::ErrorKind::UnexpectedEof,
327 "connection closed mid-frame",
328 )));
329 }
330 Ok(buf)
331}
332
333fn write_error(e: std::io::Error) -> ClientError {
335 match e.kind() {
336 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => ClientError::WriteTimeout,
337 _ => ClientError::Io(e),
338 }
339}
340
341fn read_error(e: std::io::Error) -> ClientError {
343 match e.kind() {
344 std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => ClientError::ReadTimeout,
345 _ => ClientError::Io(e),
346 }
347}
348
349fn validate_envelope(frame: &[u8], expected_id: u64) -> Result<serde_json::Value, ClientError> {
356 let text = std::str::from_utf8(frame).map_err(|_| ClientError::InvalidUtf8)?;
357 let raw: serde_json::Value =
358 serde_json::from_str(text.trim_end()).map_err(ClientError::InvalidJson)?;
359 match raw.get("jsonrpc").and_then(serde_json::Value::as_str) {
360 Some("2.0") => {}
361 _ => return Err(EnvelopeError::WrongVersion.into()),
362 }
363 let actual = raw
364 .get("id")
365 .and_then(serde_json::Value::as_u64)
366 .ok_or(EnvelopeError::MissingId)?;
367 if actual != expected_id {
368 return Err(ClientError::MismatchedResponseId {
369 expected: expected_id,
370 actual,
371 });
372 }
373 match (raw.get("result"), raw.get("error")) {
374 (Some(_), Some(_)) => Err(EnvelopeError::BothResultAndError.into()),
375 (None, None) => Err(EnvelopeError::NeitherResultNorError.into()),
376 (Some(result), None) => Ok(result.clone()),
377 (None, Some(error)) => {
378 let rpc: RpcError = serde_json::from_value(error.clone())
379 .map_err(|_| EnvelopeError::InvalidErrorObject)?;
380 Err(ClientError::Remote(rpc))
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::ERR_METHOD_NOT_FOUND;
389
390 #[test]
391 fn socket_path_exposed() {
392 let p = socket_path();
393 assert!(!p.as_os_str().is_empty());
394 }
395
396 #[test]
397 fn request_serializes_to_single_line() {
398 let req = Request {
399 jsonrpc: JsonRpcVersion,
400 id: 1,
401 method: "open".into(),
402 params: serde_json::json!({"path": "/tmp/foo.md"}),
403 };
404 let s = serde_json::to_string(&req).unwrap();
405 assert!(!s.contains('\n'));
406 }
407
408 #[test]
409 fn valid_result_envelope_unwraps() {
410 let frame = br#"{"jsonrpc":"2.0","id":1,"result":{"applied":true}}
411"#;
412 let v = validate_envelope(frame, 1).unwrap();
413 assert_eq!(v["applied"], true);
414 }
415
416 #[test]
417 fn remote_error_envelope_is_typed() {
418 let frame = format!(
419 "{{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{{\"code\":{ERR_METHOD_NOT_FOUND},\"message\":\"x\"}}}}\n",
420 );
421 let err = validate_envelope(frame.as_bytes(), 1).unwrap_err();
422 match err {
423 ClientError::Remote(e) => assert_eq!(e.code, ERR_METHOD_NOT_FOUND),
424 other => panic!("expected Remote, got {other:?}"),
425 }
426 }
427
428 #[test]
429 fn wrong_version_is_envelope_error() {
430 let frame = br#"{"jsonrpc":"1.0","id":1,"result":{}}"#;
431 let err = validate_envelope(frame, 1).unwrap_err();
432 assert!(matches!(
433 err,
434 ClientError::InvalidEnvelope(EnvelopeError::WrongVersion)
435 ));
436 }
437
438 #[test]
439 fn missing_version_is_envelope_error() {
440 let frame = br#"{"id":1,"result":{}}"#;
441 let err = validate_envelope(frame, 1).unwrap_err();
442 assert!(matches!(
443 err,
444 ClientError::InvalidEnvelope(EnvelopeError::WrongVersion)
445 ));
446 }
447
448 #[test]
449 fn missing_id_is_envelope_error() {
450 let frame = br#"{"jsonrpc":"2.0","result":{}}"#;
451 let err = validate_envelope(frame, 1).unwrap_err();
452 assert!(matches!(
453 err,
454 ClientError::InvalidEnvelope(EnvelopeError::MissingId)
455 ));
456 }
457
458 #[test]
459 fn mismatched_id_carries_both_ids() {
460 let frame = br#"{"jsonrpc":"2.0","id":7,"result":{}}"#;
461 let err = validate_envelope(frame, 1).unwrap_err();
462 match err {
463 ClientError::MismatchedResponseId { expected, actual } => {
464 assert_eq!(expected, 1);
465 assert_eq!(actual, 7);
466 }
467 other => panic!("expected MismatchedResponseId, got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn both_result_and_error_is_envelope_error() {
473 let frame =
474 br#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":-32000,"message":"x"}}"#;
475 let err = validate_envelope(frame, 1).unwrap_err();
476 assert!(matches!(
477 err,
478 ClientError::InvalidEnvelope(EnvelopeError::BothResultAndError)
479 ));
480 }
481
482 #[test]
483 fn neither_result_nor_error_is_envelope_error() {
484 let frame = br#"{"jsonrpc":"2.0","id":1}"#;
485 let err = validate_envelope(frame, 1).unwrap_err();
486 assert!(matches!(
487 err,
488 ClientError::InvalidEnvelope(EnvelopeError::NeitherResultNorError)
489 ));
490 }
491
492 #[test]
493 fn malformed_error_object_is_envelope_error() {
494 let frame = br#"{"jsonrpc":"2.0","id":1,"error":"boom"}"#;
495 let err = validate_envelope(frame, 1).unwrap_err();
496 assert!(matches!(
497 err,
498 ClientError::InvalidEnvelope(EnvelopeError::InvalidErrorObject)
499 ));
500 }
501
502 #[test]
503 fn invalid_utf8_is_typed() {
504 let err = validate_envelope(&[0xff, 0xfe, b'\n'], 1).unwrap_err();
505 assert!(matches!(err, ClientError::InvalidUtf8));
506 }
507
508 #[test]
509 fn invalid_json_is_typed() {
510 let err = validate_envelope(b"{not json\n", 1).unwrap_err();
511 assert!(matches!(err, ClientError::InvalidJson(_)));
512 }
513
514 #[cfg(unix)]
515 #[test]
516 fn try_connect_at_types_missing_socket_as_not_running() {
517 let path = std::path::PathBuf::from("/tmp/scrybe-nonexistent-sock-rpc-unit-test");
518 let err = try_connect_at(&path).unwrap_err();
519 assert!(err.is_not_running());
520 assert!(matches!(
521 err,
522 ClientError::SocketUnavailable {
523 kind: UnavailableKind::NotFound,
524 ..
525 }
526 ));
527 }
528
529 #[cfg(unix)]
530 #[test]
531 fn send_to_types_missing_socket_as_not_running() {
532 let path = std::path::PathBuf::from("/tmp/scrybe-nonexistent-sock-rpc-send-test");
533 let err = send_to(&path, "open", serde_json::json!({"path": "/tmp/foo.md"})).unwrap_err();
534 assert!(err.is_not_running(), "actual: {err:?}");
535 }
536
537 #[cfg(unix)]
538 #[test]
539 fn is_live_false_without_server() {
540 let _ = is_live();
543 }
544}