use std::{collections::HashSet, io::Write, str::FromStr};
use gix_error::ExnMessageResult;
use bstr::{BStr, BString, ByteVec};
use gix_packetline::blocking_io::{StreamingPeekableIter, Writer, encode};
use crate::driver::{
process,
process::{Capabilities, Client, PacketlineReader},
};
impl Client {
pub fn handshake(
mut process: std::process::Child,
welcome_prefix: &str,
versions: &[usize],
desired_capabilities: &[&str],
) -> ExnMessageResult<Self> {
use gix_error::{ErrorExt, ResultExt, message};
let mut out = Writer::new(process.stdin.take().expect("configured stdin when spawning"));
out.write_all(format!("{welcome_prefix}-client").as_bytes())
.or_raise(|| message("Failed to read or write to the process"))?;
for version in versions {
out.write_all(format!("version={version}").as_bytes())
.or_raise(|| message("Failed to read or write to the process"))?;
}
encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the process"))?;
out.flush()
.or_raise(|| message("Failed to read or write to the process"))?;
let mut input = StreamingPeekableIter::new(
process.stdout.take().expect("configured stdout when spawning"),
&[gix_packetline::PacketLineRef::Flush],
false,
);
let mut read = input.as_read();
let mut buf = String::new();
read.read_line_to_string(&mut buf)
.or_raise(|| message("Failed to read or write to the process"))?;
if buf
.strip_prefix(welcome_prefix)
.is_none_or(|rest| rest.trim_end() != "-server")
{
return Err(message!("Wanted '{welcome_prefix}-server, got '{buf}'").raise());
}
buf.clear();
read.read_line_to_string(&mut buf)
.or_raise(|| message("Failed to read or write to the process"))?;
let chosen_version = match buf
.strip_prefix("version=")
.and_then(|version| usize::from_str(version.trim_end()).ok())
{
Some(version) => version,
None => {
return Err(message!("Needed 'version=<integer>', got '{buf}'").raise());
}
};
if !versions.contains(&chosen_version) {
return Err(message!(
"Server offered {chosen_version}, we only support '{}'",
versions.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
)
.raise());
}
if read
.read_line_to_string(&mut buf)
.or_raise(|| message("Failed to read or write to the process"))?
!= 0
{
return Err(message!("expected flush packet, got '{buf}'").raise());
}
for capability in desired_capabilities {
out.write_all(format!("capability={capability}").as_bytes())
.or_raise(|| message("Failed to read or write to the process"))?;
}
encode::flush_to_write(out.inner_mut()).or_raise(|| message("Failed to read or write to the process"))?;
out.flush()
.or_raise(|| message("Failed to read or write to the process"))?;
read.reset_with(&[gix_packetline::PacketLineRef::Flush]);
let mut capabilities = HashSet::new();
loop {
buf.clear();
let num_read = read
.read_line_to_string(&mut buf)
.or_raise(|| message("Failed to read or write to the process"))?;
if num_read == 0 {
break;
}
match buf.strip_prefix("capability=") {
Some(cap) => {
let cap = cap.trim_end();
if !desired_capabilities.contains(&cap) {
return Err(message!(
"The server sent the '{cap}' capability which isn't among the ones we desire can support"
)
.raise());
}
capabilities.insert(cap.to_owned());
}
None => continue,
}
}
drop(read);
Ok(Client {
child: process,
out: input,
input: out,
capabilities,
version: chosen_version,
})
}
pub fn invoke(
&mut self,
command: &str,
meta: &mut dyn Iterator<Item = (&str, BString)>,
content: &mut dyn std::io::Read,
) -> ExnMessageResult<process::Status> {
use gix_error::{ResultExt, message};
self.send_command_and_meta(command, meta)?;
std::io::copy(content, &mut self.input).or_raise(|| message("Failed to read or write to the process"))?;
encode::flush_to_write(self.input.inner_mut())
.or_raise(|| message("Failed to read or write to the process"))?;
self.input
.flush()
.or_raise(|| message("Failed to read or write to the process"))?;
self.read_status()
.or_raise(|| message("Failed to read or write to the process"))
}
pub fn invoke_without_content<'a>(
&mut self,
command: &str,
meta: &mut dyn Iterator<Item = (&'a str, BString)>,
inspect_line: &mut dyn FnMut(&BStr),
) -> ExnMessageResult<process::Status> {
use gix_error::{ResultExt, message};
self.send_command_and_meta(command, meta)?;
while let Some(data) = self.out.read_line() {
let line = data
.or_raise(|| message("Failed to read from the process"))?
.or_raise(|| message("Failed to decode a packet line from the process"))?;
if let Some(line) = line.as_text() {
inspect_line(line.as_bstr());
}
}
self.out.reset_with(&[gix_packetline::PacketLineRef::Flush]);
let status = self
.read_status()
.or_raise(|| message("Failed to read or write to the process"))?;
Ok(status)
}
pub fn as_read(&mut self) -> impl std::io::Read + '_ {
self.out.reset_with(&[gix_packetline::PacketLineRef::Flush]);
ReadProcessOutputAndStatus {
inner: self.out.as_read(),
}
}
pub fn read_status(&mut self) -> std::io::Result<process::Status> {
read_status(&mut self.out.as_read())
}
}
impl Client {
fn send_command_and_meta(
&mut self,
command: &str,
meta: &mut dyn Iterator<Item = (&str, BString)>,
) -> ExnMessageResult {
use gix_error::{ResultExt, message};
self.input
.write_all(format!("command={command}").as_bytes())
.or_raise(|| message("Failed to read or write to the process"))?;
let mut buf = BString::default();
for (key, value) in meta {
buf.clear();
buf.push_str(key);
buf.push(b'=');
buf.push_str(&value);
self.input
.write_all(&buf)
.or_raise(|| message("Failed to read or write to the process"))?;
}
encode::flush_to_write(self.input.inner_mut())
.or_raise(|| message("Failed to read or write to the process"))?;
Ok(())
}
}
fn read_status(read: &mut PacketlineReader<'_>) -> std::io::Result<process::Status> {
let mut status = process::Status::Previous;
let mut buf = String::new();
let mut count = 0;
loop {
buf.clear();
let num_read = read.read_line_to_string(&mut buf)?;
if num_read == 0 {
break;
}
if let Some(name) = buf.strip_prefix("status=") {
status = process::Status::Named(name.trim_end().into());
}
count += 1;
}
if count > 0 && matches!(status, process::Status::Previous) {
status = process::Status::Unset;
}
read.reset_with(&[gix_packetline::PacketLineRef::Flush]);
Ok(status)
}
struct ReadProcessOutputAndStatus<'a> {
inner: PacketlineReader<'a>,
}
impl std::io::Read for ReadProcessOutputAndStatus<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let num_read = self.inner.read(buf)?;
if num_read == 0 {
self.inner.reset_with(&[gix_packetline::PacketLineRef::Flush]);
let status = read_status(&mut self.inner)?;
if status.is_success() {
Ok(0)
} else {
Err(std::io::Error::other(format!(
"Process indicated error after reading: {}",
status.message().unwrap_or_default()
)))
}
} else {
Ok(num_read)
}
}
}
impl Client {
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
pub fn capabilities_mut(&mut self) -> &mut Capabilities {
&mut self.capabilities
}
pub fn version(&self) -> usize {
self.version
}
}
impl Client {
pub fn into_child(self) -> std::process::Child {
self.child
}
}