1use hardy_bpa::async_trait;
9
10#[derive(Debug, Clone)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(feature = "serde", serde(default))]
14#[derive(Default)]
15pub struct Config(
16 pub Vec<hardy_eid_patterns::EidPattern>,
18);
19
20pub struct IpnLegacyFilter {
34 peer_patterns: Vec<hardy_eid_patterns::EidPattern>,
35}
36
37impl IpnLegacyFilter {
38 pub fn new(peer_patterns: Vec<hardy_eid_patterns::EidPattern>) -> Self {
43 Self { peer_patterns }
44 }
45}
46
47#[async_trait]
48impl hardy_bpa::filters::WriteFilter for IpnLegacyFilter {
49 async fn filter(
50 &self,
51 bundle: &hardy_bpa::bundle::Bundle,
52 data: &[u8],
53 ) -> Result<hardy_bpa::filters::RewriteResult, hardy_bpa::Error> {
54 let Some(next_hop) = &bundle.metadata.read_only.next_hop else {
56 return Ok(hardy_bpa::filters::RewriteResult::Continue(None, None));
57 };
58
59 if !self.peer_patterns.iter().any(|p| p.matches(next_hop)) {
60 return Ok(hardy_bpa::filters::RewriteResult::Continue(None, None));
61 }
62
63 let needs_source = matches!(bundle.bundle.id.source, hardy_bpv7::eid::Eid::Ipn { .. });
65 let needs_dest = matches!(bundle.bundle.destination, hardy_bpv7::eid::Eid::Ipn { .. });
66
67 if !needs_source && !needs_dest {
68 return Ok(hardy_bpa::filters::RewriteResult::Continue(None, None));
69 }
70
71 let mut editor = hardy_bpv7::editor::Editor::new(&bundle.bundle, data);
73
74 if let hardy_bpv7::eid::Eid::Ipn {
75 fqnn,
76 service_number,
77 } = &bundle.bundle.id.source
78 {
79 editor = editor
80 .with_source(hardy_bpv7::eid::Eid::LegacyIpn {
81 fqnn: fqnn.clone(),
82 service_number: *service_number,
83 })
84 .map_err(|(_, e)| e)?;
85 }
86
87 if let hardy_bpv7::eid::Eid::Ipn {
88 fqnn,
89 service_number,
90 } = &bundle.bundle.destination
91 {
92 editor = editor
93 .with_destination(hardy_bpv7::eid::Eid::LegacyIpn {
94 fqnn: fqnn.clone(),
95 service_number: *service_number,
96 })
97 .map_err(|(_, e)| e)?;
98 }
99
100 let new_data = editor.rebuild()?;
101
102 Ok(hardy_bpa::filters::RewriteResult::Continue(
103 None,
104 Some(new_data),
105 ))
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use hardy_bpa::bundle::{Bundle, BundleMetadata};
113 use hardy_bpa::filters::{RewriteResult, WriteFilter};
114 use hardy_bpv7::eid::Eid;
115
116 fn make_config(patterns: &[&str]) -> Config {
117 Config(patterns.iter().map(|p| p.parse().unwrap()).collect())
118 }
119
120 fn make_bundle(source: &str, dest: &str, next_hop: Option<&str>) -> (Bundle, Vec<u8>) {
121 let src: Eid = source.parse().unwrap();
122 let dst: Eid = dest.parse().unwrap();
123
124 let (bpv7_bundle, data) = hardy_bpv7::builder::Builder::new(src, dst)
125 .with_payload(std::borrow::Cow::Borrowed(b"test"))
126 .build(hardy_bpv7::creation_timestamp::CreationTimestamp::now())
127 .unwrap();
128
129 let mut metadata = BundleMetadata::default();
130 metadata.read_only.next_hop = next_hop.map(|nh| nh.parse().unwrap());
131
132 let bundle = Bundle {
133 bundle: bpv7_bundle,
134 metadata,
135 };
136 (bundle, data.into())
137 }
138
139 fn make_filter(patterns: &[&str]) -> IpnLegacyFilter {
140 IpnLegacyFilter::new(make_config(patterns).0)
141 }
142
143 #[tokio::test]
145 async fn test_no_next_hop() {
146 let filter = make_filter(&["ipn:*.*"]);
147 let (bundle, data) = make_bundle("ipn:1.1.1", "ipn:1.2.1", None);
148
149 let result = filter.filter(&bundle, &data).await.unwrap();
150 assert!(
151 matches!(result, RewriteResult::Continue(None, None)),
152 "No next-hop should mean no rewrite"
153 );
154 }
155
156 #[tokio::test]
158 async fn test_dtn_no_rewrite() {
159 let filter = make_filter(&["ipn:*.*"]);
160 let (bundle, data) = make_bundle("dtn://node-a/svc", "dtn://node-b/svc", Some("ipn:0.3.0"));
161
162 let result = filter.filter(&bundle, &data).await.unwrap();
163 assert!(
164 matches!(result, RewriteResult::Continue(None, None)),
165 "DTN EIDs should not be rewritten"
166 );
167 }
168
169 #[tokio::test]
175 async fn test_alloc0_non_matching() {
176 let filter = make_filter(&["ipn:0.99.*"]);
177 let (bundle, data) = make_bundle("ipn:0.1.1", "ipn:0.2.1", Some("ipn:0.3.0"));
178
179 let result = filter.filter(&bundle, &data).await.unwrap();
180 assert!(
181 matches!(result, RewriteResult::Continue(None, None)),
182 "Non-matching next-hop should mean no rewrite"
183 );
184 }
185
186 #[tokio::test]
190 async fn test_alloc0_matching() {
191 let filter = make_filter(&["ipn:*.*"]);
192 let (bundle, data) = make_bundle("ipn:0.1.1", "ipn:0.2.1", Some("ipn:0.3.0"));
193
194 let result = filter.filter(&bundle, &data).await.unwrap();
195 let RewriteResult::Continue(None, Some(new_data)) = result else {
196 panic!("Expected rewrite path, got {result:?}");
197 };
198
199 assert_eq!(
202 data,
203 new_data.as_ref(),
204 "allocator_id=0: rewrite should be idempotent"
205 );
206 }
207
208 #[tokio::test]
210 async fn test_alloc1_non_matching() {
211 let filter = make_filter(&["ipn:0.99.*"]);
212 let (bundle, data) = make_bundle("ipn:1.1.1", "ipn:1.2.1", Some("ipn:0.3.0"));
213
214 let result = filter.filter(&bundle, &data).await.unwrap();
215 assert!(
216 matches!(result, RewriteResult::Continue(None, None)),
217 "Non-matching next-hop should mean no rewrite"
218 );
219 }
220
221 #[tokio::test]
224 async fn test_alloc1_matching() {
225 let filter = make_filter(&["ipn:*.*"]);
226 let (bundle, data) = make_bundle("ipn:1.1.1", "ipn:1.2.1", Some("ipn:0.3.0"));
227
228 let result = filter.filter(&bundle, &data).await.unwrap();
229 let RewriteResult::Continue(None, Some(new_data)) = result else {
230 panic!("Expected rewrite path, got {result:?}");
231 };
232
233 assert_ne!(
235 data,
236 new_data.as_ref(),
237 "allocator_id!=0: 3-element should be rewritten to 2-element"
238 );
239
240 let parsed =
242 hardy_bpv7::bundle::ParsedBundle::parse(&new_data, hardy_bpv7::bpsec::no_keys).unwrap();
243
244 assert!(
245 matches!(parsed.bundle.id.source, Eid::LegacyIpn { .. }),
246 "Source should be LegacyIpn, got {:?}",
247 parsed.bundle.id.source
248 );
249 assert!(
250 matches!(parsed.bundle.destination, Eid::LegacyIpn { .. }),
251 "Destination should be LegacyIpn, got {:?}",
252 parsed.bundle.destination
253 );
254 }
255}