1use std::cell::Cell;
9use std::collections::HashSet;
10use std::ops::RangeInclusive;
11
12use roxmltree::{Document, Node, NodeId};
13
14const MAX_NODE_SET_ENTRIES: usize = 65_536;
15const MAX_NODE_SET_OWNED_STRING_BYTES: usize = 8 * 1024 * 1024;
16const MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES: usize = 64 * 1024 * 1024;
17
18use crate::c14n::NodeVisibility;
19
20pub enum TransformData<'a> {
29 NodeSet(NodeSet<'a>),
31 Binary(Vec<u8>),
33}
34
35impl std::fmt::Debug for TransformData<'_> {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::NodeSet(_) => f.debug_tuple("NodeSet").field(&"...").finish(),
39 Self::Binary(b) => f.debug_tuple("Binary").field(&b.len()).finish(),
40 }
41 }
42}
43
44impl<'a> TransformData<'a> {
45 pub fn into_node_set(self) -> Result<NodeSet<'a>, TransformError> {
47 match self {
48 Self::NodeSet(ns) => Ok(ns),
49 Self::Binary(_) => Err(TransformError::TypeMismatch {
50 expected: "NodeSet",
51 got: "Binary",
52 }),
53 }
54 }
55
56 pub fn into_binary(self) -> Result<Vec<u8>, TransformError> {
58 match self {
59 Self::Binary(b) => Ok(b),
60 Self::NodeSet(_) => Err(TransformError::TypeMismatch {
61 expected: "Binary",
62 got: "NodeSet",
63 }),
64 }
65 }
66}
67
68pub struct NodeSet<'a> {
77 doc: &'a Document<'a>,
79 nodes: HashSet<XmlNodeKey>,
80 with_comments: bool,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86enum XmlNodeKey {
87 Tree(NodeId),
88 Attribute {
89 owner: NodeId,
90 namespace: Option<String>,
91 local_name: String,
92 },
93 Namespace {
94 owner: NodeId,
95 prefix: String,
96 uri: String,
97 },
98}
99
100pub(crate) struct NodeSetMaterializationBudget {
101 remaining_owned_string_bytes: Cell<usize>,
102}
103
104impl Default for NodeSetMaterializationBudget {
105 fn default() -> Self {
106 Self {
107 remaining_owned_string_bytes: Cell::new(MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES),
108 }
109 }
110}
111
112impl NodeSetMaterializationBudget {
113 fn charge(&self, owned_string_bytes: usize) -> Result<(), TransformError> {
114 let Some(remaining) = self
115 .remaining_owned_string_bytes
116 .get()
117 .checked_sub(owned_string_bytes)
118 else {
119 self.remaining_owned_string_bytes.set(0);
120 return Err(TransformError::NodeSetCumulativeStringsTooLarge {
121 max_bytes: MAX_NODE_SET_CUMULATIVE_OWNED_STRING_BYTES,
122 });
123 };
124 self.remaining_owned_string_bytes.set(remaining);
125 Ok(())
126 }
127
128 #[cfg(test)]
129 pub(crate) fn with_limit(limit: usize) -> Self {
130 Self {
131 remaining_owned_string_bytes: Cell::new(limit),
132 }
133 }
134}
135
136impl XmlNodeKey {
137 fn owner_id(&self) -> NodeId {
138 match self {
139 Self::Tree(id) => *id,
140 Self::Attribute { owner, .. } | Self::Namespace { owner, .. } => *owner,
141 }
142 }
143}
144
145impl<'a> NodeSet<'a> {
146 pub fn entire_document_without_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
157 Self::ensure_subtree_materialization_fits(doc.root())?;
158 Ok(Self::collect_document(doc, false))
159 }
160
161 pub(crate) fn entire_document_without_comments_with_budget(
162 doc: &'a Document<'a>,
163 budget: &NodeSetMaterializationBudget,
164 ) -> Result<Self, TransformError> {
165 Self::charge_subtree_materialization(doc.root(), budget)?;
166 Ok(Self::collect_document(doc, false))
167 }
168
169 pub fn entire_document_with_comments(doc: &'a Document<'a>) -> Result<Self, TransformError> {
179 Self::ensure_subtree_materialization_fits(doc.root())?;
180 Ok(Self::collect_document(doc, true))
181 }
182
183 pub(crate) fn entire_document_with_comments_with_budget(
184 doc: &'a Document<'a>,
185 budget: &NodeSetMaterializationBudget,
186 ) -> Result<Self, TransformError> {
187 Self::charge_subtree_materialization(doc.root(), budget)?;
188 Ok(Self::collect_document(doc, true))
189 }
190
191 pub fn subtree(element: Node<'a, 'a>) -> Result<Self, TransformError> {
201 Self::ensure_subtree_materialization_fits(element)?;
202 Ok(Self::collect_subtree(element))
203 }
204
205 pub(crate) fn subtree_with_budget(
206 element: Node<'a, 'a>,
207 budget: &NodeSetMaterializationBudget,
208 ) -> Result<Self, TransformError> {
209 Self::charge_subtree_materialization(element, budget)?;
210 Ok(Self::collect_subtree(element))
211 }
212
213 fn collect_subtree(element: Node<'a, 'a>) -> Self {
214 let mut set = Self {
215 doc: element.document(),
216 nodes: HashSet::new(),
217 with_comments: true,
218 };
219 set.insert_subtree(element);
220 set
221 }
222
223 pub fn document(&self) -> &'a Document<'a> {
225 self.doc
226 }
227
228 pub fn contains(&self, node: Node<'_, '_>) -> bool {
233 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
237 return false;
238 }
239
240 self.nodes.contains(&XmlNodeKey::Tree(node.id()))
241 }
242
243 pub fn exclude_subtree(&mut self, node: Node<'_, '_>) {
247 if !std::ptr::eq(node.document() as *const _, self.doc as *const _) {
249 return;
250 }
251 let excluded_ids = subtree_node_id_range(node);
252 self.nodes
257 .retain(|key| !excluded_ids.contains(&key.owner_id().get()));
258 }
259
260 pub fn with_comments(&self) -> bool {
262 self.with_comments
263 }
264
265 pub(crate) fn empty(doc: &'a Document<'a>) -> Self {
266 Self {
267 doc,
268 nodes: HashSet::new(),
269 with_comments: false,
270 }
271 }
272
273 #[cfg(test)]
274 pub(crate) fn try_entire_document(doc: &'a Document<'a>) -> Result<Self, TransformError> {
275 Self::entire_document_with_comments(doc)
276 }
277
278 pub(crate) fn try_entire_document_with_budget(
279 doc: &'a Document<'a>,
280 budget: &NodeSetMaterializationBudget,
281 ) -> Result<Self, TransformError> {
282 Self::entire_document_with_comments_with_budget(doc, budget)
283 }
284
285 pub(crate) fn len(&self) -> usize {
286 self.nodes.len()
287 }
288
289 pub(crate) fn insert_node(&mut self, node: Node<'_, '_>) {
290 if self.owns(node) {
291 self.with_comments |= node.is_comment();
292 self.nodes.insert(XmlNodeKey::Tree(node.id()));
293 }
294 }
295
296 pub(crate) fn insert_attribute(
297 &mut self,
298 owner: Node<'_, '_>,
299 namespace: Option<&str>,
300 local_name: &str,
301 ) {
302 if self.owns(owner) {
303 self.nodes.insert(XmlNodeKey::Attribute {
304 owner: owner.id(),
305 namespace: namespace.map(str::to_owned),
306 local_name: local_name.to_owned(),
307 });
308 }
309 }
310
311 pub(crate) fn insert_attribute_with_budget(
312 &mut self,
313 owner: Node<'_, '_>,
314 namespace: Option<&str>,
315 local_name: &str,
316 budget: &NodeSetMaterializationBudget,
317 ) -> Result<(), TransformError> {
318 if self.owns(owner) {
319 let owned_string_bytes = namespace
320 .map_or(0, str::len)
321 .checked_add(local_name.len())
322 .ok_or(TransformError::NodeSetStringsTooLarge {
323 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
324 })?;
325 budget.charge(owned_string_bytes)?;
326 self.insert_attribute(owner, namespace, local_name);
327 }
328 Ok(())
329 }
330
331 pub(crate) fn insert_namespace(&mut self, owner: Node<'_, '_>, prefix: &str, uri: &str) {
332 if self.owns(owner) {
333 self.nodes.insert(XmlNodeKey::Namespace {
334 owner: owner.id(),
335 prefix: prefix.to_owned(),
336 uri: uri.to_owned(),
337 });
338 }
339 }
340
341 pub(crate) fn insert_namespace_with_budget(
342 &mut self,
343 owner: Node<'_, '_>,
344 prefix: &str,
345 uri: &str,
346 budget: &NodeSetMaterializationBudget,
347 ) -> Result<(), TransformError> {
348 if self.owns(owner) {
349 let owned_string_bytes = prefix.len().checked_add(uri.len()).ok_or(
350 TransformError::NodeSetStringsTooLarge {
351 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
352 },
353 )?;
354 budget.charge(owned_string_bytes)?;
355 self.insert_namespace(owner, prefix, uri);
356 }
357 Ok(())
358 }
359
360 pub(crate) fn insert_subtree(&mut self, root: Node<'_, '_>) {
361 if !self.owns(root) {
362 return;
363 }
364 let mut stack = vec![root];
365 while let Some(node) = stack.pop() {
366 self.insert_node(node);
367 if node.is_element() {
368 for attribute in node.attributes() {
369 self.insert_attribute(node, attribute.namespace(), attribute.name());
370 }
371 for namespace in node.namespaces() {
372 self.insert_namespace(node, namespace.name().unwrap_or(""), namespace.uri());
373 }
374 }
375 stack.extend(node.children());
376 }
377 }
378
379 pub(crate) fn intersect_with(&mut self, other: &Self) {
380 if !std::ptr::eq(self.doc as *const _, other.doc as *const _) {
381 self.nodes.clear();
382 self.with_comments = false;
383 return;
384 }
385 self.nodes.retain(|key| other.nodes.contains(key));
386 self.with_comments &= other.with_comments;
387 }
388
389 pub(crate) fn subtract(&mut self, other: &Self) {
390 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
391 self.nodes.retain(|key| !other.nodes.contains(key));
392 }
393 }
394
395 pub(crate) fn union_with_budget(
396 &mut self,
397 other: &Self,
398 budget: &NodeSetMaterializationBudget,
399 ) -> Result<(), TransformError> {
400 if std::ptr::eq(self.doc as *const _, other.doc as *const _) {
401 for key in &other.nodes {
402 if self.nodes.contains(key) {
403 continue;
404 }
405 let owned_string_bytes = match key {
406 XmlNodeKey::Tree(_) => 0,
407 XmlNodeKey::Attribute {
408 namespace,
409 local_name,
410 ..
411 } => namespace.as_ref().map_or(0, String::len) + local_name.len(),
412 XmlNodeKey::Namespace { prefix, uri, .. } => prefix.len() + uri.len(),
413 };
414 budget.charge(owned_string_bytes)?;
415 self.nodes.insert(key.clone());
416 }
417 self.with_comments |= other.with_comments;
418 }
419 Ok(())
420 }
421
422 fn collect_document(doc: &'a Document<'a>, with_comments: bool) -> Self {
423 let mut set = Self::empty(doc);
424 set.insert_subtree(doc.root());
425 if !with_comments {
426 set.nodes.retain(|key| match key {
427 XmlNodeKey::Tree(id) => !doc.get_node(*id).is_some_and(|node| node.is_comment()),
428 _ => true,
429 });
430 }
431 set.with_comments = with_comments;
432 set
433 }
434
435 pub(crate) fn ensure_subtree_materialization_fits(
436 root: Node<'_, '_>,
437 ) -> Result<usize, TransformError> {
438 Ok(Self::subtree_materialization(root)?.entries)
439 }
440
441 fn charge_subtree_materialization(
442 root: Node<'_, '_>,
443 budget: &NodeSetMaterializationBudget,
444 ) -> Result<(), TransformError> {
445 let materialization = Self::subtree_materialization(root)?;
446 budget.charge(materialization.owned_string_bytes)
447 }
448
449 fn subtree_materialization(
450 root: Node<'_, '_>,
451 ) -> Result<NodeSetMaterialization, TransformError> {
452 let mut entries = 0_usize;
453 let mut owned_string_bytes = 0_usize;
454 let mut stack = vec![root];
455 while let Some(node) = stack.pop() {
456 let projected = if node.is_element() {
457 for attribute in node.attributes() {
458 owned_string_bytes = charge_node_set_string_bytes(
459 owned_string_bytes,
460 attribute.namespace().map_or(0, str::len),
461 )?;
462 owned_string_bytes =
463 charge_node_set_string_bytes(owned_string_bytes, attribute.name().len())?;
464 }
465 for namespace in node.namespaces() {
466 owned_string_bytes = charge_node_set_string_bytes(
467 owned_string_bytes,
468 namespace.name().map_or(0, str::len),
469 )?;
470 owned_string_bytes =
471 charge_node_set_string_bytes(owned_string_bytes, namespace.uri().len())?;
472 }
473 1_usize
474 .checked_add(node.attributes().len())
475 .and_then(|count| count.checked_add(node.namespaces().len()))
476 } else {
477 Some(1)
478 }
479 .ok_or(TransformError::NodeSetTooLarge {
480 max: MAX_NODE_SET_ENTRIES,
481 })?;
482 entries = entries
483 .checked_add(projected)
484 .ok_or(TransformError::NodeSetTooLarge {
485 max: MAX_NODE_SET_ENTRIES,
486 })?;
487 if entries > MAX_NODE_SET_ENTRIES {
488 return Err(TransformError::NodeSetTooLarge {
489 max: MAX_NODE_SET_ENTRIES,
490 });
491 }
492 stack.extend(node.children());
493 }
494 Ok(NodeSetMaterialization {
495 entries,
496 owned_string_bytes,
497 })
498 }
499
500 fn owns(&self, node: Node<'_, '_>) -> bool {
501 std::ptr::eq(node.document() as *const _, self.doc as *const _)
502 }
503}
504
505struct NodeSetMaterialization {
506 entries: usize,
507 owned_string_bytes: usize,
508}
509
510fn charge_node_set_string_bytes(
511 current: usize,
512 additional: usize,
513) -> Result<usize, TransformError> {
514 let total = current
515 .checked_add(additional)
516 .ok_or(TransformError::NodeSetStringsTooLarge {
517 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
518 })?;
519 if total > MAX_NODE_SET_OWNED_STRING_BYTES {
520 return Err(TransformError::NodeSetStringsTooLarge {
521 max_bytes: MAX_NODE_SET_OWNED_STRING_BYTES,
522 });
523 }
524 Ok(total)
525}
526
527fn subtree_node_id_range(node: Node<'_, '_>) -> RangeInclusive<u32> {
528 let last_id = node
529 .descendants()
530 .next_back()
531 .map_or(node.id(), |descendant| descendant.id());
532 node.id().get()..=last_id.get()
533}
534
535impl NodeVisibility for NodeSet<'_> {
536 fn contains_node(&self, node: Node<'_, '_>) -> bool {
537 self.contains(node)
538 }
539
540 fn contains_attribute(
541 &self,
542 owner: Node<'_, '_>,
543 namespace: Option<&str>,
544 local_name: &str,
545 ) -> bool {
546 self.owns(owner)
547 && self.nodes.contains(&XmlNodeKey::Attribute {
548 owner: owner.id(),
549 namespace: namespace.map(str::to_owned),
550 local_name: local_name.to_owned(),
551 })
552 }
553
554 fn contains_namespace(&self, owner: Node<'_, '_>, prefix: &str, uri: &str) -> bool {
555 self.owns(owner)
556 && self.nodes.contains(&XmlNodeKey::Namespace {
557 owner: owner.id(),
558 prefix: prefix.to_owned(),
559 uri: uri.to_owned(),
560 })
561 }
562}
563
564#[derive(Debug, thiserror::Error)]
566#[non_exhaustive]
567pub enum TransformError {
568 #[error("type mismatch: expected {expected}, got {got}")]
570 TypeMismatch {
571 expected: &'static str,
573 got: &'static str,
575 },
576
577 #[error("element not found by ID: {0}")]
579 ElementNotFound(String),
580
581 #[error("unsupported URI: {0}")]
583 UnsupportedUri(String),
584
585 #[error("unsupported transform: {0}")]
587 UnsupportedTransform(String),
588
589 #[error("transform chain exceeds maximum length of {max}")]
591 TooManyTransforms {
592 max: usize,
594 },
595
596 #[error("node-set materialization exceeds maximum of {max} entries")]
598 NodeSetTooLarge {
599 max: usize,
601 },
602
603 #[error("node-set materialization exceeds maximum of {max_bytes} owned string bytes")]
605 NodeSetStringsTooLarge {
606 max_bytes: usize,
608 },
609
610 #[error(
612 "node-set materialization exceeds signature-wide maximum of {max_bytes} cumulative owned string bytes"
613 )]
614 NodeSetCumulativeStringsTooLarge {
615 max_bytes: usize,
617 },
618
619 #[error("node-set filtering exceeds signature-wide maximum of {max_entries} entry visits")]
621 NodeSetFilterWorkTooLarge {
622 max_entries: usize,
624 },
625
626 #[error(
628 "XPath mirrors exceed signature-wide maximum of {max_bytes} cumulative copied string bytes"
629 )]
630 XPathMirrorTooLarge {
631 max_bytes: usize,
633 },
634
635 #[error(
637 "XPath transform exceeds signature-wide maximum of {max_bytes} string-processing work bytes"
638 )]
639 XPathStringWorkTooLarge {
640 max_bytes: usize,
642 },
643
644 #[error("C14N error: {0}")]
646 C14n(#[from] crate::c14n::C14nError),
647
648 #[error("cumulative canonical output exceeds signature-wide maximum of {max_bytes} bytes")]
650 C14nOutputTooLarge {
651 max_bytes: usize,
653 },
654
655 #[error("base64 transform decode error: {0}")]
657 Base64(String),
658
659 #[error("cumulative base64 transform input exceeds maximum of {max_bytes} bytes")]
661 Base64InputTooLarge {
662 max_bytes: usize,
664 },
665
666 #[error("base64 transform output exceeds maximum of {max_bytes} bytes")]
668 Base64OutputTooLarge {
669 max_bytes: usize,
671 },
672
673 #[error("XPath transform error: {0}")]
675 XPath(String),
676
677 #[error("XML transform input parse error: {0}")]
680 XmlParse(String),
681
682 #[error("enveloped-signature transform: invalid Signature node for this document")]
685 CrossDocumentSignatureNode,
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use crate::c14n::{C14nAlgorithm, C14nMode, canonicalize_with_visibility};
692
693 #[test]
694 fn document_without_comments_preserves_comment_policy() {
695 let document = Document::parse("<root><!-- excluded --><child/></root>")
698 .expect("fixed comment fixture must parse");
699 let nodes = NodeSet::entire_document_without_comments(&document)
700 .expect("fixed fixture must fit the node-set materialization budget");
701 let comment = document
702 .descendants()
703 .find(|node| node.is_comment())
704 .expect("fixed fixture contains one comment");
705
706 assert!(!nodes.contains(comment));
707 assert!(!nodes.with_comments());
708 }
709
710 #[test]
711 fn excluding_disjoint_oversized_subtree_only_scans_input_keys() {
712 let xml = format!(
716 "<root><target Id=\"selected\"><child/></target><Signature>{}</Signature></root>",
717 "<Object/>".repeat(MAX_NODE_SET_ENTRIES + 1)
718 );
719 let document = Document::parse(&xml).expect("fixed oversized fixture must parse");
720 let target = document
721 .descendants()
722 .find(|node| node.attribute("Id") == Some("selected"))
723 .expect("fixed fixture contains the selected subtree");
724 let signature = document
725 .descendants()
726 .find(|node| node.has_tag_name("Signature"))
727 .expect("fixed fixture contains the excluded Signature subtree");
728 let mut nodes = NodeSet::subtree(target)
729 .expect("small selected subtree must fit the materialization budget");
730 let entries_before = nodes.nodes.len();
731
732 nodes.exclude_subtree(signature);
733
734 assert_eq!(nodes.nodes.len(), entries_before);
735 assert!(nodes.contains(target));
736 assert!(
737 nodes.contains(
738 target
739 .first_element_child()
740 .expect("fixed target subtree contains a child")
741 )
742 );
743 }
744
745 #[test]
746 fn materialization_rejects_inherited_namespace_byte_amplification() {
747 let namespace_uri = "x".repeat(8_192);
751 let xml = format!(
752 "<root xmlns:amplified=\"{namespace_uri}\">{}</root>",
753 "<child/>".repeat(1_025)
754 );
755 let document = Document::parse(&xml).expect("fixed namespace fixture must parse");
756
757 let error = NodeSet::entire_document_without_comments(&document)
758 .err()
759 .expect("amplified namespace bytes must exceed the materialization budget");
760
761 assert!(matches!(
762 error,
763 TransformError::NodeSetStringsTooLarge { .. }
764 ));
765 }
766
767 #[test]
768 fn subtree_node_id_range_contains_only_the_selected_subtree() {
769 let document = Document::parse(
773 "<root><before/><excluded xmlns:gone=\"urn:gone\" a=\"1\"><child/></excluded><after/></root>",
774 )
775 .expect("fixed subtree range fixture must parse");
776 let excluded = document
777 .descendants()
778 .find(|node| node.has_tag_name("excluded"))
779 .expect("fixed fixture contains the excluded subtree");
780 let range = subtree_node_id_range(excluded);
781 let before = document
782 .descendants()
783 .find(|node| node.has_tag_name("before"))
784 .expect("fixed fixture contains the preceding sibling");
785 let child = excluded
786 .first_element_child()
787 .expect("fixed fixture contains an excluded child");
788 let after = document
789 .descendants()
790 .find(|node| node.has_tag_name("after"))
791 .expect("fixed fixture contains the following sibling");
792
793 assert!(!range.contains(&before.id().get()));
794 assert!(range.contains(&excluded.id().get()));
795 assert!(range.contains(&child.id().get()));
796 assert!(!range.contains(&after.id().get()));
797
798 let mut nodes = NodeSet::entire_document_with_comments(&document)
799 .expect("fixed fixture must fit the node-set materialization budget");
800 nodes.exclude_subtree(excluded);
801
802 assert!(nodes.contains(before));
803 assert!(!nodes.contains(excluded));
804 assert!(!nodes.contains(child));
805 assert!(!nodes.contains_attribute(excluded, None, "a"));
806 assert!(!nodes.contains_namespace(excluded, "gone", "urn:gone"));
807 assert!(nodes.contains(after));
808 }
809
810 #[test]
811 fn excluding_subtree_removes_trailing_text_and_comments_from_canonical_output() {
812 let document = Document::parse(
816 "<root><before/>keep-before<excluded><child/>drop-text<!--drop-comment--></excluded>keep-after<after/></root>",
817 )
818 .expect("fixed trailing-node fixture must parse");
819 let excluded = document
820 .descendants()
821 .find(|node| node.has_tag_name("excluded"))
822 .expect("fixed fixture contains the excluded subtree");
823 let trailing_text = excluded
824 .children()
825 .find(|node| node.is_text())
826 .expect("fixed fixture contains trailing text");
827 let trailing_comment = excluded
828 .children()
829 .find(|node| node.is_comment())
830 .expect("fixed fixture contains a trailing comment");
831 let mut nodes = NodeSet::entire_document_with_comments(&document)
832 .expect("fixed fixture must fit the node-set materialization budget");
833
834 nodes.exclude_subtree(excluded);
835
836 assert!(!nodes.contains(trailing_text));
837 assert!(!nodes.contains(trailing_comment));
838 let mut output = Vec::new();
839 canonicalize_with_visibility(
840 &document,
841 Some(&nodes),
842 &C14nAlgorithm::new(C14nMode::Inclusive1_0, true),
843 &mut output,
844 )
845 .expect("the retained node set must canonicalize");
846 assert_eq!(
847 output,
848 b"<root><before></before>keep-beforekeep-after<after></after></root>"
849 );
850 }
851}