mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat: inital support for .RDP files (#862)
This is paving the way for .rdp file support. Issue: ARC-339 Issue: ARC-355
This commit is contained in:
@@ -123,6 +123,14 @@ RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway.
|
||||
Lightweight and `no_std`-compatible generic `Error` and `Report` types.
|
||||
The `Error` type wraps a custom consumer-defined type for domain-specific details (such as `PduErrorKind`).
|
||||
|
||||
#### [`crates/ironrdp-propertyset`](./crates/ironrdp-propertyset)
|
||||
|
||||
The main type is `PropertySet`, a key-value store for configuration options.
|
||||
|
||||
#### [`crates/ironrdp-rdpfile`](./crates/ironrdp-rdpfile)
|
||||
|
||||
Loader and writer for the .RDP file format.
|
||||
|
||||
### Extra Tier
|
||||
|
||||
Higher level libraries and binaries built on top of the core tier.
|
||||
@@ -188,6 +196,10 @@ Web-based frontend using `Svelte` and `Material` frameworks.
|
||||
|
||||
Native CLIPRDR backend implementations.
|
||||
|
||||
#### [`crates/ironrdp-cfg`](./crates/ironrdp-cfg)
|
||||
|
||||
IronRDP-related utilities for ironrdp-propertyset.
|
||||
|
||||
### Internal Tier
|
||||
|
||||
Crates that are only used inside the IronRDP project, not meant to be published.
|
||||
|
||||
Generated
+26
@@ -2422,6 +2422,13 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-cfg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ironrdp-propertyset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-client"
|
||||
version = "0.1.0"
|
||||
@@ -2431,10 +2438,13 @@ dependencies = [
|
||||
"futures-util",
|
||||
"inquire",
|
||||
"ironrdp",
|
||||
"ironrdp-cfg",
|
||||
"ironrdp-cliprdr-native",
|
||||
"ironrdp-core",
|
||||
"ironrdp-dvc-pipe-proxy",
|
||||
"ironrdp-propertyset",
|
||||
"ironrdp-rdcleanpath",
|
||||
"ironrdp-rdpfile",
|
||||
"ironrdp-rdpsnd-native",
|
||||
"ironrdp-tls",
|
||||
"ironrdp-tokio",
|
||||
@@ -2635,6 +2645,13 @@ dependencies = [
|
||||
name = "ironrdp-pdu-generators"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-propertyset"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdcleanpath"
|
||||
version = "0.1.3"
|
||||
@@ -2666,6 +2683,13 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpfile"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ironrdp-propertyset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpsnd"
|
||||
version = "0.5.0"
|
||||
@@ -2764,7 +2788,9 @@ dependencies = [
|
||||
"ironrdp-graphics",
|
||||
"ironrdp-input",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-propertyset",
|
||||
"ironrdp-rdcleanpath",
|
||||
"ironrdp-rdpfile",
|
||||
"ironrdp-rdpsnd",
|
||||
"ironrdp-session",
|
||||
"lazy_static",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ironrdp-cfg"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "IronRDP utilities for ironrdp-cfgstore"
|
||||
publish = false # TODO: publish
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,7 @@
|
||||
# IronRDP Configuration
|
||||
|
||||
IronRDP-related utilities for ironrdp-propertyset.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
@@ -0,0 +1,63 @@
|
||||
// QUESTION: consider auto-generating this file based on a reference file?
|
||||
// https://gist.github.com/awakecoding/838c7fe2ed3a6208e3ca5d8af25363f6
|
||||
|
||||
use ironrdp_propertyset::PropertySet;
|
||||
|
||||
pub trait PropertySetExt {
|
||||
fn full_address(&self) -> Option<&str>;
|
||||
|
||||
fn server_port(&self) -> Option<i64>;
|
||||
|
||||
fn alternate_full_address(&self) -> Option<&str>;
|
||||
|
||||
fn gateway_hostname(&self) -> Option<&str>;
|
||||
|
||||
fn remote_application_name(&self) -> Option<&str>;
|
||||
|
||||
fn remote_application_program(&self) -> Option<&str>;
|
||||
|
||||
fn kdc_proxy_url(&self) -> Option<&str>;
|
||||
|
||||
fn username(&self) -> Option<&str>;
|
||||
|
||||
/// Target RDP server password - use for testing only
|
||||
fn clear_text_password(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
impl PropertySetExt for PropertySet {
|
||||
fn full_address(&self) -> Option<&str> {
|
||||
self.get::<&str>("full address")
|
||||
}
|
||||
|
||||
fn server_port(&self) -> Option<i64> {
|
||||
self.get::<i64>("server port")
|
||||
}
|
||||
|
||||
fn alternate_full_address(&self) -> Option<&str> {
|
||||
self.get::<&str>("alternate full address")
|
||||
}
|
||||
|
||||
fn gateway_hostname(&self) -> Option<&str> {
|
||||
self.get::<&str>("gatewayhostname")
|
||||
}
|
||||
|
||||
fn remote_application_name(&self) -> Option<&str> {
|
||||
self.get::<&str>("remoteapplicationname")
|
||||
}
|
||||
|
||||
fn remote_application_program(&self) -> Option<&str> {
|
||||
self.get::<&str>("remoteapplicationprogram")
|
||||
}
|
||||
|
||||
fn kdc_proxy_url(&self) -> Option<&str> {
|
||||
self.get::<&str>("kdcproxyurl")
|
||||
}
|
||||
|
||||
fn username(&self) -> Option<&str> {
|
||||
self.get::<&str>("username")
|
||||
}
|
||||
|
||||
fn clear_text_password(&self) -> Option<&str> {
|
||||
self.get::<&str>("ClearTextPassword")
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" }
|
||||
ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.6", features = ["reqwest"] }
|
||||
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
|
||||
ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy"
|
||||
ironrdp-propertyset.path = "../ironrdp-propertyset"
|
||||
ironrdp-rdpfile.path = "../ironrdp-rdpfile"
|
||||
ironrdp-cfg.path = "../ironrdp-cfg"
|
||||
|
||||
# Windowing and rendering
|
||||
winit = { version = "0.30", features = ["rwh_06"] }
|
||||
|
||||
@@ -8,6 +8,7 @@ use clap::Parser;
|
||||
use ironrdp::connector::{self, Credentials};
|
||||
use ironrdp::pdu::rdp::capability_sets::{client_codecs_capabilities, MajorPlatformType};
|
||||
use ironrdp::pdu::rdp::client_info::PerformanceFlags;
|
||||
use std::path::PathBuf;
|
||||
use tap::prelude::*;
|
||||
use url::Url;
|
||||
|
||||
@@ -176,13 +177,17 @@ impl FromStr for DvcProxyInfo {
|
||||
#[clap(author = "Devolutions", about = "Devolutions-IronRDP client")]
|
||||
#[clap(version, long_about = None)]
|
||||
struct Args {
|
||||
/// An address on which the client will connect.
|
||||
destination: Option<Destination>,
|
||||
|
||||
/// Path to a .rdp file to read the configuration from.
|
||||
#[clap(long)]
|
||||
rdp_file: Option<PathBuf>,
|
||||
|
||||
/// A file with IronRDP client logs
|
||||
#[clap(short, long)]
|
||||
log_file: Option<String>,
|
||||
|
||||
/// An address on which the client will connect.
|
||||
destination: Option<Destination>,
|
||||
|
||||
/// A target RDP server user name
|
||||
#[clap(short, long)]
|
||||
username: Option<String>,
|
||||
@@ -273,21 +278,46 @@ struct Args {
|
||||
#[clap(long, num_args = 1.., value_delimiter = ',')]
|
||||
codecs: Vec<String>,
|
||||
|
||||
/// Add DVC channel named pipe proxy.
|
||||
/// the format is <name>=<pipe>
|
||||
/// e.g. `ChannelName=PipeName` where `ChannelName` is the name of the channel,
|
||||
/// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix),
|
||||
/// e.g. PipeName will automatically be prefixed with `\\.\pipe\` on Windows.
|
||||
/// Add DVC channel named pipe proxy
|
||||
///
|
||||
/// The format is `<name>=<pipe>`, e.g., `ChannelName=PipeName` where `ChannelName` is the name of the channel,
|
||||
/// and `PipeName` is the name of the named pipe to connect to (without OS-specific prefix).
|
||||
/// `<pipe>` will automatically be prefixed with `\\.\pipe\` on Windows.
|
||||
#[clap(long)]
|
||||
dvc_proxy: Vec<DvcProxyInfo>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn parse_args() -> anyhow::Result<Self> {
|
||||
use ironrdp_cfg::PropertySetExt as _;
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let mut properties = ironrdp_propertyset::PropertySet::new();
|
||||
|
||||
if let Some(rdp_file) = args.rdp_file {
|
||||
let input =
|
||||
std::fs::read_to_string(&rdp_file).with_context(|| format!("failed to read {}", rdp_file.display()))?;
|
||||
|
||||
if let Err(errors) = ironrdp_rdpfile::load(&mut properties, &input) {
|
||||
for e in errors {
|
||||
#[expect(clippy::print_stderr)]
|
||||
{
|
||||
eprintln!("Error when reading {}: {e}", rdp_file.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let destination = if let Some(destination) = args.destination {
|
||||
destination
|
||||
} else if let Some(destination) = properties.full_address() {
|
||||
if let Some(port) = properties.server_port() {
|
||||
format!("{destination}:{port}").parse()
|
||||
} else {
|
||||
destination.parse()
|
||||
}
|
||||
.context("invalid destination")?
|
||||
} else {
|
||||
inquire::Text::new("Server address:")
|
||||
.prompt()
|
||||
@@ -297,12 +327,16 @@ impl Config {
|
||||
|
||||
let username = if let Some(username) = args.username {
|
||||
username
|
||||
} else if let Some(username) = properties.username() {
|
||||
username.to_owned()
|
||||
} else {
|
||||
inquire::Text::new("Username:").prompt().context("Username prompt")?
|
||||
};
|
||||
|
||||
let password = if let Some(password) = args.password {
|
||||
password
|
||||
} else if let Some(password) = properties.clear_text_password() {
|
||||
password.to_owned()
|
||||
} else {
|
||||
inquire::Password::new("Password:")
|
||||
.without_confirmation()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::boxed::Box;
|
||||
use core::fmt;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ironrdp-propertyset"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "A key-value store for configuration options"
|
||||
publish = false # TODO: publish
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,7 @@
|
||||
# IronRDP PropertySet
|
||||
|
||||
The main type is `PropertySet`, a key-value store for configuration options.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
@@ -0,0 +1,171 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use core::fmt::{self, Display};
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
|
||||
pub type Key = Cow<'static, str>;
|
||||
|
||||
/// Key-value store for configuration keys.
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
pub struct PropertySet {
|
||||
inner: BTreeMap<Key, Value>,
|
||||
}
|
||||
|
||||
impl PropertySet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: impl Into<Key>, value: impl Into<Value>) -> Option<Value> {
|
||||
let (key, value) = (key.into(), value.into());
|
||||
debug!("PropertySet::insert({key}, {value})");
|
||||
self.inner.insert(key, value)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &str) -> Option<Value> {
|
||||
let value = self.inner.remove(key);
|
||||
|
||||
match &value {
|
||||
Some(value) => debug!("PropertySet::remove({key}) = {value}"),
|
||||
None => debug!("PropertySet::remove({key}) = None"),
|
||||
}
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
pub fn get<'a, V: ExtractFrom<&'a Value>>(&'a self, key: &str) -> Option<V> {
|
||||
let value = self.inner.get(key);
|
||||
|
||||
match &value {
|
||||
Some(value) => debug!("PropertySet::get({key}) = {value}"),
|
||||
None => debug!("PropertySet::get({key}) = None"),
|
||||
}
|
||||
|
||||
value.and_then(|val| V::extract_from(val, private::Token))
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Key, &Value)> {
|
||||
self.inner.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for PropertySet {
|
||||
type Item = (Key, Value);
|
||||
|
||||
type IntoIter = alloc::collections::btree_map::IntoIter<Key, Value>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PropertySet {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.inner, f)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($from:ty => $enum:ident :: $variant:ident) => {
|
||||
impl From<$from> for $enum {
|
||||
fn from(value: $from) -> Self {
|
||||
Self::$variant(value.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_extract_from {
|
||||
(ref $enum:ident :: as_int => $to:ty) => {
|
||||
impl ExtractFrom<&$enum> for $to {
|
||||
fn extract_from(value: &$enum, _token: private::Token) -> Option<Self> {
|
||||
value.as_int().and_then(|v| v.try_into().ok())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait ExtractFrom<Value>: Sized {
|
||||
fn extract_from(value: Value, _token: private::Token) -> Option<Self>;
|
||||
}
|
||||
|
||||
/// Represents a value of any type of the .RDP file format.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Value {
|
||||
/// Numerical value.
|
||||
Int(i64),
|
||||
/// String value.
|
||||
Str(String),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
if let Self::Str(value) = self {
|
||||
Some(value.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_int(&self) -> Option<i64> {
|
||||
if let Self::Int(value) = self {
|
||||
Some(*value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Value::Int(value) => write!(f, "{value}"),
|
||||
Value::Str(value) => write!(f, "\"{value}\""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(String => Value::Str);
|
||||
impl_from!(&str => Value::Str);
|
||||
impl_from!(u8 => Value::Int);
|
||||
impl_from!(u16 => Value::Int);
|
||||
impl_from!(u32 => Value::Int);
|
||||
impl_from!(i8 => Value::Int);
|
||||
impl_from!(i16 => Value::Int);
|
||||
impl_from!(i32 => Value::Int);
|
||||
impl_from!(i64 => Value::Int);
|
||||
impl_from!(bool => Value::Int);
|
||||
|
||||
impl_extract_from!(ref Value::as_int => u8);
|
||||
impl_extract_from!(ref Value::as_int => u16);
|
||||
impl_extract_from!(ref Value::as_int => u32);
|
||||
impl_extract_from!(ref Value::as_int => i8);
|
||||
impl_extract_from!(ref Value::as_int => i16);
|
||||
impl_extract_from!(ref Value::as_int => i32);
|
||||
impl_extract_from!(ref Value::as_int => i64);
|
||||
|
||||
impl<'a> ExtractFrom<&'a Value> for &'a str {
|
||||
fn extract_from(value: &'a Value, _token: private::Token) -> Option<Self> {
|
||||
value.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractFrom<&Value> for bool {
|
||||
fn extract_from(value: &Value, _token: private::Token) -> Option<Self> {
|
||||
value.as_int().map(|value| value != 0)
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub struct Token;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ironrdp-rdpfile"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "Parser and writer for .RDP file format"
|
||||
publish = false # TODO: publish
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,7 @@
|
||||
# IronRDP .RDP file
|
||||
|
||||
Loader and writer for the .RDP file format.
|
||||
|
||||
This crate is part of the [IronRDP] project.
|
||||
|
||||
[IronRDP]: https://github.com/Devolutions/IronRDP
|
||||
@@ -0,0 +1,126 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use ironrdp_propertyset::{PropertySet, Value};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ErrorKind {
|
||||
UnknownType { ty: String },
|
||||
InvalidValue { ty: String, value: String },
|
||||
MalformedLine { line: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
pub line: usize,
|
||||
}
|
||||
|
||||
impl core::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let line_number = self.line;
|
||||
|
||||
match &self.kind {
|
||||
ErrorKind::UnknownType { ty } => write!(f, "unknown type at line {line_number} ({ty})"),
|
||||
ErrorKind::InvalidValue { ty, value } => {
|
||||
write!(f, "invalid value at line {line_number} for type {ty} ({value})")
|
||||
}
|
||||
ErrorKind::MalformedLine { line } => write!(f, "malformed line at line {line_number} ({line})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(properties: &mut PropertySet, input: &str) -> Result<(), Vec<Error>> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (idx, line) in input.lines().enumerate() {
|
||||
let mut split = line.splitn(3, ':');
|
||||
|
||||
if let (Some(key), Some(ty), Some(value)) = (split.next(), split.next(), split.next()) {
|
||||
match ty {
|
||||
"i" => {
|
||||
if let Ok(value) = value.parse::<i64>() {
|
||||
properties.insert(key.to_owned(), value);
|
||||
} else {
|
||||
errors.push(Error {
|
||||
kind: ErrorKind::InvalidValue {
|
||||
ty: ty.to_owned(),
|
||||
value: value.to_owned(),
|
||||
},
|
||||
line: idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
"s" => {
|
||||
properties.insert(key.to_owned(), value);
|
||||
}
|
||||
_ => {
|
||||
errors.push(Error {
|
||||
kind: ErrorKind::UnknownType { ty: ty.to_owned() },
|
||||
line: idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(Error {
|
||||
kind: ErrorKind::MalformedLine { line: line.to_owned() },
|
||||
line: idx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParseResult {
|
||||
pub properties: PropertySet,
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> ParseResult {
|
||||
let mut properties = PropertySet::new();
|
||||
|
||||
let errors = match load(&mut properties, input) {
|
||||
Ok(()) => Vec::new(),
|
||||
Err(errors) => errors,
|
||||
};
|
||||
|
||||
ParseResult { properties, errors }
|
||||
}
|
||||
|
||||
pub fn write(properties: &PropertySet) -> String {
|
||||
let mut buf = String::new();
|
||||
|
||||
for (key, value) in properties.iter() {
|
||||
buf.push_str(key);
|
||||
|
||||
match value {
|
||||
Value::Int(value) => {
|
||||
buf.push_str(":i:");
|
||||
buf.push_str(&value.to_string());
|
||||
}
|
||||
Value::Str(value) => {
|
||||
buf.push_str(":s:");
|
||||
buf.push_str(value);
|
||||
}
|
||||
}
|
||||
|
||||
buf.push('\n');
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
@@ -45,6 +45,8 @@ ironrdp-input.path = "../ironrdp-input"
|
||||
ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath"
|
||||
ironrdp-rdpsnd.path = "../ironrdp-rdpsnd"
|
||||
ironrdp-session.path = "../ironrdp-session"
|
||||
ironrdp-propertyset.path = "../ironrdp-propertyset"
|
||||
ironrdp-rdpfile.path = "../ironrdp-rdpfile"
|
||||
png = "0.17"
|
||||
pretty_assertions = "1.4"
|
||||
proptest.workspace = true
|
||||
|
||||
@@ -19,6 +19,7 @@ mod graphics;
|
||||
mod input;
|
||||
mod pcb;
|
||||
mod pdu;
|
||||
mod propertyset;
|
||||
mod rdcleanpath;
|
||||
mod rdpsnd;
|
||||
mod server;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use expect_test::expect;
|
||||
use ironrdp_rdpfile::ParseResult;
|
||||
|
||||
const RDP_FILE_SAMPLE: &str = r#"remoteapplicationmode:i:0
|
||||
server port:i:3389
|
||||
promptcredentialonce:i:1
|
||||
full address:s:192.168.56.101
|
||||
alternate shell:s:|explorer
|
||||
remoteapplicationname:s:|explorer
|
||||
alternate full address:s:some.alternateaddress.ninja
|
||||
username:s:David
|
||||
ClearTextPassword:s:Devolutions123!
|
||||
MalformedLine:s
|
||||
UnknownType:z:10293"#;
|
||||
|
||||
const RDP_FILE_SAMPLE_2: &str = r#"remoteapplicationmode:i:50
|
||||
server port:i:4000
|
||||
full address:s:192.168.56.2"#;
|
||||
|
||||
#[test]
|
||||
fn parse_file() {
|
||||
let ParseResult { mut properties, errors } = ironrdp_rdpfile::parse(RDP_FILE_SAMPLE);
|
||||
|
||||
expect![[r#"
|
||||
{
|
||||
"ClearTextPassword": Str(
|
||||
"Devolutions123!",
|
||||
),
|
||||
"alternate full address": Str(
|
||||
"some.alternateaddress.ninja",
|
||||
),
|
||||
"alternate shell": Str(
|
||||
"|explorer",
|
||||
),
|
||||
"full address": Str(
|
||||
"192.168.56.101",
|
||||
),
|
||||
"promptcredentialonce": Int(
|
||||
1,
|
||||
),
|
||||
"remoteapplicationmode": Int(
|
||||
0,
|
||||
),
|
||||
"remoteapplicationname": Str(
|
||||
"|explorer",
|
||||
),
|
||||
"server port": Int(
|
||||
3389,
|
||||
),
|
||||
"username": Str(
|
||||
"David",
|
||||
),
|
||||
}
|
||||
"#]]
|
||||
.assert_debug_eq(&properties);
|
||||
|
||||
expect![[r#"
|
||||
[
|
||||
Error {
|
||||
kind: MalformedLine {
|
||||
line: "MalformedLine:s",
|
||||
},
|
||||
line: 9,
|
||||
},
|
||||
Error {
|
||||
kind: UnknownType {
|
||||
ty: "z",
|
||||
},
|
||||
line: 10,
|
||||
},
|
||||
]
|
||||
"#]]
|
||||
.assert_debug_eq(&errors);
|
||||
|
||||
// Verify the `get` operation.
|
||||
assert_eq!(properties.get::<bool>("remoteapplicationmode"), Some(false));
|
||||
assert_eq!(properties.get::<bool>("promptcredentialonce"), Some(true));
|
||||
assert_eq!(properties.get::<bool>("absentproperty"), None);
|
||||
assert_eq!(properties.get::<i64>("server port"), Some(3389));
|
||||
assert_eq!(properties.get::<&str>("full address"), Some("192.168.56.101"));
|
||||
|
||||
// Merge another file.
|
||||
ironrdp_rdpfile::load(&mut properties, RDP_FILE_SAMPLE_2).expect("valid rdp file format");
|
||||
|
||||
expect![[r#"
|
||||
{
|
||||
"ClearTextPassword": Str(
|
||||
"Devolutions123!",
|
||||
),
|
||||
"alternate full address": Str(
|
||||
"some.alternateaddress.ninja",
|
||||
),
|
||||
"alternate shell": Str(
|
||||
"|explorer",
|
||||
),
|
||||
"full address": Str(
|
||||
"192.168.56.2",
|
||||
),
|
||||
"promptcredentialonce": Int(
|
||||
1,
|
||||
),
|
||||
"remoteapplicationmode": Int(
|
||||
50,
|
||||
),
|
||||
"remoteapplicationname": Str(
|
||||
"|explorer",
|
||||
),
|
||||
"server port": Int(
|
||||
4000,
|
||||
),
|
||||
"username": Str(
|
||||
"David",
|
||||
),
|
||||
}
|
||||
"#]]
|
||||
.assert_debug_eq(&properties);
|
||||
|
||||
// Anything that is not 0 is considered to be 'true'.
|
||||
assert_eq!(properties.get::<bool>("remoteapplicationmode"), Some(true));
|
||||
}
|
||||
Reference in New Issue
Block a user