initial commit

This commit is contained in:
Surferlul 2022-07-20 02:19:12 +02:00
commit 9744166dca
16 changed files with 829 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

203
Cargo.lock generated Normal file
View File

@ -0,0 +1,203 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "evdev"
version = "0.11.5"
dependencies = [
"bitvec",
"libc",
"nix 0.23.1",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "libc"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "nix"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "nix"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
name = "proc-macro2"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "remote-evdev"
version = "0.1.0"
dependencies = [
"byteorder",
"evdev",
"nix 0.24.2",
"ron",
]
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64",
"bitflags",
"serde",
]
[[package]]
name = "serde"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "unicode-ident"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]]
name = "wyz"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e"
dependencies = [
"tap",
]

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "remote-evdev"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
evdev = "0.11.4"
ron = "*"
byteorder = "1.4.3"
nix = "0.24.2"
[patch.crates-io]
evdev = { path = "evdev" }

92
src/config.rs Normal file
View File

@ -0,0 +1,92 @@
use std::env;
use std::path::Path;
pub struct DeviceInfo {
pub path: String,
pub device_type: String
}
pub struct NetConfig {
pub is_server: bool,
pub is_host: bool,
pub port: i32,
pub ip_address: String
}
pub fn get_config() -> (NetConfig, Vec<DeviceInfo>) {
let args: Vec<String> = env::args().collect();
let mut context = "";
let mut cfg = NetConfig {
is_server: false,
is_host: true,
port: 64654,
ip_address: String::from("auto")
};
let mut devices_info = Vec::new();
for i in &args[1..] {
match i.as_str() {
"-s" | "--server" => cfg.is_server = true,
"-c" | "--client" => cfg.is_server = false,
"-h" | "--host" => cfg.is_host = true,
"-g" | "--guest" => cfg.is_host = false,
"-p" | "--port" => context = "port",
"-a" | "--ip-address" => context = "ip_address",
"-d" | "--device" => {
context = "device";
devices_info.push(DeviceInfo {
path: String::from(""),
device_type: String::from("other")
});
},
"--id" => context = "device_id",
"--path" => context = "device_path",
"--event" => context = "device_event",
"--full-path" => context = "device_full_path",
"--type" => context = "device_type",
&_ => {
if context.starts_with("device") {
let (key, tmp) = match context {
"device_id" => ("path", "/dev/input/by-id/"),
"device_path" => ("path", "/dev/input/by-path/"),
"device_event" => ("path", "/dev/input/"),
"device_full_path" => ("path", ""),
"device_type" => {
match i.as_str() {
"pointer" | "keyboard" => ("type", ""),
&_ => panic!("Invalid device type {}!", i.as_str())
}
},
&_ => panic!("Unknown context!")
};
let mut value = tmp.to_owned();
let mut device_info = devices_info.last_mut().unwrap();
if key == "path" {
value = format!("{}{}", value, i);
if !Path::new(value.as_str()).exists() {
panic!("Path {} does not exist!", value)
}
device_info.path = value;
} else if key == "type" {
device_info.device_type = key.to_owned();
}
} else {
match context {
"port" => {
match i.parse::<i32>() {
Ok(num) => cfg.port = num,
Err(_) => panic!("Invalid port value!")
}
},
"ip_address" => cfg.ip_address = i.to_owned(),
&_ => panic!("Invalid context!")
}
}
},
}
}
if cfg.ip_address == "auto" {
panic!("Not implemented yet!")
}
return (cfg, devices_info);
}

21
src/epoll_struct.rs Normal file
View File

@ -0,0 +1,21 @@
use std::os::unix::io::{AsRawFd, RawFd};
pub struct Epoll(RawFd);
impl Epoll {
pub(crate) fn new(fd: RawFd) -> Self {
Epoll(fd)
}
}
impl AsRawFd for Epoll {
fn as_raw_fd(&self) -> RawFd {
self.0
}
}
impl Drop for Epoll {
fn drop(&mut self) {
let _ = nix::unistd::close(self.0);
}
}

View File

@ -0,0 +1,55 @@
use std::io::prelude::*;
use std::net::TcpStream;
use byteorder::{BigEndian, ReadBytesExt};
use evdev::{
InputEvent,
uinput::VirtualDevice
};
use crate::{
manage_device::deserialize::{
deserialize_device::build_virtual_device,
deserialize_input_event
}
};
fn receive_device(mut stream: &TcpStream) -> std::io::Result<Option<VirtualDevice>> {
let buf_size = stream.read_u64::<BigEndian>()?;
if buf_size == 0 {
return Ok(None)
};
let mut buf = vec![0; buf_size as usize];
stream.read_exact(&mut buf)?;
let data = String::from_utf8(buf).expect("Couldn't decode utf8");
Ok(Some(build_virtual_device(data)))
}
fn receive_input_event(mut stream: &TcpStream) -> std::io::Result<(usize, InputEvent)> {
Ok(
(
stream.read_u64::<BigEndian>()? as usize,
deserialize_input_event(
(
stream.read_u16::<BigEndian>()?,
stream.read_u16::<BigEndian>()?,
stream.read_i32::<BigEndian>()?
)
)
)
)
}
pub fn handle_client(stream: TcpStream) -> std::io::Result<()>{
let mut devices = Vec::new();
while let Some(device) = receive_device(&stream)? {
devices.push(device);
}
loop {
let (device_id, input_event) = receive_input_event(&stream)?;
if device_id as u64 == u32::MAX as u64 {
break
}
devices[device_id].emit_raw(&[input_event]).expect("Couldn't emit input event");
}
Ok(())
}

View File

@ -0,0 +1,81 @@
use evdev::{Device, InputEvent};
use crate::manage_device::set_unblocking;
pub struct FetchDevicesEventsSynced<'a> {
collection: &'a mut DeviceCollection
}
impl <'a>Iterator for FetchDevicesEventsSynced<'a> {
type Item = Vec<(usize, InputEvent)>;
fn next(&mut self) -> Option<Self::Item> {
let mut events = Vec::new();
for device_id in 0..self.collection.len() {
match self.collection[device_id].fetch_events() {
Ok(events_fetch) => {
for event in events_fetch {
events.push((device_id, event))
}
},
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
}
Err(e) => {
eprintln!("{}", e);
return None
}
}
};
Some(events)
}
}
pub struct DeviceCollection {
pub devices: Vec<Device>
}
impl DeviceCollection {
pub fn new() -> DeviceCollection {
DeviceCollection {
devices: Vec::new()
}
}
pub fn push(&mut self, mut device: Device) {
set_unblocking(&mut device);
device.grab().expect("Couldn't grab device!");
self.devices.push(device);
}
pub fn len(&mut self) -> usize {
self.devices.len()
}
pub fn fetch_events(&mut self) -> FetchDevicesEventsSynced {
FetchDevicesEventsSynced {collection: self}
}
}
impl IntoIterator for DeviceCollection {
type Item = Device;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.devices.into_iter()
}
}
impl std::ops::Index<usize> for DeviceCollection {
type Output = Device;
fn index(&self, index: usize) -> &Self::Output {
&self.devices[index]
}
}
impl std::ops::IndexMut<usize> for DeviceCollection {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.devices[index]
}
}

View File

@ -0,0 +1,62 @@
use std::io::prelude::*;
use std::net::TcpStream;
use evdev::{
Device,
InputEvent
};
use byteorder::{BigEndian, WriteBytesExt};
mod device_collection;
use crate::{
config::DeviceInfo,
manage_device::serialize::{
serialize_device::serialize,
serialize_input_event
},
handle_client::handle_host::device_collection::DeviceCollection
};
fn send_device(mut stream: &TcpStream, device: &mut Device) -> std::io::Result<()>{
let buf = serialize(&device);
let buf_len = buf.as_bytes().len() as u64;
stream.write_u64::<BigEndian>(buf_len)?;
stream.write(buf.as_bytes())?;
Ok(())
}
fn send_input_event(mut stream: &TcpStream, device_id: usize, input_event: InputEvent) -> std::io::Result<()> {
let data = serialize_input_event(input_event);
stream.write_u64::<BigEndian>(device_id as u64)?;
stream.write_u16::<BigEndian>(data.0)?;
stream.write_u16::<BigEndian>(data.1)?;
stream.write_i32::<BigEndian>(data.2)?;
Ok(())
}
pub fn handle_client(mut stream: TcpStream, devices_info: &Vec<DeviceInfo>) -> std::io::Result<()>{
let mut collection = DeviceCollection::new();
for device_info in devices_info {
match Device::open(&device_info.path) {
Ok(device) => collection.push(device),
Err(_) => panic!("Couldn't open device {}", device_info.path)
}
}
for device in collection.devices.iter_mut() {
send_device(&stream, device)?;
}
stream.write_u64::<BigEndian>(0)?;
for events in collection.fetch_events() {
for (device_id, event) in events {
send_input_event(&stream, device_id, event)?;
}
}
stream.write_u64::<BigEndian>(u32::MAX as u64)?;
stream.write_u16::<BigEndian>(0)?;
stream.write_u16::<BigEndian>(0)?;
stream.write_i32::<BigEndian>(0)?;
for device in collection.devices.iter_mut() {
device.ungrab().expect("Couldn't ungrab device {}!");
}
Ok(())
}

14
src/handle_client/mod.rs Normal file
View File

@ -0,0 +1,14 @@
use std::net::TcpStream;
mod handle_host;
mod handle_guest;
use crate::config::{NetConfig, DeviceInfo};
pub fn handle_client(stream: TcpStream, cfg: &NetConfig, devices_info: &Vec<DeviceInfo>) {
if cfg.is_host {
crate::handle_client::handle_host::handle_client(stream, devices_info).expect("Couldn't write to stream")
} else {
crate::handle_client::handle_guest::handle_client(stream).expect("Couldn't read from stream")
}
}

24
src/main.rs Normal file
View File

@ -0,0 +1,24 @@
#![feature(generic_associated_types)]
mod config;
mod epoll_struct;
mod streams;
mod handle_client;
mod manage_device;
use crate::{
config::{
get_config,
},
streams::get_streams,
handle_client::handle_client,
};
fn main() {
let (cfg, devices_info) = get_config();
let ip = format!("{}:{}", cfg.ip_address, cfg.port);
for stream in get_streams(cfg.is_server, ip) {
handle_client(stream, &cfg, &devices_info);
}
}

View File

@ -0,0 +1,94 @@
use std::ops::DerefMut;
use ron::de::from_str;
use evdev::{
Key,
InputId,
SwitchType,
RelativeAxisType,
BusType,
AttributeSet,
AttributeSetRef,
uinput::{
VirtualDevice,
VirtualDeviceBuilder
}
};
fn deserialize_name(data: &str) -> String {
from_str(data).expect("Couldn't deserialize name!")
}
fn deserialize_input_id(data: &str) -> InputId {
let vals: [u16; 4] = from_str(data).expect("Couldn't deserialize InputId!");
InputId::new(BusType(vals[0]), vals[1], vals[2], vals[3])
}
fn deserialize_supported_keys(data: &str, keys: &mut AttributeSetRef<Key>) {
let vals: Vec<u16> = from_str(data).expect("Couldn't deserialize supported keys!");
for key in vals.iter().map(|x| Key(*x)) {
keys.insert(key);
}
}
fn deserialize_supported_relative_axes(data: &str, relative_axes: &mut AttributeSetRef<RelativeAxisType>) -> Result<(), &'static str> {
match from_str::<Option<Vec<u16>>>(data).expect("Couldn't deserialize relative axes") {
Some(vals) => {
for rel_axes_type in vals.iter().map(|x| RelativeAxisType(*x)) {
relative_axes.insert(rel_axes_type);
}
Ok(())
},
None => Err("No relative axes support"),
}
}
fn deserialize_supported_switches(data: &str, switches: &mut AttributeSetRef<SwitchType>) -> Result<(), &'static str> {
match from_str::<Option<Vec<u16>>>(data).expect("Couldn't deserialize relative axes") {
Some(vals) => {
for switch_type in vals.iter().map(|x| SwitchType(*x)) {
switches.insert(switch_type);
}
Ok(())
},
None => Err("No switch support"),
}
}
fn deserialize(
data: String,
keys: &mut AttributeSetRef<Key>,
relative_axes: &mut AttributeSetRef<RelativeAxisType>,
switches: &mut AttributeSetRef<SwitchType>
) -> (String, InputId, Result<(), &'static str>, Result<(), &'static str>) {
let de_data: [String; 5] = from_str(data.as_str()).expect("Couldn't deserialize device!");
let name = deserialize_name(de_data[0].as_str());
let input_id = deserialize_input_id(de_data[1].as_str());
deserialize_supported_keys(de_data[2].as_str(), keys);
(
name,
input_id,
deserialize_supported_relative_axes(de_data[3].as_str(), relative_axes),
deserialize_supported_switches(de_data[4].as_str(), switches)
)
}
pub fn build_virtual_device(data: String) -> VirtualDevice {
let mut keys: AttributeSet<Key> = AttributeSet::new();
let mut relative_axes: AttributeSet<RelativeAxisType> = AttributeSet::new();
let mut switches: AttributeSet<SwitchType> = AttributeSet::new();
let (name, input_id, relative_axes_support, switch_support) = deserialize(data, keys.deref_mut(), relative_axes.deref_mut(), switches.deref_mut());
let mut uinput_builder = VirtualDeviceBuilder::new().expect("Couldn't create new virtual device builder!")
.name(name.as_str())
.input_id(input_id)
.with_keys(keys.deref_mut()).expect("Couldn't build virtual device with keys!");
match relative_axes_support {
Ok(_) => {},
Err(_) => uinput_builder = uinput_builder.with_relative_axes(relative_axes.deref_mut()).expect("Couldn't build virtual device with relative axes!")
}
match switch_support {
Ok(_) => {},
Err(_) => uinput_builder = uinput_builder.with_switches(switches.deref_mut()).expect("Couldn't build virtual device with switches!")
}
uinput_builder.build().expect("Couldn't build virtual device!")
}

View File

@ -0,0 +1,10 @@
use evdev::{
InputEvent,
EventType
};
pub mod deserialize_device;
pub fn deserialize_input_event(data: (u16, u16, i32)) -> InputEvent {
InputEvent::new(EventType(data.0), data.1, data.2)
}

35
src/manage_device/mod.rs Normal file
View File

@ -0,0 +1,35 @@
use std::os::unix::io::AsRawFd;
use evdev::Device;
use nix::{
fcntl::{FcntlArg, OFlag},
sys::epoll,
};
pub mod serialize;
pub mod deserialize;
use crate::{
epoll_struct::Epoll,
};
pub fn set_unblocking(device: &mut Device) {
let raw_fd = device.as_raw_fd();
// Set nonblocking
nix::fcntl::fcntl(raw_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("Couldn't set nonblocking!");
//Create epoll handle and attach raw_fd
let epoll_fd = Epoll::new(epoll::epoll_create1(
epoll::EpollCreateFlags::EPOLL_CLOEXEC,
).expect("Couldn't create epoll handle!"));
let mut event = epoll::EpollEvent::new(epoll::EpollFlags::EPOLLIN, 0);
epoll::epoll_ctl(
epoll_fd.as_raw_fd(),
epoll::EpollOp::EpollCtlAdd,
raw_fd,
Some(&mut event),
).expect("Couldn't attach raw_fd to epoll!");
// We don't care about these, but the kernel wants to fill them.
let _events = [epoll::EpollEvent::empty(); 2];
}

View File

@ -0,0 +1,7 @@
use evdev::InputEvent;
pub mod serialize_device;
pub fn serialize_input_event(event: InputEvent) -> (u16, u16, i32) {
(event.event_type().0, event.code(), event.value())
}

View File

@ -0,0 +1,60 @@
use ron::ser::to_string;
use evdev::Device;
fn serialize_name(device: &Device) -> String {
to_string(
device.name().unwrap()
).expect("Error serializing name!")
}
fn serialize_input_id(device: &Device) -> String {
let input_id = device.input_id();
to_string(
&[
input_id.bus_type().0,
input_id.vendor(),
input_id.product(),
input_id.version()
]
).expect("Error serializing InputId!")
}
fn serialize_supported_keys(device: &Device) -> String {
to_string(
&device.supported_keys().unwrap().iter().map(|x| x.0).collect::<Vec<u16>>()
).expect("Error serializing supported_keys!")
}
fn serialize_supported_relative_axes(device: &Device) -> String {
to_string::<Option<Vec<u16>>>(
&match device.supported_relative_axes() {
Some(relative_axes) => Some(
relative_axes.iter().map(|x| x.0).collect::<Vec<u16>>()
),
None => None
}
).expect("Error serializing supported_relative_axes!")
}
fn serialize_supported_switches(device: &Device) -> String {
to_string::<Option<Vec<u16>>>(
&match device.supported_switches() {
Some(switches) => Some(
switches.iter().map(|x| x.0).collect::<Vec<u16>>()
),
None => None
}
).expect("Error serializing supported_switches!")
}
pub fn serialize(device: &Device) -> String {
to_string(
&[
serialize_name(device),
serialize_input_id(device),
serialize_supported_keys(device),
serialize_supported_relative_axes(device),
serialize_supported_switches(device)
]
).expect("Error serializing device!")
}

55
src/streams.rs Normal file
View File

@ -0,0 +1,55 @@
use std::net::{TcpListener, TcpStream};
pub struct StreamIter {
ip: String,
is_server: bool,
listener: Option<TcpListener>,
}
impl Iterator for StreamIter {
type Item = TcpStream;
fn next(&mut self) -> Option<Self::Item> {
if self.is_server {
let mut incoming = match self.listener.as_mut() {
Some(listener) => listener.incoming(),
None => {
self.listener = Some(match TcpListener::bind(self.ip.as_str()) {
Ok(listener) => listener,
Err(_) => panic!("Unable to bind to {}", self.ip)
});
self.listener.as_mut().unwrap().incoming()
}
};
println!("Waiting for connection from client");
match incoming.next() {
Some(stream) => match stream {
Ok(client) => {
println!("Connection established");
Some(client)
},
Err(_) => panic!("Unable to get stream")
},
None => None
}
} else {
println!("Connecting to server");
match TcpStream::connect(self.ip.as_str()) {
Ok(client) => {
println!("Connection established");
Some(client)
},
Err(_) => panic!("Unable to connect to {}:", self.ip)
}
}
}
}
pub fn get_streams(is_server: bool, ip: String) -> StreamIter {
StreamIter {
ip,
is_server,
listener: None,
}
}