From c710909a3cb64808bfc024bbe3f326565268871e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Cortier?= <3809077+CBenoit@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:10:04 -0400 Subject: [PATCH] feat: inital support for .RDP files (#862) This is paving the way for .rdp file support. Issue: ARC-339 Issue: ARC-355 --- ARCHITECTURE.md | 12 ++ Cargo.lock | 26 +++ crates/ironrdp-cfg/Cargo.toml | 23 +++ crates/ironrdp-cfg/README.md | 7 + crates/ironrdp-cfg/src/lib.rs | 63 +++++++ crates/ironrdp-client/Cargo.toml | 3 + crates/ironrdp-client/src/config.rs | 50 ++++- crates/ironrdp-error/src/lib.rs | 1 + crates/ironrdp-propertyset/Cargo.toml | 23 +++ crates/ironrdp-propertyset/README.md | 7 + crates/ironrdp-propertyset/src/lib.rs | 171 ++++++++++++++++++ crates/ironrdp-rdpfile/Cargo.toml | 23 +++ crates/ironrdp-rdpfile/README.md | 7 + crates/ironrdp-rdpfile/src/lib.rs | 126 +++++++++++++ crates/ironrdp-testsuite-core/Cargo.toml | 2 + crates/ironrdp-testsuite-core/tests/main.rs | 1 + .../tests/propertyset.rs | 120 ++++++++++++ 17 files changed, 657 insertions(+), 8 deletions(-) create mode 100644 crates/ironrdp-cfg/Cargo.toml create mode 100644 crates/ironrdp-cfg/README.md create mode 100644 crates/ironrdp-cfg/src/lib.rs create mode 100644 crates/ironrdp-propertyset/Cargo.toml create mode 100644 crates/ironrdp-propertyset/README.md create mode 100644 crates/ironrdp-propertyset/src/lib.rs create mode 100644 crates/ironrdp-rdpfile/Cargo.toml create mode 100644 crates/ironrdp-rdpfile/README.md create mode 100644 crates/ironrdp-rdpfile/src/lib.rs create mode 100644 crates/ironrdp-testsuite-core/tests/propertyset.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a660f9fe..4bfac02d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index a8f08c78..503e76ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/ironrdp-cfg/Cargo.toml b/crates/ironrdp-cfg/Cargo.toml new file mode 100644 index 00000000..270e330f --- /dev/null +++ b/crates/ironrdp-cfg/Cargo.toml @@ -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 diff --git a/crates/ironrdp-cfg/README.md b/crates/ironrdp-cfg/README.md new file mode 100644 index 00000000..6cdd08d0 --- /dev/null +++ b/crates/ironrdp-cfg/README.md @@ -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 diff --git a/crates/ironrdp-cfg/src/lib.rs b/crates/ironrdp-cfg/src/lib.rs new file mode 100644 index 00000000..e9a00061 --- /dev/null +++ b/crates/ironrdp-cfg/src/lib.rs @@ -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; + + 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 { + self.get::("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") + } +} diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index c8c82990..a9cfc940 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -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"] } diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index ab5841d0..93f8a830 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -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, + + /// Path to a .rdp file to read the configuration from. + #[clap(long)] + rdp_file: Option, + /// A file with IronRDP client logs #[clap(short, long)] log_file: Option, - /// An address on which the client will connect. - destination: Option, - /// A target RDP server user name #[clap(short, long)] username: Option, @@ -273,21 +278,46 @@ struct Args { #[clap(long, num_args = 1.., value_delimiter = ',')] codecs: Vec, - /// Add DVC channel named pipe proxy. - /// the format is = - /// 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 `=`, 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). + /// `` will automatically be prefixed with `\\.\pipe\` on Windows. #[clap(long)] dvc_proxy: Vec, } impl Config { pub fn parse_args() -> anyhow::Result { + 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() diff --git a/crates/ironrdp-error/src/lib.rs b/crates/ironrdp-error/src/lib.rs index b57c3e69..3c731ec5 100644 --- a/crates/ironrdp-error/src/lib.rs +++ b/crates/ironrdp-error/src/lib.rs @@ -4,6 +4,7 @@ #[cfg(feature = "alloc")] extern crate alloc; + #[cfg(feature = "alloc")] use alloc::boxed::Box; use core::fmt; diff --git a/crates/ironrdp-propertyset/Cargo.toml b/crates/ironrdp-propertyset/Cargo.toml new file mode 100644 index 00000000..4cb05bb6 --- /dev/null +++ b/crates/ironrdp-propertyset/Cargo.toml @@ -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 diff --git a/crates/ironrdp-propertyset/README.md b/crates/ironrdp-propertyset/README.md new file mode 100644 index 00000000..4587bba6 --- /dev/null +++ b/crates/ironrdp-propertyset/README.md @@ -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 diff --git a/crates/ironrdp-propertyset/src/lib.rs b/crates/ironrdp-propertyset/src/lib.rs new file mode 100644 index 00000000..8e33e6fa --- /dev/null +++ b/crates/ironrdp-propertyset/src/lib.rs @@ -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, +} + +impl PropertySet { + pub fn new() -> Self { + Self::default() + } + + pub fn insert(&mut self, key: impl Into, value: impl Into) -> Option { + 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 { + 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 { + 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 { + self.inner.iter() + } +} + +impl IntoIterator for PropertySet { + type Item = (Key, Value); + + type IntoIter = alloc::collections::btree_map::IntoIter; + + 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 { + value.as_int().and_then(|v| v.try_into().ok()) + } + } + }; +} + +pub trait ExtractFrom: Sized { + fn extract_from(value: Value, _token: private::Token) -> Option; +} + +/// 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 { + 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 { + value.as_str() + } +} + +impl ExtractFrom<&Value> for bool { + fn extract_from(value: &Value, _token: private::Token) -> Option { + value.as_int().map(|value| value != 0) + } +} + +mod private { + pub struct Token; +} diff --git a/crates/ironrdp-rdpfile/Cargo.toml b/crates/ironrdp-rdpfile/Cargo.toml new file mode 100644 index 00000000..f250a9df --- /dev/null +++ b/crates/ironrdp-rdpfile/Cargo.toml @@ -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 diff --git a/crates/ironrdp-rdpfile/README.md b/crates/ironrdp-rdpfile/README.md new file mode 100644 index 00000000..3d9d8483 --- /dev/null +++ b/crates/ironrdp-rdpfile/README.md @@ -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 diff --git a/crates/ironrdp-rdpfile/src/lib.rs b/crates/ironrdp-rdpfile/src/lib.rs new file mode 100644 index 00000000..3f78c910 --- /dev/null +++ b/crates/ironrdp-rdpfile/src/lib.rs @@ -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> { + 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::() { + 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, +} + +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 +} diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 0cc87ae5..5c1f3fe9 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -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 diff --git a/crates/ironrdp-testsuite-core/tests/main.rs b/crates/ironrdp-testsuite-core/tests/main.rs index 474a5053..ad9456bc 100644 --- a/crates/ironrdp-testsuite-core/tests/main.rs +++ b/crates/ironrdp-testsuite-core/tests/main.rs @@ -19,6 +19,7 @@ mod graphics; mod input; mod pcb; mod pdu; +mod propertyset; mod rdcleanpath; mod rdpsnd; mod server; diff --git a/crates/ironrdp-testsuite-core/tests/propertyset.rs b/crates/ironrdp-testsuite-core/tests/propertyset.rs new file mode 100644 index 00000000..363451e2 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/propertyset.rs @@ -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::("remoteapplicationmode"), Some(false)); + assert_eq!(properties.get::("promptcredentialonce"), Some(true)); + assert_eq!(properties.get::("absentproperty"), None); + assert_eq!(properties.get::("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::("remoteapplicationmode"), Some(true)); +}