#![cfg(target_os = "windows")]
use std::fs::{File, OpenOptions};
use std::io;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
const PIPE_PREFIX: &str = r"\\.\pipe\";
fn validate_named_pipe_path(path: &Path) -> io::Result<()> {
let raw = path.to_string_lossy();
if !raw.starts_with(PIPE_PREFIX) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"named pipe path must start with \\\\.\\pipe\\",
));
}
if raw.len() <= PIPE_PREFIX.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"named pipe path must include a pipe name",
));
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct NamedPipeClientOptions {
read: bool,
write: bool,
}
impl Default for NamedPipeClientOptions {
fn default() -> Self {
Self {
read: true,
write: true,
}
}
}
impl NamedPipeClientOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn read(mut self, enabled: bool) -> Self {
self.read = enabled;
self
}
#[must_use]
pub fn write(mut self, enabled: bool) -> Self {
self.write = enabled;
self
}
pub fn open(self, path: impl AsRef<Path>) -> io::Result<NamedPipeClient> {
if !self.read && !self.write {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"named pipe client requires read or write access",
));
}
let path = path.as_ref();
validate_named_pipe_path(path)?;
let mut options = OpenOptions::new();
options.read(self.read).write(self.write);
let file = options.open(path)?;
Ok(NamedPipeClient {
inner: file,
path: path.to_path_buf(),
})
}
}
#[derive(Debug)]
pub struct NamedPipeClient {
inner: File,
path: PathBuf,
}
impl NamedPipeClient {
pub fn connect(path: impl AsRef<Path>) -> io::Result<Self> {
NamedPipeClientOptions::new().open(path)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn try_clone(&self) -> io::Result<Self> {
Ok(Self {
inner: self.inner.try_clone()?,
path: self.path.clone(),
})
}
#[must_use]
pub fn into_inner(self) -> File {
self.inner
}
}
impl Read for NamedPipeClient {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl Write for NamedPipeClient {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_named_pipe_path_rejects_non_pipe_namespace() {
let err = validate_named_pipe_path(Path::new(r"C:\tmp\not-a-pipe")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn validate_named_pipe_path_rejects_empty_pipe_name() {
let err = validate_named_pipe_path(Path::new(r"\\.\pipe\")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn validate_named_pipe_path_accepts_valid_prefix_and_name() {
let valid = validate_named_pipe_path(Path::new(r"\\.\pipe\asupersync-test"));
assert!(valid.is_ok());
}
#[test]
fn options_reject_read_and_write_both_disabled() {
let err = NamedPipeClientOptions::new()
.read(false)
.write(false)
.open(Path::new(r"\\.\pipe\asupersync-test"))
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
}