use rand::RngCore as _;
use crate::binding::{Endpoint, SsoResponseEndpoint};
use crate::crypto::cert::X509Certificate;
use crate::crypto::keypair::KeyPair;
use crate::dsig::algorithms::{C14nAlgorithm, DigestAlgorithm, SignatureAlgorithm};
use crate::error::Error;
use crate::nameid::NameIdFormat;
use crate::xml::emit::emit_document;
use crate::xml::parse::{Document, Element, Node, QName};
#[cfg(feature = "xmlenc")]
use crate::xmlenc::algorithms::DataEncryptionAlgorithm;
use super::{MetadataContact, MetadataExtras, MetadataOrganization};
pub(super) const MD_NS: &str = "urn:oasis:names:tc:SAML:2.0:metadata";
pub(super) const DS_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
pub(super) const SAML2_PROTOCOL: &str = "urn:oasis:names:tc:SAML:2.0:protocol";
pub struct SpMetadataInputs<'a> {
pub entity_id: &'a str,
pub acs: &'a [SsoResponseEndpoint],
pub slo: &'a [Endpoint],
pub name_id_formats: &'a [NameIdFormat],
pub signing_cert: Option<&'a X509Certificate>,
#[cfg(feature = "xmlenc")]
pub encryption_cert: Option<&'a X509Certificate>,
#[cfg(feature = "xmlenc")]
pub encryption_algorithms: &'a [DataEncryptionAlgorithm],
pub authn_requests_signed: bool,
pub want_assertions_signed: bool,
pub valid_until: Option<std::time::SystemTime>,
pub cache_duration: Option<std::time::Duration>,
pub extras: Option<&'a MetadataExtras>,
}
pub fn emit_sp_metadata(
inputs: &SpMetadataInputs<'_>,
signer: Option<(&KeyPair, SignatureAlgorithm, DigestAlgorithm, C14nAlgorithm)>,
) -> Result<String, Error> {
let entity_descriptor_id = generate_id();
let root = build_sp_entity_descriptor(inputs, &entity_descriptor_id)?;
let final_root = if let Some((key, sig_alg, digest, c14n)) = signer {
let unsigned_doc = Document::new(root.clone())?;
crate::dsig::sign::sign_element(
root,
&unsigned_doc,
crate::dsig::sign::SignOptions {
signing_key: key,
sig_alg,
digest_alg: digest,
c14n_alg: c14n,
inclusive_namespaces: &[],
include_x509_cert: true,
},
)?
} else {
root
};
let doc = Document::new(final_root)?;
emit_document(&doc)
}
fn build_sp_entity_descriptor(
inputs: &SpMetadataInputs<'_>,
entity_descriptor_id: &str,
) -> Result<Element, Error> {
let mut sp_descriptor =
Element::build(md_qname("SPSSODescriptor"))
.with_attribute(
QName::new(None, "protocolSupportEnumeration"),
SAML2_PROTOCOL,
)
.with_attribute(
QName::new(None, "AuthnRequestsSigned"),
bool_str(inputs.authn_requests_signed),
)
.with_attribute(
QName::new(None, "WantAssertionsSigned"),
bool_str(inputs.want_assertions_signed),
);
if let Some(cert) = inputs.signing_cert {
sp_descriptor = sp_descriptor
.with_child(Node::Element(build_signing_key_descriptor(cert)));
}
#[cfg(feature = "xmlenc")]
if let Some(cert) = inputs.encryption_cert {
sp_descriptor = sp_descriptor.with_child(Node::Element(
build_encryption_key_descriptor(cert, inputs.encryption_algorithms),
));
}
for fmt in inputs.name_id_formats {
sp_descriptor = sp_descriptor.with_child(Node::Element(
Element::build(md_qname("NameIDFormat"))
.with_text(fmt.as_uri().to_owned())
.finish(),
));
}
for endpoint in inputs.acs {
sp_descriptor = sp_descriptor
.with_child(Node::Element(build_acs_endpoint(endpoint)));
}
for endpoint in inputs.slo {
sp_descriptor = sp_descriptor
.with_child(Node::Element(build_slo_endpoint(endpoint)));
}
let sp_descriptor = sp_descriptor.finish();
let mut entity_descriptor =
Element::build(md_qname("EntityDescriptor"))
.with_namespace(Some("md".to_owned()), MD_NS)
.with_namespace(Some("ds".to_owned()), DS_NS)
.with_attribute(QName::new(None, "entityID"), inputs.entity_id.to_owned())
.with_attribute(QName::new(None, "ID"), entity_descriptor_id.to_owned());
if let Some(valid_until) = inputs.valid_until {
entity_descriptor = entity_descriptor.with_attribute(
QName::new(None, "validUntil"),
crate::time::format_xs_datetime(valid_until)?,
);
}
if let Some(cache_duration) = inputs.cache_duration {
entity_descriptor = entity_descriptor.with_attribute(
QName::new(None, "cacheDuration"),
format_cache_duration(cache_duration),
);
}
entity_descriptor = entity_descriptor.with_child(Node::Element(sp_descriptor));
if let Some(extras) = inputs.extras {
entity_descriptor = append_extras(entity_descriptor, extras);
}
Ok(entity_descriptor.finish())
}
pub(super) fn generate_id() -> String {
let mut bytes = [0u8; 16];
rand::rng().fill_bytes(&mut bytes);
let capacity = bytes
.len()
.checked_mul(2)
.and_then(|n| n.checked_add(1))
.unwrap_or(0);
let mut out = String::with_capacity(capacity);
out.push('_');
for b in bytes {
let hi = b >> 4;
let lo = b & 0x0f;
out.push(hex_nibble(hi));
out.push(hex_nibble(lo));
}
out
}
fn hex_nibble(n: u8) -> char {
match n {
0..=9 => char::from(b'0'.saturating_add(n)),
10..=15 => char::from(b'a'.saturating_add(n.saturating_sub(10))),
_ => '0',
}
}
pub(super) fn format_cache_duration(d: std::time::Duration) -> String {
format!("PT{}S", d.as_secs())
}
pub(super) fn bool_str(b: bool) -> &'static str {
if b { "true" } else { "false" }
}
pub(super) fn md_qname(local: &str) -> QName {
QName::new(Some(MD_NS.to_owned()), local)
}
fn ds_qname(local: &str) -> QName {
QName::new(Some(DS_NS.to_owned()), local)
}
pub(super) fn build_signing_key_descriptor(cert: &X509Certificate) -> Element {
Element::build(md_qname("KeyDescriptor"))
.with_attribute(QName::new(None, "use"), "signing")
.with_child(Node::Element(build_key_info_x509(cert)))
.finish()
}
#[cfg(feature = "xmlenc")]
pub(super) fn build_encryption_key_descriptor(
cert: &X509Certificate,
encryption_algorithms: &[DataEncryptionAlgorithm],
) -> Element {
let mut builder = Element::build(md_qname("KeyDescriptor"))
.with_attribute(QName::new(None, "use"), "encryption")
.with_child(Node::Element(build_key_info_x509(cert)));
for alg in encryption_algorithms {
builder = builder.with_child(Node::Element(
Element::build(md_qname("EncryptionMethod"))
.with_attribute(QName::new(None, "Algorithm"), alg.uri())
.finish(),
));
}
builder.finish()
}
fn build_key_info_x509(cert: &X509Certificate) -> Element {
let x509_cert = Element::build(ds_qname("X509Certificate"))
.with_text(cert.to_base64_x509())
.finish();
let x509_data = Element::build(ds_qname("X509Data"))
.with_child(Node::Element(x509_cert))
.finish();
Element::build(ds_qname("KeyInfo"))
.with_child(Node::Element(x509_data))
.finish()
}
fn build_acs_endpoint(endpoint: &SsoResponseEndpoint) -> Element {
let mut builder = Element::build(md_qname("AssertionConsumerService"))
.with_attribute(QName::new(None, "Binding"), endpoint.binding.uri())
.with_attribute(QName::new(None, "Location"), endpoint.url.clone());
if let Some(index) = endpoint.index {
builder = builder.with_attribute(QName::new(None, "index"), index.to_string());
}
if endpoint.is_default {
builder = builder.with_attribute(QName::new(None, "isDefault"), "true");
}
builder.finish()
}
fn build_slo_endpoint(endpoint: &Endpoint) -> Element {
Element::build(md_qname("SingleLogoutService"))
.with_attribute(QName::new(None, "Binding"), endpoint.binding.uri())
.with_attribute(QName::new(None, "Location"), endpoint.url.clone())
.finish()
}
pub(super) fn append_extras(
mut entity_descriptor: crate::xml::emit::ElementBuilder,
extras: &MetadataExtras,
) -> crate::xml::emit::ElementBuilder {
if let Some(org) = &extras.organization {
entity_descriptor =
entity_descriptor.with_child(Node::Element(build_organization(org)));
}
for contact in &extras.contacts {
entity_descriptor =
entity_descriptor.with_child(Node::Element(build_contact_person(contact)));
}
entity_descriptor
}
fn build_organization(org: &MetadataOrganization) -> Element {
let lang_attr = QName::new(Some(XML_NS.to_owned()), "lang");
let name = Element::build(md_qname("OrganizationName"))
.with_attribute(lang_attr.clone(), org.language.clone())
.with_text(org.name.clone())
.finish();
let display_name = Element::build(md_qname("OrganizationDisplayName"))
.with_attribute(lang_attr.clone(), org.language.clone())
.with_text(org.display_name.clone())
.finish();
let url = Element::build(md_qname("OrganizationURL"))
.with_attribute(lang_attr, org.language.clone())
.with_text(org.url.clone())
.finish();
Element::build(md_qname("Organization"))
.with_child(Node::Element(name))
.with_child(Node::Element(display_name))
.with_child(Node::Element(url))
.finish()
}
fn build_contact_person(contact: &MetadataContact) -> Element {
let mut builder = Element::build(md_qname("ContactPerson"))
.with_attribute(
QName::new(None, "contactType"),
contact.contact_type.as_str(),
);
if let Some(company) = &contact.company {
builder = builder.with_child(Node::Element(
Element::build(md_qname("Company"))
.with_text(company.clone())
.finish(),
));
}
if let Some(given_name) = &contact.given_name {
builder = builder.with_child(Node::Element(
Element::build(md_qname("GivenName"))
.with_text(given_name.clone())
.finish(),
));
}
if let Some(surname) = &contact.surname {
builder = builder.with_child(Node::Element(
Element::build(md_qname("SurName"))
.with_text(surname.clone())
.finish(),
));
}
for email in &contact.email_addresses {
builder = builder.with_child(Node::Element(
Element::build(md_qname("EmailAddress"))
.with_text(email.clone())
.finish(),
));
}
for phone in &contact.telephone_numbers {
builder = builder.with_child(Node::Element(
Element::build(md_qname("TelephoneNumber"))
.with_text(phone.clone())
.finish(),
));
}
builder.finish()
}
#[cfg(test)]
#[cfg(feature = "xmlenc")]
mod tests {
use super::*;
use crate::binding::{Binding, Endpoint, SsoResponseEndpoint};
use crate::crypto::cert::X509Certificate;
use crate::crypto::cert::test_vectors::{RSA_CERT_PEM, RSA_KEY_PKCS8_PEM};
use crate::dsig::algorithms::{C14nAlgorithm, DigestAlgorithm, SignatureAlgorithm};
use crate::metadata::{
MetadataContact, MetadataContactType, MetadataExtras, MetadataOrganization,
};
use crate::xml::parse::Document;
use std::time::{Duration, SystemTime};
fn rsa_cert() -> X509Certificate {
X509Certificate::from_pem(RSA_CERT_PEM).unwrap()
}
fn signing_keypair() -> KeyPair {
KeyPair::from_pkcs8_pem(RSA_KEY_PKCS8_PEM)
.unwrap()
.with_certificate(rsa_cert())
}
fn baseline_inputs<'a>(
cert: &'a X509Certificate,
acs: &'a [SsoResponseEndpoint],
slo: &'a [Endpoint],
formats: &'a [NameIdFormat],
algos: &'a [DataEncryptionAlgorithm],
) -> SpMetadataInputs<'a> {
SpMetadataInputs {
entity_id: "https://sp.example.com/saml",
acs,
slo,
name_id_formats: formats,
signing_cert: Some(cert),
encryption_cert: Some(cert),
encryption_algorithms: algos,
authn_requests_signed: true,
want_assertions_signed: true,
valid_until: None,
cache_duration: None,
extras: None,
}
}
#[test]
fn emits_well_formed_entity_descriptor_with_expected_shape() {
let cert = rsa_cert();
let acs = [SsoResponseEndpoint::post(
"https://sp.example.com/acs/post",
0,
true,
)];
let slo = [Endpoint::redirect("https://sp.example.com/slo", 0, false)];
let formats = [NameIdFormat::EmailAddress, NameIdFormat::Persistent];
let algos = [
DataEncryptionAlgorithm::Aes256Gcm,
DataEncryptionAlgorithm::Aes128Gcm,
];
let inputs = baseline_inputs(&cert, &acs, &slo, &formats, &algos);
let xml = emit_sp_metadata(&inputs, None).expect("emit");
let doc = Document::parse(xml.as_bytes()).expect("re-parse");
let root = doc.root();
assert_eq!(root.qname().namespace(), Some(MD_NS));
assert_eq!(root.qname().local(), "EntityDescriptor");
assert_eq!(
root.attribute(None, "entityID"),
Some("https://sp.example.com/saml")
);
let id_attr = root.attribute(None, "ID").expect("ID attribute");
assert!(id_attr.starts_with('_'), "ID = {id_attr}");
let sp_desc = root
.child_element(Some(MD_NS), "SPSSODescriptor")
.expect("SPSSODescriptor present");
assert_eq!(
sp_desc.attribute(None, "protocolSupportEnumeration"),
Some(SAML2_PROTOCOL)
);
assert_eq!(sp_desc.attribute(None, "AuthnRequestsSigned"), Some("true"));
assert_eq!(sp_desc.attribute(None, "WantAssertionsSigned"), Some("true"));
let key_descriptors: Vec<_> = sp_desc
.all_child_elements(Some(MD_NS), "KeyDescriptor")
.collect();
assert_eq!(key_descriptors.len(), 2);
assert_eq!(key_descriptors[0].attribute(None, "use"), Some("signing"));
assert_eq!(
key_descriptors[1].attribute(None, "use"),
Some("encryption")
);
let enc_methods: Vec<_> = key_descriptors[1]
.all_child_elements(Some(MD_NS), "EncryptionMethod")
.collect();
assert_eq!(enc_methods.len(), 2);
assert_eq!(
enc_methods[0].attribute(None, "Algorithm"),
Some(DataEncryptionAlgorithm::Aes256Gcm.uri())
);
assert_eq!(
enc_methods[1].attribute(None, "Algorithm"),
Some(DataEncryptionAlgorithm::Aes128Gcm.uri())
);
let name_id_formats: Vec<_> = sp_desc
.all_child_elements(Some(MD_NS), "NameIDFormat")
.map(Element::text_content)
.collect();
assert_eq!(
name_id_formats,
vec![
NameIdFormat::EmailAddress.as_uri().to_owned(),
NameIdFormat::Persistent.as_uri().to_owned(),
]
);
let acs_elements: Vec<_> = sp_desc
.all_child_elements(Some(MD_NS), "AssertionConsumerService")
.collect();
assert_eq!(acs_elements.len(), 1);
assert_eq!(
acs_elements[0].attribute(None, "Binding"),
Some(Binding::HttpPost.uri())
);
assert_eq!(
acs_elements[0].attribute(None, "Location"),
Some("https://sp.example.com/acs/post")
);
assert_eq!(acs_elements[0].attribute(None, "index"), Some("0"));
assert_eq!(acs_elements[0].attribute(None, "isDefault"), Some("true"));
let slo_elements: Vec<_> = sp_desc
.all_child_elements(Some(MD_NS), "SingleLogoutService")
.collect();
assert_eq!(slo_elements.len(), 1);
assert_eq!(
slo_elements[0].attribute(None, "Binding"),
Some(Binding::HttpRedirect.uri())
);
assert_eq!(
slo_elements[0].attribute(None, "Location"),
Some("https://sp.example.com/slo")
);
}
#[test]
fn multiple_acs_endpoints_emit_index_and_is_default() {
let cert = rsa_cert();
let acs = [
SsoResponseEndpoint::post("https://sp.example.com/acs/post", 0, true),
SsoResponseEndpoint::artifact("https://sp.example.com/acs/art", 1, false),
SsoResponseEndpoint::post("https://sp.example.com/acs/extra", 2, false),
];
let formats = [NameIdFormat::Transient];
let algos: [DataEncryptionAlgorithm; 0] = [];
let inputs = baseline_inputs(&cert, &acs, &[], &formats, &algos);
let xml = emit_sp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let sp_desc = doc
.root()
.child_element(Some(MD_NS), "SPSSODescriptor")
.unwrap();
let acs_elements: Vec<_> = sp_desc
.all_child_elements(Some(MD_NS), "AssertionConsumerService")
.collect();
assert_eq!(acs_elements.len(), 3);
assert_eq!(acs_elements[0].attribute(None, "index"), Some("0"));
assert_eq!(acs_elements[1].attribute(None, "index"), Some("1"));
assert_eq!(acs_elements[2].attribute(None, "index"), Some("2"));
assert_eq!(acs_elements[0].attribute(None, "isDefault"), Some("true"));
assert_eq!(acs_elements[1].attribute(None, "isDefault"), None);
assert_eq!(acs_elements[2].attribute(None, "isDefault"), None);
assert_eq!(
acs_elements[0].attribute(None, "Binding"),
Some(Binding::HttpPost.uri())
);
assert_eq!(
acs_elements[1].attribute(None, "Binding"),
Some(Binding::HttpArtifact.uri())
);
assert_eq!(
acs_elements[2].attribute(None, "Binding"),
Some(Binding::HttpPost.uri())
);
}
#[test]
fn valid_until_and_cache_duration_emit_attributes() {
let cert = rsa_cert();
let acs = [SsoResponseEndpoint::post("https://sp.example.com/acs", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let mut inputs = baseline_inputs(&cert, &acs, &[], &formats, &algos);
let valid_until =
SystemTime::UNIX_EPOCH + Duration::from_secs(2_000_000_000); inputs.valid_until = Some(valid_until);
inputs.cache_duration = Some(Duration::from_hours(1));
let xml = emit_sp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let vu = root.attribute(None, "validUntil").expect("validUntil");
let parsed = crate::time::parse_xs_datetime(vu).expect("parse");
assert_eq!(parsed, valid_until);
assert_eq!(root.attribute(None, "cacheDuration"), Some("PT3600S"));
}
#[test]
fn extras_emit_organization_and_contact_person() {
let cert = rsa_cert();
let acs = [SsoResponseEndpoint::post("https://sp.example.com/acs", 0, true)];
let formats = [NameIdFormat::EmailAddress];
let algos: [DataEncryptionAlgorithm; 0] = [];
let extras = MetadataExtras {
organization: Some(MetadataOrganization {
name: "Example Corp".into(),
display_name: "Example Corporation".into(),
url: "https://example.com".into(),
language: "en".into(),
}),
contacts: vec![MetadataContact {
contact_type: MetadataContactType::Technical,
given_name: Some("Alex".into()),
surname: Some("Operator".into()),
email_addresses: vec!["sso-admin@example.com".into()],
telephone_numbers: vec!["+15551234567".into()],
company: Some("Example Corp".into()),
}],
};
let mut inputs = baseline_inputs(&cert, &acs, &[], &formats, &algos);
inputs.extras = Some(&extras);
let xml = emit_sp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let org = root
.child_element(Some(MD_NS), "Organization")
.expect("Organization");
let name = org
.child_element(Some(MD_NS), "OrganizationName")
.expect("OrganizationName");
assert_eq!(name.text_content(), "Example Corp");
assert_eq!(name.attribute(Some(XML_NS), "lang"), Some("en"));
let display = org
.child_element(Some(MD_NS), "OrganizationDisplayName")
.expect("OrganizationDisplayName");
assert_eq!(display.text_content(), "Example Corporation");
let url = org
.child_element(Some(MD_NS), "OrganizationURL")
.expect("OrganizationURL");
assert_eq!(url.text_content(), "https://example.com");
let contact = root
.child_element(Some(MD_NS), "ContactPerson")
.expect("ContactPerson");
assert_eq!(contact.attribute(None, "contactType"), Some("technical"));
assert_eq!(
contact
.child_element(Some(MD_NS), "Company")
.map(Element::text_content),
Some("Example Corp".to_owned())
);
assert_eq!(
contact
.child_element(Some(MD_NS), "GivenName")
.map(Element::text_content),
Some("Alex".to_owned())
);
assert_eq!(
contact
.child_element(Some(MD_NS), "SurName")
.map(Element::text_content),
Some("Operator".to_owned())
);
assert_eq!(
contact
.child_element(Some(MD_NS), "EmailAddress")
.map(Element::text_content),
Some("sso-admin@example.com".to_owned())
);
assert_eq!(
contact
.child_element(Some(MD_NS), "TelephoneNumber")
.map(Element::text_content),
Some("+15551234567".to_owned())
);
}
#[test]
fn signed_metadata_carries_ds_signature_as_first_child() {
let cert = rsa_cert();
let acs = [SsoResponseEndpoint::post(
"https://sp.example.com/acs",
0,
true,
)];
let formats = [NameIdFormat::EmailAddress];
let algos: [DataEncryptionAlgorithm; 0] = [];
let inputs = baseline_inputs(&cert, &acs, &[], &formats, &algos);
let kp = signing_keypair();
let xml = emit_sp_metadata(
&inputs,
Some((
&kp,
SignatureAlgorithm::RsaSha256,
DigestAlgorithm::Sha256,
C14nAlgorithm::ExclusiveCanonical,
)),
)
.unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let root = doc.root();
let children: Vec<_> = root.child_elements().collect();
assert!(!children.is_empty());
assert_eq!(children[0].qname().namespace(), Some(DS_NS));
assert_eq!(children[0].qname().local(), "Signature");
let id_attr = root.attribute(None, "ID").expect("ID");
let expected_uri = format!("#{id_attr}");
let signed_info = children[0]
.child_element(Some(DS_NS), "SignedInfo")
.unwrap();
let reference = signed_info
.child_element(Some(DS_NS), "Reference")
.unwrap();
assert_eq!(reference.attribute(None, "URI"), Some(expected_uri.as_str()));
assert!(root.child_element(Some(MD_NS), "SPSSODescriptor").is_some());
}
#[test]
fn unsigned_metadata_has_no_ds_signature_child() {
let cert = rsa_cert();
let acs = [SsoResponseEndpoint::post("https://sp.example.com/acs", 0, true)];
let formats = [NameIdFormat::Persistent];
let algos: [DataEncryptionAlgorithm; 0] = [];
let inputs = baseline_inputs(&cert, &acs, &[], &formats, &algos);
let xml = emit_sp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
assert!(doc.root().child_element(Some(DS_NS), "Signature").is_none());
}
#[test]
fn omits_key_descriptors_when_certs_absent() {
let acs = [SsoResponseEndpoint::post("https://sp.example.com/acs", 0, true)];
let formats = [NameIdFormat::Transient];
let algos: [DataEncryptionAlgorithm; 0] = [];
let inputs = SpMetadataInputs {
entity_id: "https://sp.example.com/saml",
acs: &acs,
slo: &[],
name_id_formats: &formats,
signing_cert: None,
encryption_cert: None,
encryption_algorithms: &algos,
authn_requests_signed: false,
want_assertions_signed: false,
valid_until: None,
cache_duration: None,
extras: None,
};
let xml = emit_sp_metadata(&inputs, None).unwrap();
let doc = Document::parse(xml.as_bytes()).unwrap();
let sp_desc = doc
.root()
.child_element(Some(MD_NS), "SPSSODescriptor")
.unwrap();
assert_eq!(
sp_desc.all_child_elements(Some(MD_NS), "KeyDescriptor").count(),
0
);
assert_eq!(sp_desc.attribute(None, "AuthnRequestsSigned"), Some("false"));
assert_eq!(
sp_desc.attribute(None, "WantAssertionsSigned"),
Some("false")
);
}
}