1use iroh::RelayUrl;
6use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6};
7
8#[derive(Debug, Default)]
9pub struct SendOptions {
10 pub relay_mode: RelayModeOption,
11 pub ticket_type: AddrInfoOptions,
12 pub magic_ipv4_addr: Option<SocketAddrV4>,
13 pub magic_ipv6_addr: Option<SocketAddrV6>,
14}
15
16#[derive(Debug, Clone, Copy)]
17pub struct ReceiveRetryPolicy {
18 pub size_fetch_retry_limit: u32,
19 pub size_fetch_chunk_size: u64,
20 pub size_fetch_backoff_ms: u64,
21}
22
23impl Default for ReceiveRetryPolicy {
24 fn default() -> Self {
25 Self {
26 size_fetch_retry_limit: 3,
27 size_fetch_chunk_size: 1024 * 1024 * 32,
28 size_fetch_backoff_ms: 250,
29 }
30 }
31}
32
33impl ReceiveRetryPolicy {
34 pub fn validate(self) -> anyhow::Result<()> {
36 anyhow::ensure!(
37 self.size_fetch_retry_limit > 0,
38 "size-fetch retry limit must be greater than zero"
39 );
40 anyhow::ensure!(
41 self.size_fetch_chunk_size > 0,
42 "size-fetch chunk size must be greater than zero"
43 );
44 Ok(())
45 }
46}
47
48#[derive(Debug, Default)]
49pub struct ReceiveOptions {
50 pub output_dir: Option<std::path::PathBuf>,
51 pub relay_mode: RelayModeOption,
52 pub magic_ipv4_addr: Option<SocketAddrV4>,
53 pub magic_ipv6_addr: Option<SocketAddrV6>,
54 pub retry_policy: ReceiveRetryPolicy,
55}
56
57pub trait EndpointOptions: BindAddressOptions {
58 fn relay_mode(&self) -> RelayModeOption;
59}
60
61pub trait BindAddressOptions {
62 fn magic_ipv4_addr(&self) -> Option<SocketAddrV4>;
63 fn magic_ipv6_addr(&self) -> Option<SocketAddrV6>;
64}
65
66impl EndpointOptions for SendOptions {
67 fn relay_mode(&self) -> RelayModeOption {
68 self.relay_mode.clone()
69 }
70}
71
72impl BindAddressOptions for SendOptions {
73 fn magic_ipv4_addr(&self) -> Option<SocketAddrV4> {
74 self.magic_ipv4_addr
75 }
76
77 fn magic_ipv6_addr(&self) -> Option<SocketAddrV6> {
78 self.magic_ipv6_addr
79 }
80}
81
82impl EndpointOptions for ReceiveOptions {
83 fn relay_mode(&self) -> RelayModeOption {
84 self.relay_mode.clone()
85 }
86}
87
88impl BindAddressOptions for ReceiveOptions {
89 fn magic_ipv4_addr(&self) -> Option<SocketAddrV4> {
90 self.magic_ipv4_addr
91 }
92
93 fn magic_ipv6_addr(&self) -> Option<SocketAddrV6> {
94 self.magic_ipv6_addr
95 }
96}
97
98pub fn apply_bind_addrs<T: BindAddressOptions>(
99 mut builder: iroh::endpoint::Builder,
100 options: &T,
101) -> anyhow::Result<iroh::endpoint::Builder> {
102 if let Some(addr) = options.magic_ipv4_addr() {
103 builder = builder.bind_addr(SocketAddr::V4(addr))?;
104 }
105 if let Some(addr) = options.magic_ipv6_addr() {
106 builder = builder.bind_addr(SocketAddr::V6(addr))?;
107 }
108 Ok(builder)
109}
110
111#[derive(Clone, Debug, Default)]
112pub enum RelayModeOption {
113 Disabled,
114 #[default]
115 Default,
116 Custom(RelayUrl),
117}
118
119impl From<RelayModeOption> for iroh::RelayMode {
120 fn from(value: RelayModeOption) -> Self {
121 match value {
122 RelayModeOption::Disabled => Self::Disabled,
123 RelayModeOption::Default => Self::Default,
124 RelayModeOption::Custom(url) => Self::Custom(url.into()),
125 }
126 }
127}
128
129impl std::str::FromStr for RelayModeOption {
130 type Err = anyhow::Error;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 match s {
134 "disabled" => Ok(Self::Disabled),
135 "default" => Ok(Self::Default),
136 _ => Ok(Self::Custom(RelayUrl::from_str(s)?)),
137 }
138 }
139}
140
141impl std::fmt::Display for RelayModeOption {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Disabled => f.write_str("disabled"),
145 Self::Default => f.write_str("default"),
146 Self::Custom(url) => url.fmt(f),
147 }
148 }
149}
150
151#[derive(
152 Copy,
153 Clone,
154 PartialEq,
155 Eq,
156 Default,
157 Debug,
158 derive_more::Display,
159 derive_more::FromStr,
160 serde::Serialize,
161 serde::Deserialize,
162)]
163pub enum AddrInfoOptions {
164 #[default]
165 Id,
166 RelayAndAddresses,
167 Relay,
168 Addresses,
169}
170pub fn apply_options(addr: &mut iroh::EndpointAddr, opts: AddrInfoOptions) {
171 use iroh::TransportAddr;
172 match opts {
173 AddrInfoOptions::Id => {
174 addr.addrs = Default::default();
175 }
176 AddrInfoOptions::RelayAndAddresses => {
177 }
179 AddrInfoOptions::Relay => {
180 addr.addrs = addr
181 .addrs
182 .iter()
183 .filter(|addr| matches!(addr, TransportAddr::Relay(_)))
184 .cloned()
185 .collect();
186 }
187 AddrInfoOptions::Addresses => {
188 addr.addrs = addr
189 .addrs
190 .iter()
191 .filter(|addr| matches!(addr, TransportAddr::Ip(_)))
192 .cloned()
193 .collect();
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::ReceiveRetryPolicy;
201
202 #[test]
203 fn receive_retry_policy_defaults_match_receiver_expectations() {
204 let policy = ReceiveRetryPolicy::default();
205 assert_eq!(policy.size_fetch_retry_limit, 3);
206 assert_eq!(policy.size_fetch_chunk_size, 1024 * 1024 * 32);
207 assert_eq!(policy.size_fetch_backoff_ms, 250);
208 }
209
210 #[test]
211 fn receive_retry_policy_rejects_zero_retry_limit() {
212 let policy = ReceiveRetryPolicy {
213 size_fetch_retry_limit: 0,
214 ..Default::default()
215 };
216
217 let error = policy
218 .validate()
219 .expect_err("zero retries should be rejected");
220 assert!(error.to_string().contains("retry limit"));
221 }
222
223 #[test]
224 fn receive_retry_policy_rejects_zero_chunk_size() {
225 let policy = ReceiveRetryPolicy {
226 size_fetch_chunk_size: 0,
227 ..Default::default()
228 };
229
230 let error = policy
231 .validate()
232 .expect_err("zero chunk size should be rejected");
233 assert!(error.to_string().contains("chunk size"));
234 }
235
236 #[test]
237 fn receive_retry_policy_allows_zero_backoff() {
238 let policy = ReceiveRetryPolicy {
239 size_fetch_backoff_ms: 0,
240 ..Default::default()
241 };
242
243 policy
244 .validate()
245 .expect("zero backoff should allow immediate retries");
246 }
247}