1#![allow(clippy::type_complexity)]
7
8macro_rules! impl_stream_helpers {
10 ($type:ident < $($gen:tt),+ >) => {
11 impl<$($gen),+> $type<$($gen),+>
12 where
13 $($gen: crate::transport::Transport + 'static,)+
14 {
15 pub async fn next(&mut self) -> Option<crate::error::Result<crate::varbind::VarBind>> {
17 std::future::poll_fn(|cx| std::pin::Pin::new(&mut *self).poll_next(cx)).await
18 }
19
20 pub async fn collect(mut self) -> crate::error::Result<Vec<crate::varbind::VarBind>> {
28 let mut results = Vec::new();
29 while let Some(result) = self.next().await {
30 results.push(result?);
31 }
32 if results.is_empty() {
33 crate::client::walk::walk_scalar_fallback(
34 &self.client,
35 &self.base_oid,
36 self.max_results,
37 &mut results,
38 )
39 .await?;
40 }
41 Ok(results)
42 }
43 }
44 };
45}
46
47use std::collections::{HashSet, VecDeque};
48use std::pin::Pin;
49use std::task::{Context, Poll};
50
51use futures_core::Stream;
52
53use crate::error::{Error, Result, WalkAbortReason};
54use crate::oid::Oid;
55use crate::transport::Transport;
56use crate::varbind::VarBind;
57use crate::version::Version;
58
59use super::Client;
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub enum WalkMode {
64 #[default]
67 Auto,
68 GetNext,
70 GetBulk,
72}
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
91pub enum OidOrdering {
92 #[default]
97 Strict,
98
99 AllowNonIncreasing,
113}
114
115enum OidTracker {
116 Strict { last: Option<Oid> },
117 Relaxed { seen: HashSet<Oid> },
118}
119
120enum VarbindOutcome {
122 Yield,
124 Done,
126 Abort(Box<Error>),
128}
129
130fn validate_walk_varbind(
135 vb: &VarBind,
136 base_oid: &Oid,
137 oid_tracker: &mut OidTracker,
138 target: std::net::SocketAddr,
139) -> VarbindOutcome {
140 if vb.value.is_exception() {
141 return VarbindOutcome::Done;
142 }
143 if !vb.oid.starts_with(base_oid) {
144 return VarbindOutcome::Done;
145 }
146 match oid_tracker.check(&vb.oid, target) {
147 Ok(()) => VarbindOutcome::Yield,
148 Err(e) => VarbindOutcome::Abort(e),
149 }
150}
151
152async fn walk_scalar_fallback<T: Transport + 'static>(
168 client: &Client<T>,
169 base_oid: &Oid,
170 max_results: Option<usize>,
171 results: &mut Vec<VarBind>,
172) -> Result<()> {
173 if matches!(max_results, Some(max) if results.len() >= max) {
175 return Ok(());
176 }
177 match client.get(base_oid).await {
178 Ok(vb) if !vb.value.is_exception() => {
180 results.push(vb);
181 Ok(())
182 }
183 Ok(_) => Ok(()),
185 Err(e)
187 if matches!(
188 &*e,
189 Error::Snmp {
190 status: crate::error::ErrorStatus::NoSuchName,
191 ..
192 }
193 ) =>
194 {
195 Ok(())
196 }
197 Err(e) => Err(e),
199 }
200}
201
202impl OidTracker {
203 fn new(ordering: OidOrdering) -> Self {
204 match ordering {
205 OidOrdering::Strict => OidTracker::Strict { last: None },
206 OidOrdering::AllowNonIncreasing => OidTracker::Relaxed {
207 seen: HashSet::new(),
208 },
209 }
210 }
211
212 fn check(&mut self, oid: &Oid, target: std::net::SocketAddr) -> Result<()> {
213 match self {
214 OidTracker::Strict { last } => {
215 if let Some(prev) = last
216 && oid <= prev
217 {
218 tracing::debug!(target: "async_snmp::walk", { previous_oid = %prev, current_oid = %oid, %target }, "non-increasing OID detected");
219 return Err(Error::WalkAborted {
220 target,
221 reason: WalkAbortReason::NonIncreasing,
222 }
223 .boxed());
224 }
225 *last = Some(oid.clone());
226 Ok(())
227 }
228 OidTracker::Relaxed { seen } => {
229 if !seen.insert(oid.clone()) {
230 tracing::debug!(target: "async_snmp::walk", { %oid, %target }, "duplicate OID detected (cycle)");
231 return Err(Error::WalkAborted {
232 target,
233 reason: WalkAbortReason::Cycle,
234 }
235 .boxed());
236 }
237 Ok(())
238 }
239 }
240 }
241}
242
243pub struct Walk<T: Transport> {
247 client: Client<T>,
248 base_oid: Oid,
249 current_oid: Oid,
250 oid_tracker: OidTracker,
252 max_results: Option<usize>,
254 count: usize,
256 done: bool,
257 pending: Option<Pin<Box<dyn std::future::Future<Output = Result<VarBind>> + Send>>>,
258}
259
260impl<T: Transport> Walk<T> {
261 pub(crate) fn new(
262 client: Client<T>,
263 oid: Oid,
264 ordering: OidOrdering,
265 max_results: Option<usize>,
266 ) -> Self {
267 Self {
268 client,
269 base_oid: oid.clone(),
270 current_oid: oid,
271 oid_tracker: OidTracker::new(ordering),
272 max_results,
273 count: 0,
274 done: false,
275 pending: None,
276 }
277 }
278}
279
280impl_stream_helpers!(Walk<T>);
281
282impl<T: Transport + 'static> Stream for Walk<T> {
283 type Item = Result<VarBind>;
284
285 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
286 if self.done {
287 return Poll::Ready(None);
288 }
289
290 if let Some(max) = self.max_results
292 && self.count >= max
293 {
294 self.done = true;
295 return Poll::Ready(None);
296 }
297
298 if self.pending.is_none() {
300 let client = self.client.clone();
302 let oid = self.current_oid.clone();
303
304 let fut = Box::pin(async move { client.get_next(&oid).await });
305 self.pending = Some(fut);
306 }
307
308 let pending = self.pending.as_mut().unwrap();
310 match pending.as_mut().poll(cx) {
311 Poll::Pending => Poll::Pending,
312 Poll::Ready(result) => {
313 self.pending = None;
314
315 match result {
316 Ok(vb) => {
317 let target = self.client.peer_addr();
318 let base_oid = self.base_oid.clone();
319 match validate_walk_varbind(&vb, &base_oid, &mut self.oid_tracker, target) {
320 VarbindOutcome::Done => {
321 self.done = true;
322 return Poll::Ready(None);
323 }
324 VarbindOutcome::Abort(e) => {
325 self.done = true;
326 return Poll::Ready(Some(Err(e)));
327 }
328 VarbindOutcome::Yield => {}
329 }
330
331 self.current_oid = vb.oid.clone();
333 self.count += 1;
334
335 Poll::Ready(Some(Ok(vb)))
336 }
337 Err(e) => {
338 if self.client.inner.config.version == Version::V1
339 && matches!(
340 &*e,
341 Error::Snmp {
342 status: crate::error::ErrorStatus::NoSuchName,
343 ..
344 }
345 )
346 {
347 self.done = true;
348 return Poll::Ready(None);
349 }
350
351 self.done = true;
352 Poll::Ready(Some(Err(e)))
353 }
354 }
355 }
356 }
357 }
358}
359
360pub struct BulkWalk<T: Transport> {
364 client: Client<T>,
365 base_oid: Oid,
366 current_oid: Oid,
367 max_repetitions: i32,
368 oid_tracker: OidTracker,
370 max_results: Option<usize>,
372 count: usize,
374 done: bool,
375 buffer: VecDeque<VarBind>,
377 pending: Option<Pin<Box<dyn std::future::Future<Output = Result<Vec<VarBind>>> + Send>>>,
378}
379
380impl<T: Transport> BulkWalk<T> {
381 pub(crate) fn new(
382 client: Client<T>,
383 oid: Oid,
384 max_repetitions: i32,
385 ordering: OidOrdering,
386 max_results: Option<usize>,
387 ) -> Self {
388 Self {
389 client,
390 base_oid: oid.clone(),
391 current_oid: oid,
392 max_repetitions,
393 oid_tracker: OidTracker::new(ordering),
394 max_results,
395 count: 0,
396 done: false,
397 buffer: VecDeque::new(),
398 pending: None,
399 }
400 }
401}
402
403impl_stream_helpers!(BulkWalk<T>);
404
405impl<T: Transport + 'static> Stream for BulkWalk<T> {
406 type Item = Result<VarBind>;
407
408 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
409 loop {
410 if self.done {
411 return Poll::Ready(None);
412 }
413
414 if let Some(max) = self.max_results
416 && self.count >= max
417 {
418 self.done = true;
419 return Poll::Ready(None);
420 }
421
422 if let Some(vb) = self.buffer.pop_front() {
424 let target = self.client.peer_addr();
425 let base_oid = self.base_oid.clone();
426 match validate_walk_varbind(&vb, &base_oid, &mut self.oid_tracker, target) {
427 VarbindOutcome::Done => {
428 self.done = true;
429 return Poll::Ready(None);
430 }
431 VarbindOutcome::Abort(e) => {
432 self.done = true;
433 return Poll::Ready(Some(Err(e)));
434 }
435 VarbindOutcome::Yield => {}
436 }
437
438 self.current_oid = vb.oid.clone();
440 self.count += 1;
441
442 return Poll::Ready(Some(Ok(vb)));
443 }
444
445 if self.pending.is_none() {
447 let client = self.client.clone();
448 let oid = self.current_oid.clone();
449 let max_rep = self.max_repetitions;
450
451 let fut = Box::pin(async move { client.get_bulk(&[oid], 0, max_rep).await });
452 self.pending = Some(fut);
453 }
454
455 let pending = self.pending.as_mut().unwrap();
457 match pending.as_mut().poll(cx) {
458 Poll::Pending => return Poll::Pending,
459 Poll::Ready(result) => {
460 self.pending = None;
461
462 match result {
463 Ok(varbinds) => {
464 if varbinds.is_empty() {
465 self.done = true;
466 return Poll::Ready(None);
467 }
468
469 self.buffer = varbinds.into();
470 }
472 Err(e) => {
473 if self.max_repetitions > 1
478 && matches!(
479 &*e,
480 Error::Snmp {
481 status: crate::error::ErrorStatus::TooBig,
482 ..
483 }
484 )
485 {
486 let reduced = (self.max_repetitions / 2).max(1);
487 tracing::debug!(target: "async_snmp::client", { peer = %self.client.peer_addr(), snmp.max_repetitions = self.max_repetitions, snmp.reduced_max_repetitions = reduced }, "tooBig response, reducing max-repetitions and retrying");
488 self.max_repetitions = reduced;
489 continue;
491 }
492
493 self.done = true;
494 return Poll::Ready(Some(Err(e)));
495 }
496 }
497 }
498 }
499 }
500 }
501}
502
503pub enum WalkStream<T: Transport> {
515 GetNext(Walk<T>),
517 GetBulk(BulkWalk<T>),
519}
520
521impl<T: Transport> WalkStream<T> {
522 pub(crate) fn new(
524 client: Client<T>,
525 oid: Oid,
526 version: Version,
527 walk_mode: WalkMode,
528 ordering: OidOrdering,
529 max_results: Option<usize>,
530 max_repetitions: i32,
531 ) -> Result<Self> {
532 let use_bulk = match walk_mode {
533 WalkMode::Auto => version != Version::V1,
534 WalkMode::GetNext => false,
535 WalkMode::GetBulk => {
536 if version == Version::V1 {
537 return Err(Error::Config("GETBULK is not supported in SNMPv1".into()).boxed());
538 }
539 true
540 }
541 };
542
543 Ok(if use_bulk {
544 WalkStream::GetBulk(BulkWalk::new(
545 client,
546 oid,
547 max_repetitions,
548 ordering,
549 max_results,
550 ))
551 } else {
552 WalkStream::GetNext(Walk::new(client, oid, ordering, max_results))
553 })
554 }
555}
556
557impl<T: Transport + 'static> WalkStream<T> {
558 pub async fn next(&mut self) -> Option<Result<VarBind>> {
560 std::future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
561 }
562
563 pub async fn collect(mut self) -> Result<Vec<VarBind>> {
574 let mut results = Vec::new();
575 while let Some(result) = self.next().await {
576 results.push(result?);
577 }
578 if results.is_empty() {
579 let (client, base_oid, max_results) = match &self {
580 WalkStream::GetNext(w) => (&w.client, &w.base_oid, w.max_results),
581 WalkStream::GetBulk(bw) => (&bw.client, &bw.base_oid, bw.max_results),
582 };
583 walk_scalar_fallback(client, base_oid, max_results, &mut results).await?;
584 }
585 Ok(results)
586 }
587}
588
589impl<T: Transport + 'static> Stream for WalkStream<T> {
590 type Item = Result<VarBind>;
591
592 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
593 match self.get_mut() {
595 WalkStream::GetNext(walk) => Pin::new(walk).poll_next(cx),
596 WalkStream::GetBulk(bulk_walk) => Pin::new(bulk_walk).poll_next(cx),
597 }
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::oid;
605 use crate::value::Value;
606
607 fn target_addr() -> std::net::SocketAddr {
608 "127.0.0.1:161".parse().unwrap()
609 }
610
611 #[test]
612 fn test_walk_terminates_on_no_such_object() {
613 let base = oid!(1, 3, 6, 1, 2, 1, 1);
614 let mut tracker = OidTracker::new(OidOrdering::Strict);
615 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::NoSuchObject);
616 assert!(matches!(
617 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
618 VarbindOutcome::Done
619 ));
620 }
621
622 #[test]
623 fn test_walk_terminates_on_no_such_instance() {
624 let base = oid!(1, 3, 6, 1, 2, 1, 1);
625 let mut tracker = OidTracker::new(OidOrdering::Strict);
626 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::NoSuchInstance);
627 assert!(matches!(
628 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
629 VarbindOutcome::Done
630 ));
631 }
632
633 #[test]
634 fn test_walk_terminates_on_end_of_mib_view() {
635 let base = oid!(1, 3, 6, 1, 2, 1, 1);
636 let mut tracker = OidTracker::new(OidOrdering::Strict);
637 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::EndOfMibView);
638 assert!(matches!(
639 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
640 VarbindOutcome::Done
641 ));
642 }
643
644 #[test]
645 fn test_walk_yields_normal_value() {
646 let base = oid!(1, 3, 6, 1, 2, 1, 1);
647 let mut tracker = OidTracker::new(OidOrdering::Strict);
648 let vb = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(42));
649 assert!(matches!(
650 validate_walk_varbind(&vb, &base, &mut tracker, target_addr()),
651 VarbindOutcome::Yield
652 ));
653 }
654
655 #[test]
656 fn test_walk_strict_aborts_on_non_increasing_oid() {
657 let base = oid!(1, 3, 6, 1, 2, 1, 1);
658 let mut tracker = OidTracker::new(OidOrdering::Strict);
659
660 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
661 assert!(matches!(
662 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
663 VarbindOutcome::Yield
664 ));
665
666 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(2));
668 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
669 VarbindOutcome::Abort(e) => match *e {
670 Error::WalkAborted { reason, .. } => {
671 assert_eq!(reason, WalkAbortReason::NonIncreasing);
672 }
673 other => panic!("expected WalkAborted, got {other:?}"),
674 },
675 _ => panic!("expected Abort outcome"),
676 }
677 }
678
679 #[test]
680 fn test_walk_strict_aborts_on_equal_oid() {
681 let base = oid!(1, 3, 6, 1, 2, 1, 1);
682 let mut tracker = OidTracker::new(OidOrdering::Strict);
683
684 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
685 assert!(matches!(
686 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
687 VarbindOutcome::Yield
688 ));
689
690 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
692 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
693 VarbindOutcome::Abort(e) => match *e {
694 Error::WalkAborted { reason, .. } => {
695 assert_eq!(reason, WalkAbortReason::NonIncreasing);
696 }
697 other => panic!("expected WalkAborted, got {other:?}"),
698 },
699 _ => panic!("expected Abort outcome"),
700 }
701 }
702
703 #[test]
704 fn test_walk_relaxed_aborts_on_duplicate_oid_cycle() {
705 let base = oid!(1, 3, 6, 1, 2, 1, 1);
706 let mut tracker = OidTracker::new(OidOrdering::AllowNonIncreasing);
707
708 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
709 assert!(matches!(
710 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
711 VarbindOutcome::Yield
712 ));
713
714 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 2, 0), Value::Integer(1));
716 match validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()) {
717 VarbindOutcome::Abort(e) => match *e {
718 Error::WalkAborted { reason, .. } => {
719 assert_eq!(reason, WalkAbortReason::Cycle);
720 }
721 other => panic!("expected WalkAborted, got {other:?}"),
722 },
723 _ => panic!("expected Abort outcome"),
724 }
725 }
726
727 #[test]
728 fn test_walk_relaxed_allows_non_increasing_distinct_oid() {
729 let base = oid!(1, 3, 6, 1, 2, 1, 1);
730 let mut tracker = OidTracker::new(OidOrdering::AllowNonIncreasing);
731
732 let vb1 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 3, 0), Value::Integer(1));
734 assert!(matches!(
735 validate_walk_varbind(&vb1, &base, &mut tracker, target_addr()),
736 VarbindOutcome::Yield
737 ));
738
739 let vb2 = VarBind::new(oid!(1, 3, 6, 1, 2, 1, 1, 1, 0), Value::Integer(2));
741 assert!(matches!(
742 validate_walk_varbind(&vb2, &base, &mut tracker, target_addr()),
743 VarbindOutcome::Yield
744 ));
745 }
746
747 use crate::client::ClientConfig;
755 use crate::error::ErrorStatus;
756 use crate::message::CommunityMessage;
757 use crate::pdu::{Pdu, PduType};
758 use bytes::Bytes;
759 use std::collections::VecDeque;
760 use std::net::SocketAddr;
761 use std::sync::{Arc, Mutex};
762
763 #[derive(Clone)]
764 struct BulkTooBigTransport {
765 max_repetitions: i32,
767 pending: Arc<Mutex<VecDeque<(i32, i32)>>>,
769 too_big_count: Arc<Mutex<usize>>,
771 }
772
773 impl BulkTooBigTransport {
774 fn new(max_repetitions: i32) -> Self {
775 Self {
776 max_repetitions,
777 pending: Arc::new(Mutex::new(VecDeque::new())),
778 too_big_count: Arc::new(Mutex::new(0)),
779 }
780 }
781 }
782
783 impl Transport for BulkTooBigTransport {
784 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
785 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
786 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
787 let pdu = msg.pdu.standard().unwrap();
788 let max_rep = pdu.error_index;
790 self.pending
791 .lock()
792 .unwrap()
793 .push_back((request_id, max_rep));
794 async { Ok(()) }
795 }
796
797 fn recv(
798 &self,
799 _request_id: i32,
800 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
801 let (request_id, max_rep) = self.pending.lock().unwrap().pop_front().unwrap_or((1, 0));
802 let threshold = self.max_repetitions;
803 let too_big_count = self.too_big_count.clone();
804 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
805
806 async move {
807 let pdu = if max_rep > threshold {
808 *too_big_count.lock().unwrap() += 1;
809 Pdu {
810 pdu_type: PduType::Response,
811 request_id,
812 error_status: ErrorStatus::TooBig.as_i32(),
813 error_index: 0,
814 varbinds: vec![],
815 }
816 } else {
817 let varbinds = vec![
819 VarBind::new(oid!(1, 3, 6, 1, 2, 1, 2, 1, 0), Value::Integer(1)),
820 VarBind::new(oid!(1, 3, 6, 1, 2, 1, 2, 2, 0), Value::EndOfMibView),
821 ];
822 Pdu {
823 pdu_type: PduType::Response,
824 request_id,
825 error_status: 0,
826 error_index: 0,
827 varbinds,
828 }
829 };
830
831 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
832 Ok((msg.encode(), peer))
833 }
834 }
835
836 fn peer_addr(&self) -> SocketAddr {
837 "127.0.0.1:161".parse().unwrap()
838 }
839
840 fn local_addr(&self) -> SocketAddr {
841 "127.0.0.1:0".parse().unwrap()
842 }
843
844 fn is_reliable(&self) -> bool {
845 true
846 }
847 }
848
849 #[tokio::test]
850 async fn bulk_walk_degrades_max_repetitions_on_too_big() {
851 let transport = BulkTooBigTransport::new(4);
855 let too_big_count = transport.too_big_count.clone();
856 let config = ClientConfig {
857 version: Version::V2c,
858 retry: crate::client::retry::Retry::none(),
859 ..Default::default()
860 };
861 let client = Client::new(transport, config);
862
863 let results = client
864 .bulk_walk(oid!(1, 3, 6, 1, 2, 1, 2), 25)
865 .collect()
866 .await
867 .unwrap();
868
869 assert_eq!(results.len(), 1);
871 assert_eq!(results[0].oid, oid!(1, 3, 6, 1, 2, 1, 2, 1, 0));
872 assert!(*too_big_count.lock().unwrap() >= 1);
874 }
875
876 #[tokio::test]
877 async fn bulk_walk_too_big_at_min_repetitions_is_unrecoverable() {
878 let transport = BulkTooBigTransport::new(0);
881 let config = ClientConfig {
882 version: Version::V2c,
883 retry: crate::client::retry::Retry::none(),
884 ..Default::default()
885 };
886 let client = Client::new(transport, config);
887
888 let err = client
889 .bulk_walk(oid!(1, 3, 6, 1, 2, 1, 2), 8)
890 .collect()
891 .await
892 .unwrap_err();
893
894 assert!(
895 matches!(
896 &*err,
897 Error::Snmp {
898 status: ErrorStatus::TooBig,
899 ..
900 }
901 ),
902 "expected TooBig, got: {err}"
903 );
904 }
905
906 #[derive(Clone, Copy)]
914 enum ScalarGetMode {
915 Value,
917 Absent,
919 Error,
921 }
922
923 #[derive(Clone)]
924 struct ScalarFallbackTransport {
925 mode: ScalarGetMode,
926 pending: Arc<Mutex<VecDeque<(i32, PduType)>>>,
928 }
929
930 impl ScalarFallbackTransport {
931 fn new(mode: ScalarGetMode) -> Self {
932 Self {
933 mode,
934 pending: Arc::new(Mutex::new(VecDeque::new())),
935 }
936 }
937 }
938
939 impl Transport for ScalarFallbackTransport {
940 fn send(&self, data: &[u8]) -> impl std::future::Future<Output = Result<()>> + Send {
941 let request_id = crate::transport::extract_request_id(data).unwrap_or(1);
942 let msg = CommunityMessage::decode(Bytes::copy_from_slice(data)).unwrap();
943 let pdu = msg.pdu.standard().unwrap();
944 self.pending
945 .lock()
946 .unwrap()
947 .push_back((request_id, pdu.pdu_type));
948 async { Ok(()) }
949 }
950
951 fn recv(
952 &self,
953 _request_id: i32,
954 ) -> impl std::future::Future<Output = Result<(Bytes, SocketAddr)>> + Send {
955 let (request_id, pdu_type) = self
956 .pending
957 .lock()
958 .unwrap()
959 .pop_front()
960 .unwrap_or((1, PduType::GetRequest));
961 let mode = self.mode;
962 let peer: SocketAddr = "127.0.0.1:161".parse().unwrap();
963 let base = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
964
965 async move {
966 let pdu = match pdu_type {
967 PduType::GetNextRequest | PduType::GetBulkRequest => Pdu {
969 pdu_type: PduType::Response,
970 request_id,
971 error_status: 0,
972 error_index: 0,
973 varbinds: vec![VarBind::new(
974 oid!(1, 3, 6, 1, 2, 1, 99),
975 Value::EndOfMibView,
976 )],
977 },
978 _ => match mode {
980 ScalarGetMode::Value => Pdu {
981 pdu_type: PduType::Response,
982 request_id,
983 error_status: 0,
984 error_index: 0,
985 varbinds: vec![VarBind::new(base.clone(), Value::Integer(7))],
986 },
987 ScalarGetMode::Absent => Pdu {
988 pdu_type: PduType::Response,
989 request_id,
990 error_status: 0,
991 error_index: 0,
992 varbinds: vec![VarBind::new(base.clone(), Value::NoSuchInstance)],
993 },
994 ScalarGetMode::Error => Pdu {
995 pdu_type: PduType::Response,
996 request_id,
997 error_status: ErrorStatus::GenErr.as_i32(),
998 error_index: 1,
999 varbinds: vec![VarBind::new(base.clone(), Value::Null)],
1000 },
1001 },
1002 };
1003 let msg = CommunityMessage::v2c(Bytes::from_static(b"public"), pdu);
1004 Ok((msg.encode(), peer))
1005 }
1006 }
1007
1008 fn peer_addr(&self) -> SocketAddr {
1009 "127.0.0.1:161".parse().unwrap()
1010 }
1011
1012 fn local_addr(&self) -> SocketAddr {
1013 "127.0.0.1:0".parse().unwrap()
1014 }
1015
1016 fn is_reliable(&self) -> bool {
1017 true
1018 }
1019 }
1020
1021 fn scalar_client(
1022 mode: ScalarGetMode,
1023 max_walk_results: Option<usize>,
1024 ) -> Client<ScalarFallbackTransport> {
1025 let config = ClientConfig {
1026 version: Version::V2c,
1027 retry: crate::client::retry::Retry::none(),
1028 max_walk_results,
1029 ..Default::default()
1030 };
1031 Client::new(ScalarFallbackTransport::new(mode), config)
1032 }
1033
1034 #[tokio::test]
1035 async fn scalar_fallback_agrees_across_collect_paths() {
1036 let base = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
1039 let expected = VarBind::new(base.clone(), Value::Integer(7));
1040
1041 let walk_getnext = scalar_client(ScalarGetMode::Value, None)
1042 .walk_getnext(base.clone())
1043 .collect()
1044 .await
1045 .unwrap();
1046 let bulk_walk = scalar_client(ScalarGetMode::Value, None)
1047 .bulk_walk(base.clone(), 10)
1048 .collect()
1049 .await
1050 .unwrap();
1051 let walk_stream = scalar_client(ScalarGetMode::Value, None)
1052 .walk(base.clone())
1053 .unwrap()
1054 .collect()
1055 .await
1056 .unwrap();
1057
1058 assert_eq!(walk_getnext, vec![expected.clone()]);
1059 assert_eq!(bulk_walk, vec![expected.clone()]);
1060 assert_eq!(walk_stream, vec![expected]);
1061 }
1062
1063 #[tokio::test]
1064 async fn scalar_fallback_swallows_genuine_absence() {
1065 let base = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
1067 for results in [
1068 scalar_client(ScalarGetMode::Absent, None)
1069 .walk_getnext(base.clone())
1070 .collect()
1071 .await
1072 .unwrap(),
1073 scalar_client(ScalarGetMode::Absent, None)
1074 .walk(base.clone())
1075 .unwrap()
1076 .collect()
1077 .await
1078 .unwrap(),
1079 ] {
1080 assert!(results.is_empty());
1081 }
1082 }
1083
1084 #[tokio::test]
1085 async fn scalar_fallback_propagates_non_absence_error() {
1086 let base = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
1089
1090 let err = scalar_client(ScalarGetMode::Error, None)
1091 .walk_getnext(base.clone())
1092 .collect()
1093 .await
1094 .unwrap_err();
1095 assert!(
1096 matches!(
1097 &*err,
1098 Error::Snmp {
1099 status: ErrorStatus::GenErr,
1100 ..
1101 }
1102 ),
1103 "expected GenErr, got: {err}"
1104 );
1105
1106 let err = scalar_client(ScalarGetMode::Error, None)
1107 .walk(base.clone())
1108 .unwrap()
1109 .collect()
1110 .await
1111 .unwrap_err();
1112 assert!(
1113 matches!(
1114 &*err,
1115 Error::Snmp {
1116 status: ErrorStatus::GenErr,
1117 ..
1118 }
1119 ),
1120 "expected GenErr, got: {err}"
1121 );
1122 }
1123
1124 #[tokio::test]
1125 async fn scalar_fallback_respects_max_results_zero() {
1126 let base = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
1129
1130 let walk_getnext = scalar_client(ScalarGetMode::Value, Some(0))
1131 .walk_getnext(base.clone())
1132 .collect()
1133 .await
1134 .unwrap();
1135 let walk_stream = scalar_client(ScalarGetMode::Value, Some(0))
1136 .walk(base.clone())
1137 .unwrap()
1138 .collect()
1139 .await
1140 .unwrap();
1141
1142 assert!(walk_getnext.is_empty());
1143 assert!(walk_stream.is_empty());
1144 }
1145}