gtk somewhat working

This commit is contained in:
2023-12-13 13:04:15 +01:00
parent 7a578073cd
commit b983d78575
16 changed files with 1377 additions and 206 deletions
+1
View File
@@ -0,0 +1 @@
pub mod run;
+38
View File
@@ -0,0 +1,38 @@
use log::{info, trace};
use sieverman::ConnectionInfo;
use crate::protocol::BackToFront;
pub(crate) async fn run(
to_frontend_tx: tokio::sync::mpsc::Sender<BackToFront>,
) -> anyhow::Result<()> {
trace!("Starting up sieverman…");
let info = ConnectionInfo::new("teilgedanken.de", 4190);
let mut connected = info.connect().await.unwrap();
info!("connected to the server");
loop {
match connected.read_introduction().await {
Ok(sieverman::IsComplete::Yes(c)) => {
connected = c;
break;
}
Ok(sieverman::IsComplete::No(c)) => {
trace!("Incomplete data waiting for some time…");
connected = c
}
Err(e) => return Err(anyhow::anyhow!("something went wrong: {}", e)),
};
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
connected.log_buffer().await;
}
trace!("Fully read the introduction:");
connected.log_buffer().await;
connected.log_server_settings().await;
to_frontend_tx
.send(BackToFront::ServerConnected(
connected.get_greeting().unwrap(),
))
.await
.unwrap();
Ok(())
}
+54
View File
@@ -0,0 +1,54 @@
use std::{cell::RefCell, rc::Rc};
use gtk4::{
glib,
prelude::{ApplicationExt, GtkWindowExt, WidgetExt},
Application, ApplicationWindow, Builder, Label,
};
use log::trace;
use crate::protocol::BackToFront;
pub(crate) fn get_app(
from_backend_rx: Rc<RefCell<Option<tokio::sync::mpsc::Receiver<BackToFront>>>>,
) -> Application {
let app = Application::builder()
.application_id("de.teilgedanken.sieverman")
.build();
app.connect_activate(move |app| build_ui(app, from_backend_rx.clone()));
app
}
fn build_ui(
app: &gtk4::Application,
from_backend_rx: Rc<RefCell<Option<tokio::sync::mpsc::Receiver<BackToFront>>>>,
) {
let glade_src = include_str!("../../../gui/sieverman.ui");
let builder = Builder::from_string(glade_src);
let main_window: ApplicationWindow = builder.object("sieverman").expect("get main-window");
main_window.set_height_request(400);
main_window.set_application(Some(app));
main_window.present();
let future = {
let mut data_event_receiver = from_backend_rx.take().expect("data_event_reciver");
async move {
while let Some(event) = data_event_receiver.recv().await {
trace!("data event: {:?}", event);
match event {
BackToFront::ServerConnected(message) => {
trace!("Received {}", message);
let status_label: Label = builder.object("serverstatus").unwrap();
status_label.set_text(&message)
}
}
}
}
};
let c = glib::MainContext::default();
c.spawn_local(future);
main_window.present();
}
+1
View File
@@ -0,0 +1 @@
pub mod main_window;
+5 -84
View File
@@ -1,98 +1,17 @@
use log::info;
use nom::branch::alt;
use nom::bytes::streaming::is_not;
use nom::bytes::complete::is_not;
use nom::character::is_space;
use nom::character::streaming::{
alpha1, digit1, line_ending, multispace0, multispace1, newline, none_of, not_line_ending,
};
use nom::multi::many_m_n;
use nom::sequence::{delimited, preceded, separated_pair};
use nom::{bytes::streaming::tag, IResult};
use nom::{bytes::complete::tag, IResult};
use nom::{character, number};
use sieverman::parser::parse_server_config;
use sieverman::{Methods, ServerSettings};
use std::error::Error;
pub fn do_nothing_parser(input: &str) -> IResult<&str, &str> {
Ok((input, ""))
}
fn quoted(input: &str) -> IResult<&str, &str> {
preceded(multispace0, delimited(tag("\""), is_not("\n\""), tag("\"")))(input)
}
fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
// note that this is really creating a function, the parser for abc
// vvvvv
// which is then called here, returning an IResult<&str, &str>
// vvvvv
preceded(multispace0, separated_pair(quoted, multispace1, quoted))(input)
}
fn parse_version(input: &str) -> IResult<&str, Vec<i32>> {
many_m_n(
1,
5,
alt((
character::complete::i32,
preceded(character::complete::char('.'), character::complete::i32),
)),
)(input)
}
fn parse_methods(input: &str) -> IResult<&str, Vec<Methods>> {
let (rem, res) = many_m_n(1, 10, alt((alpha1, preceded(multispace1, alpha1))))(input)?;
let methods = res
.into_iter()
.map(|m| match m {
"PLAIN" => Methods::Plain,
"OAUTHBEARER" => Methods::OAuthbearer,
any => todo!("That method is not yet implemented: {}", any),
})
.collect();
Ok((rem, methods))
}
fn parse_extensions(input: &str) -> IResult<&str, Vec<String>> {
let (rem, res) = many_m_n(1, 200, alt((is_not(" "), preceded(tag(" "), is_not(" ")))))(input)?;
let extensions = res.into_iter().map(|m| m.to_string()).collect();
Ok((rem, extensions))
}
fn parse_server_config(input: &str) -> IResult<&str, ServerSettings> {
let (remain, (key, implementation)) = key_value(input)?;
info!("Implementation:# {}", implementation);
let (remain, (key, version)) = key_value(remain)?;
info!("{}", version);
let (_, version) = parse_version(version)?;
info!("Version:# {:?}", version);
let (remain, (key, sasl)) = key_value(remain)?;
info!("{}", sasl);
let (_, sasl) = parse_methods(sasl)?;
info!("SASL:# {:?}", sasl);
let (remain, (key, capabilities)) = key_value(remain)?;
info!("{}", capabilities);
let (_, capabilities) = parse_extensions(capabilities)?;
info!("Extensions:# {:?}", &capabilities);
let (remain, (key, notify)) = key_value(remain)?;
info!("{}", notify);
let (_, notify) = parse_extensions(remain)?;
info!("Notify:# {:?}", &notify);
let (remain, (key, max_redirects)) = key_value(remain)?;
info!("{}", max_redirects);
let (_, max_redirects) = character::complete::u32(max_redirects)?;
info!("Max Redirects:# {}", max_redirects);
IResult::Ok((
remain,
ServerSettings {
implementation: implementation.to_string(),
version,
sasl,
capabilities,
notify,
max_redirects,
},
))
}
fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
@@ -105,5 +24,7 @@ fn main() -> Result<(), Box<dyn Error>> {
"MAXREDIRECTS" "1"
"#;
let (remaining_input, output) = parse_server_config(input)?;
dbg!(output);
Ok(())
}
+6
View File
@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum BackToFront {
ServerConnected(String),
}
+21 -104
View File
@@ -1,108 +1,25 @@
/// This is the simplest possible client using rustls that does something useful:
/// it accepts the default configuration, loads some root certs, and then connects
/// to google.com and issues a basic HTTP request. The response is printed to stdout.
///
/// It makes use of rustls::Stream to treat the underlying TLS connection as a basic
/// bi-directional stream -- the underlying IO is performed transparently.
///
/// Note that `unwrap()` is used to deal with networking errors; this is not something
/// that is sensible outside of example code.
use std::{os::unix::net::SocketAddr, sync::Arc};
mod backend;
mod gui;
pub mod protocol;
use std::{cell::RefCell, rc::Rc, thread};
use log::{info, trace};
use std::net::ToSocketAddrs;
use tokio::{
io::{self, split, AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf},
net::TcpStream,
};
use tokio_rustls::{
rustls::{ClientConfig, KeyLogFile, RootCertStore},
TlsConnector,
};
use gtk4::{glib, prelude::ApplicationExtManual as _};
async fn run() -> io::Result<()> {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// Allow using SSLKEYLOGFILE.
config.key_log = Arc::new(KeyLogFile::new());
let connector = TlsConnector::from(Arc::new(config));
let server_address = ("teilgedanken.de", 4190).to_socket_addrs()?.next().unwrap();
let stream = TcpStream::connect(&server_address).await?;
let domain = pki_types::ServerName::try_from("teilgedanken.de").unwrap();
let stream = connector.connect(domain, stream).await?;
let (mut reader, mut writer) = split(stream);
let mut dst = String::new();
reader
.read_to_string(&mut dst)
.await
.expect("Failed to read");
info!("{:?}", &dst);
//writer.write(src)
/* {
let mut reader = BufReader::new(stream);
let mut read_all = String::new();
/* loop {
trace!("reading max {} bytes", buffer.len());
let bytes_read = rea.read(&mut buffer);
if let Ok(bytes_read) = bytes_read {
trace!("read {:?} bytes", bytes_read);
if bytes_read == buffer.len() {
let cur = String::from_utf8_lossy(&buffer[..bytes_read]);
read_all += &cur;
trace!("Received: \n{}", cur);
} else {
let cur = String::from_utf8_lossy(&buffer[..bytes_read]);
read_all += &cur;
trace!("Last: \n{}", cur);
break;
}
} else {
trace!("Error after reading continue");
break;
}
} */
reader.read_to_string(&mut read_all);
info!("Read all:\n{}", read_all);
}
let ciphersuite = stream.conn.negotiated_cipher_suite().unwrap();
writeln!(
&mut std::io::stderr(),
"Current ciphersuite: {:?}",
ciphersuite.suite()
)
.unwrap();
/* stream
.clone()
.write_all(concat!("UNAUTHENTICATE\r\n",).as_bytes())
.unwrap();
loop {
let bytes_read = stream.read(&mut buffer);
if let Ok(bytes_read) = bytes_read {
if bytes_read > 0 {
trace!(
"Received: {:?}",
String::from_utf8_lossy(&buffer[..bytes_read])
);
} else {
break;
}
} else {
break;
}
} */ */
Ok(())
}
fn main() {
fn main() -> glib::ExitCode {
env_logger::init();
tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
.unwrap()
.block_on(run())
.expect("Failed to run or interrupted");
gtk4::init().expect("Failed to initialize GTK");
let (to_frontent_tx, from_backend_rx) = tokio::sync::mpsc::channel(5);
let _handle = thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap()
.block_on(backend::run::run(to_frontent_tx))
.expect("Failed to run or interrupted");
});
let app = gui::main_window::get_app(Rc::new(RefCell::new(Some(from_backend_rx))));
app.run()
}
+183
View File
@@ -0,0 +1,183 @@
/// This is the simplest possible client using rustls that does something useful:
/// it accepts the default configuration, loads some root certs, and then connects
/// to google.com and issues a basic HTTP request. The response is printed to stdout.
///
/// It makes use of rustls::Stream to treat the underlying TLS connection as a basic
/// bi-directional stream -- the underlying IO is performed transparently.
///
/// Note that `unwrap()` is used to deal with networking errors; this is not something
/// that is sensible outside of example code.
use std::sync::Arc;
use log::{error, info, trace};
use managesieve::{Capability, MSResult, Response};
use nom::{error::ParseError, IResult};
use sieverman::parser::parse_server_config;
use std::fmt::Debug;
use std::net::ToSocketAddrs;
use tokio::{
io::{self, split, AsyncBufReadExt, AsyncWriteExt, BufReader},
net::TcpStream,
};
use tokio_rustls::{
rustls::{ClientConfig, KeyLogFile, RootCertStore},
TlsConnector,
};
#[macro_export]
macro_rules! parse_response {
($reader:expr, $parser:expr, $dst:expr) => {{
let mut buf = String::new();
let capabilities = 'read_more: loop {
trace!("reading");
let read = $reader.read_line(&mut buf).await;
if let Ok(num) = read {
trace!("Read {} bytes", num);
trace!("Read Buffer: {:?}", &read);
trace!("Read Buffer: {:?}", &buf);
$dst += &buf;
buf.clear();
};
match $parser(&$dst) {
read @ Ok((_, _)) => {
//trace!("{:?}", &response);
break 'read_more read;
}
Err(_e) => continue,
Err(e) => {
error!("invalid response");
break Err(e);
}
Err(e) => {
error!("invalid input");
break Err(e);
}
};
};
capabilities
}};
}
async fn run() -> io::Result<()> {
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// Allow using SSLKEYLOGFILE.
config.key_log = Arc::new(KeyLogFile::new());
let connector = TlsConnector::from(Arc::new(config));
let server_address = ("teilgedanken.de", 4190).to_socket_addrs()?.next().unwrap();
let stream = TcpStream::connect(&server_address).await?;
let domain = pki_types::ServerName::try_from("teilgedanken.de").unwrap();
let stream = connector.connect(domain, stream).await?;
let (reader, mut writer) = split(stream);
let mut reader = BufReader::new(reader);
let mut dst = String::new();
let mut buf = String::new();
let capabilities: Option<(&str, Vec<Capability>)> = 'read_more: loop {
trace!("reading");
let read = reader.read_line(&mut buf).await;
if let Ok(num) = read {
trace!("Read {} bytes", num);
trace!("Read Buffer: {:?}", &read);
trace!("Read Buffer: {:?}", &buf);
dst += &buf;
buf.clear();
};
match managesieve::response_capability(&dst) {
Ok((rest, capability, response)) => {
trace!("{:?}", response);
break 'read_more Some((rest, capability));
}
Err(managesieve::MSError::IncompleteResponse) => continue,
Err(managesieve::MSError::InvalidResponse) => {
error!("invalid response");
break None;
}
Err(managesieve::MSError::InvalidInput) => {
error!("invalid input");
break None;
}
};
};
if let Some((rest, capability)) = capabilities {
trace!("Parsed: \n{}", dst);
dst = rest.to_string();
info!("Rest content:\n{:?}", &dst);
info!("The server config: {:?}", capability);
let have_space_command = managesieve::Command::have_space("loser.test", 249)
.expect("failed to create have_space command");
writer
.write_all(have_space_command.to_string().as_bytes())
.await?;
let response_parser = managesieve::response_havespace;
let result = parse_response!(reader, response_parser, dst);
dbg!(result);
let auth_command = managesieve::Command::authenticate();
writer
.write_all(auth_command.to_string().as_bytes())
.await?;
let response_parser = managesieve::response_authenticate;
let result = parse_response!(reader, response_parser, dst);
dbg!(result);
}
Ok(())
}
async fn read_until_complete<'a, Reader, Target>(
mut reader: Reader,
mut parser: impl FnMut(&str) -> IResult<&'a str, (Target, Response)>,
) -> IResult<&'a str, Target>
where
Reader: AsyncBufReadExt + Unpin,
Target: Debug,
{
let mut dst = String::new();
let mut buf = String::new();
let capabilities: MSResult<Target> = 'read_more: loop {
trace!("reading");
let read = reader.read_line(&mut buf).await;
if let Ok(num) = read {
trace!("Read {} bytes", num);
trace!("Read Buffer: {:?}", &read);
trace!("Read Buffer: {:?}", &buf);
dst += &buf;
buf.clear();
};
match parser(&dst) {
read @ Ok((_, _)) => {
//trace!("{:?}", &response);
break 'read_more read;
}
Err(_e) => continue,
Err(e) => {
error!("invalid response");
break Err(e);
}
Err(e) => {
error!("invalid input");
break Err(e);
}
};
};
if let Ok((rest, (target, response))) = capabilities {
trace!("Parsed: \n{}", dst);
dst = rest.to_string();
trace!("Rest content:\n{:?}", &dst);
trace!("Response content: {:?}", &response);
info!("The server config: {:?}", target);
}
todo!()
}
fn main() {
env_logger::init();
tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
.unwrap()
.block_on(run())
.expect("Failed to run or interrupted");
}
+165
View File
@@ -1,3 +1,168 @@
use std::{any, net::SocketAddr, sync::Arc};
use log::{error, info, trace};
use managesieve::Capability;
use std::net::ToSocketAddrs;
use thiserror::Error;
use tokio::{
io::{split, AsyncBufReadExt, BufReader, ReadHalf, WriteHalf},
net::TcpStream,
sync::Mutex,
task::JoinHandle,
};
use tokio_rustls::{
client::TlsStream,
rustls::{ClientConfig, KeyLogFile, RootCertStore},
TlsConnector,
};
pub mod parser;
#[derive(Debug, Clone)]
pub struct ConnectionInfo {
domain: String,
port: u16,
}
impl ConnectionInfo {
pub fn new(domain: &str, port: u16) -> Self {
Self {
domain: domain.to_string(),
port,
}
}
pub async fn connect(self) -> Result<ConnectionConnected, anyhow::Error> {
trace!("Building Cert Store");
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
// Allow using SSLKEYLOGFILE.
config.key_log = Arc::new(KeyLogFile::new());
trace!("Creating connector");
let connector = TlsConnector::from(Arc::new(config));
trace!("Connecting to the server.");
let stream = TcpStream::connect((self.domain.clone(), self.port)).await?;
let domain = pki_types::ServerName::try_from(self.domain.clone())?.to_owned();
trace!("Attach the connector");
let stream = connector.connect(domain, stream).await?;
let (reader, mut writer) = split(stream);
let buffer = Arc::new(Mutex::new(String::new()));
let spawned_buffer = buffer.clone();
let join_handle = tokio::spawn(async move {
let mut buf = String::new();
let mut reader = BufReader::new(reader);
loop {
trace!("Waiting for more data to read…");
let read = reader.read_line(&mut buf).await;
if let Ok(num) = read {
trace!("Read {} bytes", num);
trace!("Read Buffer: {:?}", &buf);
{
let mut s = spawned_buffer.lock().await;
s.push_str(&buf);
}
buf.clear();
};
}
});
Ok(ConnectionConnected {
info: self,
writer: Arc::new(writer), //server_settings: todo!(),
buffer,
server_settings: Vec::new(), //join_handle: Arc::new(join_handle),
})
}
}
#[derive(Debug, Clone)]
pub struct ConnectionConnected {
info: ConnectionInfo,
//reader: Arc<ReadHalf<TlsStream<TcpStream>>>,
writer: Arc<WriteHalf<TlsStream<TcpStream>>>,
buffer: Arc<Mutex<String>>,
//pub join_handle: Arc<JoinHandle<()>>,
server_settings: Vec<Capability>,
}
#[derive(Error, Debug)]
pub enum ReadError {
#[error("The data entered was invalid")]
InvalidInput(#[source] managesieve::Error),
#[error("The data the server responded was invalid")]
InvalidResponse(#[source] managesieve::Error),
}
pub enum IsComplete<T> {
Yes(T),
No(T),
}
impl ConnectionConnected {
pub async fn log_buffer(&self) {
trace!("Buffer is: \n{}", self.buffer.lock().await)
}
pub async fn log_server_settings(&self) {
trace!("Serversettings:\n{:?}", self.server_settings)
}
pub async fn read_introduction(self) -> Result<IsComplete<Self>, anyhow::Error> {
let Self {
info,
//reader: Arc<ReadHalf<TlsStream<TcpStream>>>,
writer,
buffer,
server_settings,
} = self;
let mut bf = buffer.lock().await;
let bf_str = bf.clone();
match managesieve::response_capability(&bf_str) {
Ok((rest, capability, response)) => {
trace!("{:?}", response);
bf.clear();
bf.push_str(rest);
drop(bf);
Ok(IsComplete::Yes(Self {
info,
writer: writer.clone(),
buffer: buffer.clone(),
server_settings: capability,
}))
}
Err(managesieve::Error::IncompleteResponse) => Ok(IsComplete::No(Self {
info,
writer,
buffer: buffer.clone(),
server_settings,
})),
Err(managesieve::Error::InvalidResponse) => {
error!("invalid response");
Err(managesieve::Error::InvalidResponse)?
}
Err(managesieve::Error::InvalidInput) => {
error!("invalid input");
Err(managesieve::Error::InvalidResponse)?
}
}
}
pub fn get_greeting(&self) -> Option<String> {
for c in self.server_settings.iter() {
if let Capability::Implementation(s) = c {
return Some(s.clone());
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct ConnectionAuthenticated {
info: ConnectionInfo,
server_settings: ServerSettings,
}
#[derive(Debug, Clone)]
pub struct ServerSettings {
pub implementation: String,
+4
View File
@@ -0,0 +1,4 @@
mod server_config;
mod utils;
pub use server_config::parse_server_config;
+84
View File
@@ -0,0 +1,84 @@
use log::{info, trace};
use nom::{
branch::alt,
bytes::complete::is_not,
bytes::complete::tag,
character::{
self,
complete::{alpha1, multispace1},
},
multi::many_m_n,
sequence::preceded,
IResult,
};
use crate::{parser::utils::key_value, Methods, ServerSettings};
fn parse_version(input: &str) -> IResult<&str, Vec<i32>> {
many_m_n(
1,
5,
alt((
character::complete::i32,
preceded(character::complete::char('.'), character::complete::i32),
)),
)(input)
}
fn parse_methods(input: &str) -> IResult<&str, Vec<Methods>> {
let (rem, res) = many_m_n(1, 10, alt((alpha1, preceded(multispace1, alpha1))))(input)?;
let methods = res
.into_iter()
.map(|m| match m {
"PLAIN" => Methods::Plain,
"OAUTHBEARER" => Methods::OAuthbearer,
any => todo!("That method is not yet implemented: {}", any),
})
.collect();
Ok((rem, methods))
}
fn parse_extensions(input: &str) -> IResult<&str, Vec<String>> {
let space_then_word = preceded(tag(" "), is_not(" "));
let word = is_not(" ");
let (rem, res) = many_m_n(1, 200, alt((word, space_then_word)))(input)?;
let extensions = res.into_iter().map(|m| m.to_string()).collect();
Ok((rem, extensions))
}
pub fn parse_server_config(input: &str) -> IResult<&str, ServerSettings> {
let (remain, (_key, implementation)) = key_value(input)?;
trace!("Implementation:# {}", implementation);
let (remain, (_key, version)) = key_value(remain)?;
trace!("{}", version);
let (_, version) = parse_version(version)?;
trace!("Version:# {:?}", version);
let (remain, (_key, sasl)) = key_value(remain)?;
trace!("{}", sasl);
let (_, sasl) = parse_methods(sasl)?;
trace!("SASL:# {:?}", sasl);
let (remain, (_key, capabilities)) = key_value(remain)?;
trace!("{}", capabilities);
let (_, capabilities) = parse_extensions(capabilities)?;
trace!("Extensions:# {:?}", &capabilities);
let (remain, (_key, notify)) = key_value(remain)?;
trace!("{}", notify);
let (_, notify) = parse_extensions(remain)?;
trace!("Notify:# {:?}", &notify);
let (remain, (_key, max_redirects)) = key_value(remain)?;
trace!("{}", max_redirects);
let (_, max_redirects) = character::complete::u32(max_redirects)?;
trace!("Max Redirects:# {}", max_redirects);
IResult::Ok((
remain,
ServerSettings {
implementation: implementation.to_string(),
version,
sasl,
capabilities,
notify,
max_redirects,
},
))
}
+19
View File
@@ -0,0 +1,19 @@
use nom::{
bytes::complete::is_not,
bytes::complete::tag,
character::complete::{multispace0, multispace1},
sequence::{delimited, preceded, separated_pair},
IResult,
};
pub(crate) fn quoted(input: &str) -> IResult<&str, &str> {
preceded(multispace0, delimited(tag("\""), is_not("\n\""), tag("\"")))(input)
}
pub(crate) fn key_value(input: &str) -> IResult<&str, (&str, &str)> {
// note that this is really creating a function, the parser for abc
// vvvvv
// which is then called here, returning an IResult<&str, &str>
// vvvvv
preceded(multispace0, separated_pair(quoted, multispace1, quoted))(input)
}