Skip to content

Commit

Permalink
Support XML in binary payloads
Browse files Browse the repository at this point in the history
  • Loading branch information
einarmo committed Feb 27, 2025
1 parent 29d3c88 commit 851c904
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 10 deletions.
8 changes: 4 additions & 4 deletions async-opcua-types/src/byte_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ impl ByteString {
self.is_null() || self.is_empty()
}

/// Creates a byte string from a Base64 encoded string
/// Creates a byte string from a base64 encoded string
pub fn from_base64(data: &str) -> Option<ByteString> {
if let Ok(bytes) = STANDARD.decode(data) {
Some(Self::from(bytes))
Expand All @@ -250,17 +250,17 @@ impl ByteString {
}
}

/// Creates a byte string from a Base64 encoded string, ignoring whitespace.
/// Creates a byte string from a base64 encoded string, ignoring whitespace.
pub fn from_base64_ignore_whitespace(mut data: String) -> Option<ByteString> {
data.retain(|c| !['\n', ' ', '\t', '\r'].contains(&c));
data.retain(|c| !c.is_whitespace());
if let Ok(bytes) = STANDARD.decode(&data) {
Some(Self::from(bytes))
} else {
None
}
}

/// Encodes the bytestring as a Base64 encoded string
/// Encodes the bytestring as a base64 encoded string
pub fn as_base64(&self) -> String {
// Base64 encodes the byte string so it can be represented as a string
if let Some(ref value) = self.value {
Expand Down
6 changes: 6 additions & 0 deletions async-opcua-types/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,12 @@ pub fn read_f64<R: Read + ?Sized>(stream: &mut R) -> EncodingResult<f64> {
Ok(LittleEndian::read_f64(&buf))
}

/// Skip `bytes` bytes in the stream.
pub fn skip_bytes<R: Read + ?Sized>(stream: &mut R, bytes: u64) -> EncodingResult<()> {
std::io::copy(&mut stream.take(bytes), &mut std::io::sink())?;
Ok(())
}

#[cfg(test)]
mod tests {
use std::sync::Arc;
Expand Down
29 changes: 25 additions & 4 deletions async-opcua-types/src/extension_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ use std::{
io::{Read, Write},
};

use log::warn;

use crate::{write_i32, write_u8, Error, ExpandedMessageInfo, ExpandedNodeId};

use super::{
Expand Down Expand Up @@ -541,8 +539,31 @@ impl BinaryDecodable for ExtensionObject {
}
}
0x2 => {
warn!("Unsupported extension object encoding: XMLElement");
None
#[cfg(feature = "xml")]
{
let body = crate::UAString::decode(stream, ctx)?;
let Some(body) = body.value() else {
return Ok(ExtensionObject::null());
};
let mut cursor = std::io::Cursor::new(body.as_bytes());
let mut inner_stream =
crate::xml::XmlStreamReader::new(&mut cursor as &mut dyn Read);
if crate::xml::enter_first_tag(&mut inner_stream)? {
Some(ctx.load_from_xml(&node_id, &mut inner_stream)?)
} else {
None
}
}

#[cfg(not(feature = "xml"))]
{
log::warn!("XML feature is not enabled, deserializing XML payloads in JSON extension objects is not supported");
let size = i32::decode(stream, ctx)?;
if size > 0 {
crate::encoding::skip_bytes(stream, size as u64)?;
}
None
}
}
_ => {
return Err(Error::decoding(format!(
Expand Down
42 changes: 40 additions & 2 deletions async-opcua-types/src/tests/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use std::{io::Cursor, str::FromStr};
use std::{
io::{Cursor, Write},
str::FromStr,
};

use opcua_xml::XmlStreamWriter;

use crate::{
encoding::{BinaryDecodable, DecodingOptions},
string::UAString,
tests::*,
Array, ByteString, ContextOwned, DataValue, DateTime, DepthGauge, DiagnosticInfo,
write_u8, Array, ByteString, ContextOwned, DataValue, DateTime, DepthGauge, DiagnosticInfo,
EUInformation, EncodingMask, ExpandedNodeId, ExtensionObject, Guid, LocalizedText,
NamespaceMap, NodeId, ObjectId, QualifiedName, Variant, VariantScalarTypeId, XmlElement,
};
Expand Down Expand Up @@ -655,3 +660,36 @@ fn test_custom_union_nullable() {
assert_eq!(st.byte_len(&ctx), 4);
serialize_test(st);
}

#[test]
fn test_xml_in_binary() {
// Bit tricky to test since we don't support encoding extension objects as XML.
// We may actually allow this to some extent in the future, using the same mechanism
// we'll use for decoding fallback.
let mut buf = Vec::new();
let mut stream = Cursor::new(&mut buf);
let ctx_f = ContextOwned::default();
let ctx = ctx_f.context();
let rf = EUInformation {
namespace_uri: "https://my.namespace.uri".into(),
unit_id: 1,
display_name: LocalizedText::new("en", "MyUnit"),
description: LocalizedText::new("en", "MyDesc"),
};
let id: NodeId = ObjectId::EUInformation_Encoding_DefaultXml.into();
id.encode(&mut stream, &ctx).unwrap();

write_u8(&mut stream, 0x2).unwrap();
let mut xml_buf = Vec::new();
let mut xml_stream = Cursor::new(&mut xml_buf);
xml_stream.write(b"<EUInformation>").unwrap();
let mut xml_writer = XmlStreamWriter::new(&mut xml_stream as &mut dyn Write);
crate::xml::XmlEncodable::encode(&rf, &mut xml_writer, &ctx).unwrap();
xml_stream.write(b"</EUInformation>").unwrap();
let str = UAString::from(String::from_utf8(xml_buf).unwrap());
str.encode(&mut stream, &ctx).unwrap();

stream.set_position(0);
let decoded = ExtensionObject::decode(&mut stream, &ctx).unwrap();
assert_eq!(decoded.inner_as::<EUInformation>().unwrap(), &rf);
}

0 comments on commit 851c904

Please sign in to comment.