1#![cfg_attr(docsrs, feature(doc_cfg))]
35#![no_std]
36#![deny(unused_must_use)]
37#![deny(missing_docs)]
38
39#[cfg(feature = "std")]
40extern crate std;
41
42extern crate alloc;
43
44pub mod de;
45pub mod ser;
46
47use alloc::borrow::ToOwned;
48use alloc::borrow::Cow;
49#[cfg(feature = "non-compliant-bytes")]
50use alloc::vec::Vec;
51use alloc::string::String;
52use percent_encoding_rfc3986::{PercentDecode, PercentDecodeError};
53#[cfg(feature = "non-compliant-bytes")]
54use either::Either;
55use core::convert::{TryFrom, TryInto};
56use bitcoin::address::NetworkValidation;
57
58pub use de::{DeserializeParams, DeserializationState, DeserializationError};
59pub use ser::SerializeParams;
60
61#[non_exhaustive]
82#[derive(Debug, Clone)]
83pub struct Uri<'a, NetVal = bitcoin::address::NetworkChecked, Extras = NoExtras>
84where
85 NetVal: NetworkValidation,
86{
87 pub address: bitcoin::Address<NetVal>,
91
92 pub amount: Option<bitcoin::Amount>,
94
95 pub label: Option<Param<'a>>,
97
98 pub message: Option<Param<'a>>,
100
101 pub extras: Extras,
103}
104
105impl<NetVal: NetworkValidation, T: Default> Uri<'_, NetVal, T> {
106 pub fn new(address: bitcoin::Address<NetVal>) -> Self {
111 Uri {
112 address,
113 amount: None,
114 label: None,
115 message: None,
116 extras: Default::default(),
117 }
118 }
119}
120
121impl<NetVal: NetworkValidation, T> Uri<'_, NetVal, T> {
122 pub fn with_extras(address: bitcoin::Address<NetVal>, extras: T) -> Self {
127 Uri {
128 address,
129 amount: None,
130 label: None,
131 message: None,
132 extras,
133 }
134 }
135}
136
137#[derive(Debug, Clone)]
144pub struct Param<'a>(ParamInner<'a>);
145
146impl<'a> Param<'a> {
147 fn decode(s: &'a str) -> Result<Self, PercentDecodeError> {
149 Ok(Param(ParamInner::EncodedBorrowed(percent_encoding_rfc3986::percent_decode_str(s)?)))
150 }
151
152 #[cfg(feature = "non-compliant-bytes")]
154 #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
155 pub fn bytes(&self) -> ParamBytes<'_> {
156 ParamBytes(match &self.0 {
157 ParamInner::EncodedBorrowed(decoder) => Either::Left(decoder.clone()),
158 ParamInner::UnencodedBytes(bytes) => Either::Right(bytes.iter().cloned()),
159 ParamInner::UnencodedString(string) => Either::Right(string.as_bytes().iter().cloned()),
160 })
161 }
162
163 #[cfg(feature = "non-compliant-bytes")]
165 #[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
166 pub fn into_bytes(self) -> ParamBytesOwned<'a> {
167 ParamBytesOwned(match self.0 {
168 ParamInner::EncodedBorrowed(decoder) => Either::Left(decoder),
169 ParamInner::UnencodedBytes(Cow::Borrowed(bytes)) => Either::Right(Either::Left(bytes.iter().cloned())),
170 ParamInner::UnencodedBytes(Cow::Owned(bytes)) => Either::Right(Either::Right(bytes.into_iter())),
171 ParamInner::UnencodedString(Cow::Borrowed(string)) => Either::Right(Either::Left(string.as_bytes().iter().cloned())),
172 ParamInner::UnencodedString(Cow::Owned(string)) => Either::Right(Either::Right(Vec::from(string).into_iter())),
173 })
174 }
175
176 fn decode_into_owned<'b>(self) -> Param<'b> {
178 let owned = match self.0 {
179 ParamInner::EncodedBorrowed(decoder) => ParamInner::UnencodedBytes(decoder.collect()),
180 ParamInner::UnencodedString(Cow::Borrowed(value)) => ParamInner::UnencodedString(Cow::Owned(value.to_owned())),
181 ParamInner::UnencodedString(Cow::Owned(value)) => ParamInner::UnencodedString(Cow::Owned(value)),
182 ParamInner::UnencodedBytes(Cow::Borrowed(value)) => ParamInner::UnencodedBytes(Cow::Owned(value.to_owned())),
183 ParamInner::UnencodedBytes(Cow::Owned(value)) => ParamInner::UnencodedBytes(Cow::Owned(value)),
184 };
185 Param(owned)
186 }
187}
188
189impl<'a> From<&'a str> for Param<'a> {
191 fn from(value: &'a str) -> Self {
192 Param(ParamInner::UnencodedString(Cow::Borrowed(value)))
193 }
194}
195
196impl From<String> for Param<'_> {
198 fn from(value: String) -> Self {
199 Param(ParamInner::UnencodedString(Cow::Owned(value)))
200 }
201}
202
203#[cfg(feature = "non-compliant-bytes")]
205#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
206impl<'a> From<&'a [u8]> for Param<'a> {
207 fn from(value: &'a [u8]) -> Self {
208 Param(ParamInner::UnencodedBytes(Cow::Borrowed(value)))
209 }
210}
211
212#[cfg(feature = "non-compliant-bytes")]
214#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
215impl From<Vec<u8>> for Param<'_> {
216 fn from(value: Vec<u8>) -> Self {
217 Param(ParamInner::UnencodedBytes(Cow::Owned(value)))
218 }
219}
220
221#[cfg(feature = "non-compliant-bytes")]
223#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
224impl<'a> From<Param<'a>> for Vec<u8> {
225 fn from(value: Param<'a>) -> Self {
226 match value.0 {
227 ParamInner::EncodedBorrowed(decoder) => decoder.collect(),
228 ParamInner::UnencodedString(Cow::Borrowed(value)) => value.as_bytes().to_owned(),
229 ParamInner::UnencodedString(Cow::Owned(value)) => value.into(),
230 ParamInner::UnencodedBytes(value) => value.into(),
231 }
232 }
233}
234
235#[cfg(feature = "non-compliant-bytes")]
237#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
238impl<'a> From<Param<'a>> for Cow<'a, [u8]> {
239 fn from(value: Param<'a>) -> Self {
240 match value.0 {
241 ParamInner::EncodedBorrowed(decoder) => decoder.into(),
242 ParamInner::UnencodedString(Cow::Borrowed(value)) => Cow::Borrowed(value.as_bytes()),
243 ParamInner::UnencodedString(Cow::Owned(value)) => Cow::Owned(value.into()),
244 ParamInner::UnencodedBytes(value) => value,
245 }
246 }
247}
248
249impl<'a> TryFrom<Param<'a>> for String {
250 type Error = core::str::Utf8Error;
251
252 fn try_from(value: Param<'a>) -> Result<Self, Self::Error> {
253 match value.0 {
254 ParamInner::EncodedBorrowed(decoder) => <Cow<'_, str>>::try_from(decoder).map(Into::into),
255 ParamInner::UnencodedString(value) => Ok(value.into()),
256 ParamInner::UnencodedBytes(Cow::Borrowed(value)) => Ok(core::str::from_utf8(value)?.to_owned()),
257 ParamInner::UnencodedBytes(Cow::Owned(value)) => String::from_utf8(value).map_err(|error| error.utf8_error()),
258 }
259 }
260}
261
262impl<'a> TryFrom<Param<'a>> for Cow<'a, str> {
263 type Error = core::str::Utf8Error;
264
265 fn try_from(value: Param<'a>) -> Result<Self, Self::Error> {
266 match value.0 {
267 ParamInner::EncodedBorrowed(decoder) => decoder.try_into(),
268 ParamInner::UnencodedString(value) => Ok(value),
269 ParamInner::UnencodedBytes(Cow::Borrowed(value)) => Ok(Cow::Borrowed(core::str::from_utf8(value)?)),
270 ParamInner::UnencodedBytes(Cow::Owned(value)) => Ok(Cow::Owned(String::from_utf8(value).map_err(|error| error.utf8_error())?)),
271 }
272 }
273}
274
275#[derive(Debug, Clone)]
276enum ParamInner<'a> {
277 EncodedBorrowed(PercentDecode<'a>),
278 UnencodedBytes(Cow<'a, [u8]>),
279 UnencodedString(Cow<'a, str>),
280}
281
282#[cfg(feature = "non-compliant-bytes")]
286#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
287#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))]
288pub struct ParamBytes<'a>(ParamIterInner<'a, core::iter::Cloned<core::slice::Iter<'a, u8>>>);
289
290#[cfg(feature = "non-compliant-bytes")]
294#[cfg_attr(docsrs, doc(cfg(feature = "non-compliant-bytes")))]
295#[cfg_attr(feature = "non-compliant-bytes", allow(dead_code))]
296pub struct ParamBytesOwned<'a>(ParamIterInner<'a, Either<core::iter::Cloned<core::slice::Iter<'a, u8>>, alloc::vec::IntoIter<u8>>>);
297
298#[cfg(feature = "non-compliant-bytes")]
299type ParamIterInner<'a, T> = either::Either<PercentDecode<'a>, T>;
300
301#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
306pub struct NoExtras;
307
308#[derive(Debug, Default, Copy, Clone)]
310pub struct EmptyState;
311
312impl DeserializeParams<'_> for NoExtras {
313 type DeserializationState = EmptyState;
314}
315
316impl DeserializationError for NoExtras {
317 type Error = core::convert::Infallible;
318}
319
320impl DeserializationState<'_> for EmptyState {
321 type Value = NoExtras;
322
323 fn is_param_known(&self, _key: &str) -> bool {
324 false
325 }
326
327 fn deserialize_temp(&mut self, _key: &str, _value: Param<'_>) -> Result<de::ParamKind, <Self::Value as DeserializationError>::Error> {
328 Ok(de::ParamKind::Unknown)
329 }
330
331 fn finalize(self) -> Result<Self::Value, <Self::Value as DeserializationError>::Error> {
332 Ok(Default::default())
333 }
334}
335
336impl SerializeParams for &NoExtras {
337 type Key = core::convert::Infallible;
338 type Value = core::convert::Infallible;
339 type Iterator = core::iter::Empty<(Self::Key, Self::Value)>;
340
341 fn serialize_params(self) -> Self::Iterator {
342 core::iter::empty()
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use crate::Uri;
349 use alloc::string::ToString;
350 use alloc::borrow::Cow;
351 use core::convert::TryInto;
352
353 fn check_send_sync<T: Send + Sync>() {}
354
355 #[test]
356 fn send_sync() {
357 check_send_sync::<crate::de::UriError>();
358 }
359
360 #[test]
363 fn just_address() {
364 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd";
365 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
366 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
367 assert!(uri.amount.is_none());
368 assert!(uri.label.is_none());
369 assert!(uri.message.is_none());
370
371 assert_eq!(uri.to_string(), input);
372 }
373
374 #[test]
375 fn address_with_name() {
376 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=Luke-Jr";
377 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
378 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
379 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
380 assert_eq!(label, "Luke-Jr");
381 assert!(uri.amount.is_none());
382 assert!(uri.message.is_none());
383
384 assert_eq!(uri.to_string(), input);
385 }
386
387 #[allow(clippy::inconsistent_digit_grouping)] #[test]
389 fn request_20_point_30_btc_to_luke_dash_jr() {
390 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=20.3&label=Luke-Jr";
391 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
392 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
393 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
394 assert_eq!(label, "Luke-Jr");
395 assert_eq!(uri.amount, Some(bitcoin::Amount::from_sat(20_30_000_000)));
396 assert!(uri.message.is_none());
397
398 assert_eq!(uri.to_string(), input);
399 }
400
401 #[allow(clippy::inconsistent_digit_grouping)] #[test]
403 fn request_50_btc_with_message() {
404 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz";
405 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
406 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
407 let message: Cow<'_, str> = uri.message.clone().unwrap().try_into().unwrap();
408 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
409 assert_eq!(uri.amount, Some(bitcoin::Amount::from_sat(50_00_000_000)));
410 assert_eq!(label, "Luke-Jr");
411 assert_eq!(message, "Donation for project xyz");
412
413 assert_eq!(uri.to_string(), input);
414 }
415
416 #[test]
417 fn required_not_understood() {
418 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?req-somethingyoudontunderstand=50&req-somethingelseyoudontget=999";
419 let uri = input.parse::<Uri<'_, _>>();
420 assert!(uri.is_err());
421 }
422
423 #[test]
424 fn required_understood() {
425 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?somethingyoudontunderstand=50&somethingelseyoudontget=999";
426 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
427 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
428 assert!(uri.amount.is_none());
429 assert!(uri.label.is_none());
430 assert!(uri.message.is_none());
431
432 assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd");
433 }
434
435 #[test]
436 fn label_with_rfc3986_param_separator() {
437 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%26bar%20%3D%20baz/blah?;:@";
438 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
439 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
440 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
441 assert_eq!(label, "foo&bar = baz/blah?;:@");
442 assert!(uri.amount.is_none());
443 assert!(uri.message.is_none());
444
445 assert_eq!(uri.to_string(), input);
446 }
447
448 #[test]
449 fn label_with_rfc3986_fragment_separator() {
450 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo%23bar";
451 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
452 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
453 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
454 assert_eq!(label, "foo#bar");
455 assert!(uri.amount.is_none());
456 assert!(uri.message.is_none());
457
458 assert_eq!(uri.to_string(), input);
459 }
460
461 #[test]
462 fn rfc3986_empty_fragment_not_defined_in_bip21() {
463 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#";
464 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
465 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
466 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
467 assert_eq!(label, "foo");
468 assert!(uri.amount.is_none());
469 assert!(uri.message.is_none());
470 assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo");
471 }
472
473 #[test]
474 fn rfc3986_non_empty_fragment_not_defined_in_bip21() {
475 let input = "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo#&message=not%20part%20of%20a%20message";
476 let uri = input.parse::<Uri<'_, _>>().unwrap().require_network(bitcoin::Network::Bitcoin).unwrap();
477 let label: Cow<'_, str> = uri.label.clone().unwrap().try_into().unwrap();
478 assert_eq!(uri.address.to_string(), "1andreas3batLhQa2FawWjeyjCqyBzypd");
479 assert_eq!(label, "foo");
480 assert!(uri.amount.is_none());
481 assert!(uri.message.is_none());
482 assert_eq!(uri.to_string(), "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?label=foo");
483 }
484
485 #[test]
486 fn bad_unicode_scheme() {
487 let input = "bitcoinö:1andreas3batLhQa2FawWjeyjCqyBzypd";
488 let uri = input.parse::<Uri<'_, _>>();
489 assert!(uri.is_err());
490 }
491}