1use super::*;
8
9#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum ParseError {
13 #[error("Wrong URI scheme, must be 'wormhole-transfer' but was '{_0}'")]
15 SchemeError(String),
16 #[error("Wormhole URIs start with 'wormhole-transfer:${{code}}', they do not have a host")]
18 HasHost,
19 #[error("Code is missing or empty")]
21 MissingCode,
22 #[error("Unsupported scheme version {_0}")]
24 UnsupportedVersion(String),
25 #[error("Invalid 'role' parameter: '{_0}'")]
27 InvalidRole(String),
28 #[error("String does not parse as URL")]
30 UrlParseError(
31 #[from]
32 #[source]
33 url::ParseError,
34 ),
35 #[error("Invalid UTF-8 encoding: {_0}")]
37 Utf8Error(
38 #[from]
39 #[source]
40 std::str::Utf8Error,
41 ),
42 #[error("Error parsing the mailbox code")]
44 ParseCodeError(#[from] ParseCodeError),
45}
46
47#[derive(Debug, Clone, Eq, PartialEq)]
50pub struct WormholeTransferUri {
51 pub code: Code,
53 pub rendezvous_server: Option<url::Url>,
55 pub is_leader: bool,
63}
64
65impl WormholeTransferUri {
66 pub fn new(code: Code) -> Self {
68 Self {
69 code,
70 rendezvous_server: None,
71 is_leader: false,
72 }
73 }
74}
75
76impl TryFrom<&url::Url> for WormholeTransferUri {
77 type Error = ParseError;
78
79 fn try_from(url: &url::Url) -> Result<Self, ParseError> {
80 use std::ops::Deref;
81
82 match url.scheme() {
83 "wormhole-transfer" => {},
84 other => return Err(ParseError::SchemeError(other.into())),
85 }
86 if url.has_host() {
87 return Err(ParseError::HasHost);
88 }
89 let queries = url
90 .query_pairs()
91 .collect::<std::collections::HashMap<_, _>>();
92 match queries.get("version").map(Deref::deref).unwrap_or("0") {
93 "0" => {},
94 unsupported => return Err(ParseError::UnsupportedVersion(unsupported.into())),
95 }
96 let rendezvous_server = queries
97 .get("rendezvous")
98 .map(Deref::deref)
99 .map(url::Url::parse)
100 .transpose()?;
101 let is_leader = match queries.get("role").map(Deref::deref).unwrap_or("follower") {
102 "leader" => true,
103 "follower" => false,
104 invalid => return Err(ParseError::InvalidRole(invalid.into())),
105 };
106 let code: Code = percent_encoding::percent_decode_str(url.path())
107 .decode_utf8()?
108 .parse()
109 .map_err(|e| {
110 if matches!(e, ParseCodeError::Empty) {
112 ParseError::MissingCode
113 } else {
114 e.into()
115 }
116 })?;
117
118 Ok(WormholeTransferUri {
119 code,
120 rendezvous_server,
121 is_leader,
122 })
123 }
124}
125
126impl TryFrom<url::Url> for WormholeTransferUri {
127 type Error = ParseError;
128
129 fn try_from(url: url::Url) -> Result<Self, ParseError> {
130 (&url).try_into()
131 }
132}
133
134impl std::str::FromStr for WormholeTransferUri {
135 type Err = ParseError;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 url::Url::parse(s)?.try_into()
139 }
140}
141
142impl From<&WormholeTransferUri> for url::Url {
143 fn from(val: &WormholeTransferUri) -> Self {
144 let mut url = url::Url::parse("wormhole-transfer:").unwrap();
145 url.set_path(val.code.as_ref());
146 if val.rendezvous_server.is_some() || val.is_leader {
148 let mut query = url.query_pairs_mut();
149 query.clear();
150 if let Some(rendezvous_server) = val.rendezvous_server.as_ref() {
151 query.append_pair("rendezvous", rendezvous_server.as_ref());
152 }
153 if val.is_leader {
154 query.append_pair("role", "leader");
155 }
156 }
157 url
158 }
159}
160
161impl std::fmt::Display for WormholeTransferUri {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 url::Url::from(self).fmt(f)
164 }
165}
166
167#[cfg(test)]
168mod test {
169 use super::*;
170
171 fn test_eq(parsed: WormholeTransferUri, string: &str) {
172 assert_eq!(parsed.to_string(), string);
173 assert_eq!(string.parse::<WormholeTransferUri>().unwrap(), parsed);
174 }
175
176 #[test]
177 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
178 fn test_uri() {
179 test_eq(
180 WormholeTransferUri::new("4-hurricane-equipment".parse().unwrap()),
181 "wormhole-transfer:4-hurricane-equipment",
182 );
183
184 test_eq(
185 WormholeTransferUri::new("8-🙈-🙉-🙊".parse().unwrap()),
186 "wormhole-transfer:8-%F0%9F%99%88-%F0%9F%99%89-%F0%9F%99%8A",
187 );
188
189 test_eq(
190 WormholeTransferUri {
191 code: "8-🙈-🙉-🙊".parse().unwrap(),
192 rendezvous_server: Some(url::Url::parse("ws://localhost:4000").unwrap()),
193 is_leader: true,
194 },
195 "wormhole-transfer:8-%F0%9F%99%88-%F0%9F%99%89-%F0%9F%99%8A?rendezvous=ws%3A%2F%2Flocalhost%3A4000%2F&role=leader"
196 );
197 }
198
199 #[test]
200 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
201 fn test_uri_err() {
202 assert_eq!(
203 "wormhole-transfer:8-%F0%9F%99%88-%F0%9F%99%89-%F0%9F%99%8A?version=42&rendezvous=ws%3A%2F%2Flocalhost%3A4000%2F&role=leader".parse::<WormholeTransferUri>(),
204 Err(ParseError::UnsupportedVersion("42".into()))
205 );
206 assert_eq!(
207 "wormhole-transfer:?rendezvous=ws%3A%2F%2Flocalhost%3A4000%2F&role=leader"
208 .parse::<WormholeTransferUri>(),
209 Err(ParseError::MissingCode)
210 );
211 }
212}