1use bacnet_types::constructed::BACnetAddress;
12use bacnet_types::enums::{NetworkPriority, RejectMessageReason};
13use bacnet_types::error::Error;
14use bacnet_types::MacAddr;
15use bytes::{BufMut, Bytes, BytesMut};
16
17mod decode_error;
18pub use decode_error::{NpduAddressField, NpduDecodeError};
19
20pub const BACNET_PROTOCOL_VERSION: u8 = 1;
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct NpduAddress {
32 pub network: u16,
34 pub mac_address: MacAddr,
36}
37
38impl NpduAddress {
39 pub const MAX_MAC_LEN: usize = BACnetAddress::MAX_MAC_LEN;
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Npdu {
57 pub is_network_message: bool,
59 pub expecting_reply: bool,
61 pub priority: NetworkPriority,
63 pub destination: Option<NpduAddress>,
65 pub source: Option<NpduAddress>,
67 pub hop_count: u8,
69 pub message_type: Option<u8>,
71 pub vendor_id: Option<u16>,
73 pub payload: Bytes,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct RejectMessageToNetwork {
83 pub reason: RejectMessageReason,
85 pub dnet: u16,
87}
88
89impl Default for Npdu {
90 fn default() -> Self {
91 Self {
92 is_network_message: false,
93 expecting_reply: false,
94 priority: NetworkPriority::NORMAL,
95 destination: None,
96 source: None,
97 hop_count: 255,
98 message_type: None,
99 vendor_id: None,
100 payload: Bytes::new(),
101 }
102 }
103}
104
105pub fn encode_npdu(buf: &mut BytesMut, npdu: &Npdu) -> Result<(), Error> {
111 buf.put_u8(BACNET_PROTOCOL_VERSION);
112
113 let mut control: u8 = npdu.priority.to_raw() & 0x03;
114 if npdu.is_network_message {
115 control |= 0x80;
116 }
117 if npdu.destination.is_some() {
118 control |= 0x20;
119 }
120 if npdu.source.is_some() {
121 control |= 0x08;
122 }
123 if npdu.expecting_reply {
124 control |= 0x04;
125 }
126 buf.put_u8(control);
127
128 if let Some(dest) = &npdu.destination {
129 if dest.network == 0 {
130 return Err(Error::Encoding("NPDU DNET must not be 0".into()));
131 }
132 buf.put_u16(dest.network);
133 check_encoded_len(NpduAddressField::Destination, &dest.mac_address)?;
134 buf.put_u8(dest.mac_address.len() as u8);
135 buf.put_slice(&dest.mac_address);
136 }
137
138 if let Some(src) = &npdu.source {
139 if src.network == 0 || src.network == 0xFFFF {
140 return Err(Error::Encoding(format!(
141 "NPDU SNET must be 1..65534, got {}",
142 src.network
143 )));
144 }
145 if src.mac_address.is_empty() {
146 return Err(Error::Encoding("NPDU SLEN must not be 0".into()));
147 }
148 buf.put_u16(src.network);
149 check_encoded_len(NpduAddressField::Source, &src.mac_address)?;
150 buf.put_u8(src.mac_address.len() as u8);
151 buf.put_slice(&src.mac_address);
152 }
153
154 if npdu.destination.is_some() {
155 buf.put_u8(npdu.hop_count);
156 }
157
158 if npdu.is_network_message {
159 if let Some(msg_type) = npdu.message_type {
160 buf.put_u8(msg_type);
161 if msg_type >= 0x80 {
162 buf.put_u16(npdu.vendor_id.unwrap_or(0));
163 }
164 }
165 }
166
167 buf.put_slice(&npdu.payload);
168
169 Ok(())
170}
171
172fn check_encoded_len(field: NpduAddressField, mac: &[u8]) -> Result<(), Error> {
174 if mac.len() > NpduAddress::MAX_MAC_LEN {
175 return Err(Error::Encoding(format!(
176 "NPDU {field} address of {} octets exceeds the {}-octet limit",
177 mac.len(),
178 NpduAddress::MAX_MAC_LEN
179 )));
180 }
181 Ok(())
182}
183
184pub fn decode_npdu(data: Bytes) -> Result<Npdu, NpduDecodeError> {
200 if data.len() < 2 {
201 return Err(Error::buffer_too_short(2, data.len()).into());
202 }
203
204 let version = data[0];
205 if version != BACNET_PROTOCOL_VERSION {
206 return Err(
207 Error::decoding(0, format!("unsupported BACnet protocol version: {version}")).into(),
208 );
209 }
210
211 let control = data[1];
212 let is_network_message = control & 0x80 != 0;
213 let has_destination = control & 0x20 != 0;
214 let has_source = control & 0x08 != 0;
215 let expecting_reply = control & 0x04 != 0;
216 let priority = NetworkPriority::from_raw(control & 0x03);
217
218 if control & 0x50 != 0 {
219 tracing::warn!(
220 control_byte = control,
221 "NPDU control byte has reserved bits set (bits 4 or 6)"
222 );
223 }
224
225 let mut offset = 2;
226 let mut destination = None;
227 let mut source = None;
228 let mut hop_count: u8 = 255;
229
230 if has_destination {
231 if offset + 3 > data.len() {
232 return Err(Error::decoding(offset, "NPDU too short for destination fields").into());
233 }
234 let dnet = u16::from_be_bytes([data[offset], data[offset + 1]]);
235 if dnet == 0 {
236 return Err(Error::decoding(offset, "NPDU destination network 0 is invalid").into());
237 }
238 offset += 2;
239 let dlen = data[offset];
240 offset += 1;
241 let dadr = match decode_address(&data, offset, NpduAddressField::Destination, dlen, dnet) {
242 Ok(dadr) => dadr,
243 Err(mut refused) => {
244 if let NpduDecodeError::AddressTooLong { source, .. } = &mut refused {
245 *source = has_source
246 .then(|| source_behind(&data, offset + usize::from(dlen)))
247 .flatten();
248 }
249 return Err(refused);
250 }
251 };
252 offset += dadr.len();
253
254 destination = Some(NpduAddress {
255 network: dnet,
256 mac_address: dadr,
257 });
258 }
259
260 if has_source {
261 if offset + 3 > data.len() {
262 return Err(Error::decoding(offset, "NPDU too short for source fields").into());
263 }
264 let snet = u16::from_be_bytes([data[offset], data[offset + 1]]);
265 if snet == 0 {
266 return Err(Error::decoding(offset, "NPDU source network 0 is invalid").into());
267 }
268 if snet == 0xFFFF {
269 return Err(Error::decoding(offset, "NPDU source network 0xFFFF is invalid").into());
270 }
271 offset += 2;
272 let slen = data[offset];
273 offset += 1;
274
275 if slen == 0 {
276 return Err(Error::decoding(offset - 1, "NPDU source SLEN=0 is invalid").into());
277 }
278 let dnet = destination.as_ref().map(|d| d.network);
279 let sadr = decode_address(&data, offset, NpduAddressField::Source, slen, dnet)?;
280 offset += sadr.len();
281
282 source = Some(NpduAddress {
283 network: snet,
284 mac_address: sadr,
285 });
286 }
287
288 if has_destination {
289 if offset >= data.len() {
290 return Err(Error::decoding(offset, "NPDU too short for hop count").into());
291 }
292 hop_count = data[offset];
293 offset += 1;
294 }
295
296 let mut message_type = None;
297 let mut vendor_id = None;
298
299 if is_network_message {
300 if offset >= data.len() {
301 return Err(Error::decoding(offset, "NPDU too short for network message type").into());
302 }
303 let msg_type = data[offset];
304 offset += 1;
305 message_type = Some(msg_type);
306
307 if msg_type >= 0x80 {
308 if offset + 2 > data.len() {
309 return Err(
310 Error::decoding(offset, "NPDU too short for proprietary vendor ID").into(),
311 );
312 }
313 vendor_id = Some(u16::from_be_bytes([data[offset], data[offset + 1]]));
314 offset += 2;
315 }
316 }
317
318 let payload = data.slice(offset..);
319
320 Ok(Npdu {
321 is_network_message,
322 expecting_reply,
323 priority,
324 destination,
325 source,
326 hop_count,
327 message_type,
328 vendor_id,
329 payload,
330 })
331}
332
333fn decode_address(
337 data: &[u8],
338 offset: usize,
339 field: NpduAddressField,
340 length: u8,
341 dnet: impl Into<Option<u16>>,
342) -> Result<MacAddr, NpduDecodeError> {
343 if usize::from(length) > NpduAddress::MAX_MAC_LEN {
344 return Err(NpduDecodeError::AddressTooLong {
345 field,
346 length,
347 dnet: dnet.into(),
348 source: None,
349 });
350 }
351 let end = offset + usize::from(length);
352 if end > data.len() {
353 return Err(Error::decoding(
354 offset,
355 format!(
356 "NPDU {field} address truncated: {}={length}",
357 field.length_octet()
358 ),
359 )
360 .into());
361 }
362 Ok(MacAddr::from_slice(&data[offset..end]))
363}
364
365fn source_behind(data: &[u8], offset: usize) -> Option<NpduAddress> {
372 let header = data.get(offset..offset + 3)?;
373 let network = u16::from_be_bytes([header[0], header[1]]);
374 let length = usize::from(header[2]);
375 if network == 0 || network == 0xFFFF || length == 0 || length > NpduAddress::MAX_MAC_LEN {
376 return None;
377 }
378 let sadr = data.get(offset + 3..offset + 3 + length)?;
379 Some(NpduAddress {
380 network,
381 mac_address: MacAddr::from_slice(sadr),
382 })
383}
384
385pub fn decode_reject_message_to_network(payload: &[u8]) -> Result<RejectMessageToNetwork, Error> {
391 if payload.len() != 3 {
392 return Err(Error::decoding(
393 0,
394 format!(
395 "Reject-Message-To-Network payload must be exactly 3 octets, got {}",
396 payload.len()
397 ),
398 ));
399 }
400 Ok(RejectMessageToNetwork {
401 reason: RejectMessageReason::from_raw(payload[0]),
402 dnet: u16::from_be_bytes([payload[1], payload[2]]),
403 })
404}
405
406#[cfg(test)]
411mod address_bound_tests;
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 fn encode_to_vec(npdu: &Npdu) -> Vec<u8> {
418 let mut buf = BytesMut::with_capacity(64);
419 encode_npdu(&mut buf, npdu).unwrap();
420 buf.to_vec()
421 }
422
423 #[test]
424 fn minimal_local_apdu() {
425 let npdu = Npdu {
427 payload: Bytes::from_static(&[0x10, 0x08]), ..Default::default()
429 };
430 let encoded = encode_to_vec(&npdu);
431 assert_eq!(encoded, vec![0x01, 0x00, 0x10, 0x08]);
433
434 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
435 assert_eq!(decoded, npdu);
436 }
437
438 #[test]
439 fn expecting_reply_flag() {
440 let npdu = Npdu {
441 expecting_reply: true,
442 payload: Bytes::from_static(&[0xAA]),
443 ..Default::default()
444 };
445 let encoded = encode_to_vec(&npdu);
446 assert_eq!(encoded[1], 0x04); let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
448 assert!(decoded.expecting_reply);
449 }
450
451 #[test]
452 fn priority_encoding() {
453 for (prio, expected_bits) in [
454 (NetworkPriority::NORMAL, 0x00),
455 (NetworkPriority::URGENT, 0x01),
456 (NetworkPriority::CRITICAL_EQUIPMENT, 0x02),
457 (NetworkPriority::LIFE_SAFETY, 0x03),
458 ] {
459 let npdu = Npdu {
460 priority: prio,
461 payload: Bytes::new(),
462 ..Default::default()
463 };
464 let encoded = encode_to_vec(&npdu);
465 assert_eq!(encoded[1] & 0x03, expected_bits);
466 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
467 assert_eq!(decoded.priority, prio);
468 }
469 }
470
471 #[test]
472 fn destination_only_round_trip() {
473 let npdu = Npdu {
474 destination: Some(NpduAddress {
475 network: 1000,
476 mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
477 }),
478 hop_count: 254,
479 payload: Bytes::from_static(&[0x10, 0x08]),
480 ..Default::default()
481 };
482 let encoded = encode_to_vec(&npdu);
483 assert_eq!(encoded[1] & 0x20, 0x20);
485 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
486 assert_eq!(decoded, npdu);
487 }
488
489 #[test]
490 fn destination_broadcast() {
491 let npdu = Npdu {
493 destination: Some(NpduAddress {
494 network: 0xFFFF,
495 mac_address: MacAddr::new(),
496 }),
497 hop_count: 255,
498 payload: Bytes::from_static(&[0x10, 0x08]),
499 ..Default::default()
500 };
501 let encoded = encode_to_vec(&npdu);
502 assert_eq!(encoded.len(), 8);
504 assert_eq!(&encoded[2..4], &[0xFF, 0xFF]);
506 assert_eq!(encoded[4], 0);
508
509 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
510 assert_eq!(decoded, npdu);
511 }
512
513 #[test]
514 fn source_only_round_trip() {
515 let npdu = Npdu {
516 source: Some(NpduAddress {
517 network: 500,
518 mac_address: MacAddr::from_slice(&[0x01]),
519 }),
520 payload: Bytes::from_static(&[0x30, 0x01, 0x0C]),
521 ..Default::default()
522 };
523 let encoded = encode_to_vec(&npdu);
524 assert_eq!(encoded[1] & 0x08, 0x08);
526 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
528 assert_eq!(decoded, npdu);
529 }
530
531 #[test]
532 fn source_and_destination_round_trip() {
533 let npdu = Npdu {
534 expecting_reply: true,
535 destination: Some(NpduAddress {
536 network: 2000,
537 mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
538 }),
539 source: Some(NpduAddress {
540 network: 1000,
541 mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
542 }),
543 hop_count: 250,
544 payload: Bytes::from_static(&[0x00, 0x05, 0x01, 0x0C]),
545 ..Default::default()
546 };
547 let encoded = encode_to_vec(&npdu);
548 assert_eq!(encoded[1], 0x2C);
550 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
551 assert_eq!(decoded, npdu);
552 }
553
554 #[test]
555 fn network_message_round_trip() {
556 let npdu = Npdu {
557 is_network_message: true,
558 message_type: Some(0x01), payload: Bytes::from_static(&[0x03, 0xE8]), ..Default::default()
561 };
562 let encoded = encode_to_vec(&npdu);
563 assert_eq!(encoded[1] & 0x80, 0x80);
565 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
566 assert_eq!(decoded, npdu);
567 }
568
569 #[test]
570 fn reject_message_to_network_payload_decodes_typed_reason_and_dnet() {
571 let decoded = decode_reject_message_to_network(&[
572 RejectMessageReason::MESSAGE_TOO_LONG.to_raw(),
573 0x12,
574 0x34,
575 ])
576 .unwrap();
577 assert_eq!(decoded.reason, RejectMessageReason::MESSAGE_TOO_LONG);
578 assert_eq!(decoded.dnet, 0x1234);
579 }
580
581 #[test]
582 fn reject_message_to_network_payload_requires_exact_length() {
583 for payload in [&[][..], &[4, 0][..], &[4, 0, 1, 2][..]] {
584 assert!(decode_reject_message_to_network(payload).is_err());
585 }
586 }
587
588 #[test]
589 fn proprietary_network_message_round_trip() {
590 let npdu = Npdu {
591 is_network_message: true,
592 message_type: Some(0x80), vendor_id: Some(999),
594 payload: Bytes::from_static(&[0xDE, 0xAD]),
595 ..Default::default()
596 };
597 let encoded = encode_to_vec(&npdu);
598 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
599 assert_eq!(decoded, npdu);
600 }
601
602 #[test]
603 fn wire_format_who_is_broadcast() {
604 let wire = [0x01, 0x20, 0xFF, 0xFF, 0x00, 0xFF, 0x10, 0x08];
608 let decoded = decode_npdu(Bytes::copy_from_slice(&wire)).unwrap();
609 assert!(!decoded.is_network_message);
610 assert!(!decoded.expecting_reply);
611 assert_eq!(decoded.priority, NetworkPriority::NORMAL);
612 assert_eq!(
613 decoded.destination,
614 Some(NpduAddress {
615 network: 0xFFFF,
616 mac_address: MacAddr::new(),
617 })
618 );
619 assert!(decoded.source.is_none());
620 assert_eq!(decoded.hop_count, 255);
621 assert_eq!(decoded.payload, vec![0x10, 0x08]);
622
623 let reencoded = encode_to_vec(&decoded);
625 assert_eq!(reencoded, wire);
626 }
627
628 #[test]
629 fn decode_too_short() {
630 assert!(decode_npdu(Bytes::new()).is_err());
631 assert!(decode_npdu(Bytes::from_static(&[0x01])).is_err());
632 }
633
634 #[test]
635 fn decode_wrong_version() {
636 assert!(decode_npdu(Bytes::from_static(&[0x02, 0x00])).is_err());
637 }
638
639 #[test]
640 fn decode_truncated_destination() {
641 assert!(decode_npdu(Bytes::from_static(&[0x01, 0x20, 0xFF])).is_err());
643 }
644
645 #[test]
646 fn decode_truncated_source() {
647 assert!(decode_npdu(Bytes::from_static(&[0x01, 0x08, 0x00])).is_err());
649 }
650
651 #[test]
654 fn npdu_network_zero() {
655 let npdu = Npdu {
657 destination: Some(NpduAddress {
658 network: 0,
659 mac_address: MacAddr::from_slice(&[0x01]),
660 }),
661 hop_count: 255,
662 payload: Bytes::from_static(&[0x10, 0x08]),
663 ..Default::default()
664 };
665 let mut buf = BytesMut::new();
666 let result = encode_npdu(&mut buf, &npdu);
667 assert!(result.is_err());
668 let err = format!("{}", result.unwrap_err());
669 assert!(err.contains("DNET"), "got: {err}");
670 }
671
672 #[test]
673 fn npdu_network_fffe() {
674 let npdu = Npdu {
677 destination: Some(NpduAddress {
678 network: 0xFFFE,
679 mac_address: MacAddr::from_slice(&[0x01, 0x02]),
680 }),
681 hop_count: 200,
682 payload: Bytes::from_static(&[0xAA]),
683 ..Default::default()
684 };
685 let encoded = encode_to_vec(&npdu);
686 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
687 assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFE);
688 assert_eq!(decoded.hop_count, 200);
689 }
690
691 #[test]
692 fn npdu_hop_count_zero() {
693 let npdu = Npdu {
695 destination: Some(NpduAddress {
696 network: 1000,
697 mac_address: MacAddr::new(),
698 }),
699 hop_count: 0,
700 payload: Bytes::from_static(&[0x10, 0x08]),
701 ..Default::default()
702 };
703 let encoded = encode_to_vec(&npdu);
704 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
705 assert_eq!(decoded.hop_count, 0);
706 }
707
708 #[test]
709 fn npdu_source_with_empty_mac() {
710 let npdu = Npdu {
712 source: Some(NpduAddress {
713 network: 500,
714 mac_address: MacAddr::new(),
715 }),
716 payload: Bytes::from_static(&[0xBB]),
717 ..Default::default()
718 };
719 let mut buf = BytesMut::new();
720 let result = encode_npdu(&mut buf, &npdu);
721 assert!(result.is_err());
722 let err = format!("{}", result.unwrap_err());
723 assert!(err.contains("SLEN"), "got: {err}");
724 }
725
726 #[test]
727 fn npdu_destination_dlen_zero_broadcast_accepted() {
728 let npdu = Npdu {
730 destination: Some(NpduAddress {
731 network: 0xFFFF,
732 mac_address: MacAddr::new(),
733 }),
734 hop_count: 255,
735 payload: Bytes::from_static(&[0x10, 0x08]),
736 ..Default::default()
737 };
738 let encoded = encode_to_vec(&npdu);
739 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
740 assert_eq!(decoded.destination.as_ref().unwrap().network, 0xFFFF);
741 assert!(decoded.destination.as_ref().unwrap().mac_address.is_empty());
742 }
743
744 #[test]
745 fn npdu_destination_truncated_mac() {
746 let data = [0x01, 0x20, 0x03, 0xE8, 0x06, 0x01, 0x02];
749 assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
750 }
751
752 #[test]
753 fn npdu_source_truncated_mac() {
754 let data = [0x01, 0x08, 0x01, 0xF4, 0x04, 0x01]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
757 }
758
759 #[test]
760 fn npdu_missing_hop_count() {
761 let data = [0x01, 0x20, 0xFF, 0xFF, 0x00];
764 assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
765 }
766
767 #[test]
768 fn npdu_network_message_truncated_type() {
769 let data = [0x01, 0x80]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
772 }
773
774 #[test]
775 fn npdu_proprietary_message_truncated_vendor() {
776 let data = [0x01, 0x80, 0x80]; assert!(decode_npdu(Bytes::copy_from_slice(&data)).is_err());
779 }
780
781 #[test]
782 fn npdu_all_flags_round_trip() {
783 let npdu = Npdu {
785 is_network_message: false,
786 expecting_reply: true,
787 priority: NetworkPriority::LIFE_SAFETY,
788 destination: Some(NpduAddress {
789 network: 2000,
790 mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x02, 0x01, 0xBA, 0xC0]),
791 }),
792 source: Some(NpduAddress {
793 network: 1000,
794 mac_address: MacAddr::from_slice(&[0x0A, 0x00, 0x01, 0x01, 0xBA, 0xC0]),
795 }),
796 hop_count: 127,
797 payload: Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF]),
798 ..Default::default()
799 };
800 let encoded = encode_to_vec(&npdu);
801 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
802 assert_eq!(decoded, npdu);
803 }
804
805 #[test]
806 fn npdu_empty_payload() {
807 let npdu = Npdu {
809 payload: Bytes::new(),
810 ..Default::default()
811 };
812 let encoded = encode_to_vec(&npdu);
813 let decoded = decode_npdu(Bytes::from(encoded)).unwrap();
814 assert!(decoded.payload.is_empty());
815 }
816
817 #[test]
818 fn reject_snet_zero() {
819 let npdu = Npdu {
821 source: Some(NpduAddress {
822 network: 0,
823 mac_address: MacAddr::from_slice(&[0x01]),
824 }),
825 payload: Bytes::from_static(&[0x10, 0x08]),
826 ..Default::default()
827 };
828 let mut buf = BytesMut::new();
829 let result = encode_npdu(&mut buf, &npdu);
830 assert!(result.is_err());
831 let err = format!("{}", result.unwrap_err());
832 assert!(err.contains("SNET"), "got: {err}");
833 }
834
835 #[test]
836 fn reserved_bits_warning_still_decodes() {
837 let mut data = vec![0x01, 0x40]; data.extend_from_slice(&[0x10, 0x08]);
841
842 let result = decode_npdu(Bytes::copy_from_slice(&data));
844 assert!(
845 result.is_ok(),
846 "reserved bits should not cause decode failure"
847 );
848 }
849}