1#![cfg_attr(not(feature = "std"), no_std)]
44
45mod ffi;
46mod idna;
47mod url_search_params;
48pub use idna::Idna;
49pub use url_search_params::{
50 UrlSearchParams, UrlSearchParamsEntry, UrlSearchParamsEntryIterator,
51 UrlSearchParamsKeyIterator, UrlSearchParamsValueIterator,
52};
53
54#[cfg(feature = "std")]
55extern crate std;
56
57#[cfg(feature = "std")]
58use std::string::String;
59
60use core::{borrow, ffi::c_uint, fmt, hash, ops};
61
62#[derive(Debug, PartialEq, Eq)]
64pub struct ParseUrlError<Input> {
65 pub input: Input,
67}
68
69impl<Input: core::fmt::Debug> fmt::Display for ParseUrlError<Input> {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "Invalid url: {:?}", self.input)
72 }
73}
74
75#[cfg(feature = "std")] impl<Input: core::fmt::Debug> std::error::Error for ParseUrlError<Input> {}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum HostType {
81 Domain = 0,
82 IPV4 = 1,
83 IPV6 = 2,
84}
85
86impl From<c_uint> for HostType {
87 fn from(value: c_uint) -> Self {
88 match value {
89 0 => Self::Domain,
90 1 => Self::IPV4,
91 2 => Self::IPV6,
92 _ => Self::Domain,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum SchemeType {
100 Http = 0,
101 NotSpecial = 1,
102 Https = 2,
103 Ws = 3,
104 Ftp = 4,
105 Wss = 5,
106 File = 6,
107}
108
109impl From<c_uint> for SchemeType {
110 fn from(value: c_uint) -> Self {
111 match value {
112 0 => Self::Http,
113 1 => Self::NotSpecial,
114 2 => Self::Https,
115 3 => Self::Ws,
116 4 => Self::Ftp,
117 5 => Self::Wss,
118 6 => Self::File,
119 _ => Self::NotSpecial,
120 }
121 }
122}
123
124#[derive(Debug)]
144pub struct UrlComponents {
145 pub protocol_end: u32,
146 pub username_end: u32,
147 pub host_start: u32,
148 pub host_end: u32,
149 pub port: Option<u32>,
150 pub pathname_start: Option<u32>,
151 pub search_start: Option<u32>,
152 pub hash_start: Option<u32>,
153}
154
155impl From<&ffi::ada_url_components> for UrlComponents {
156 fn from(value: &ffi::ada_url_components) -> Self {
157 let port = (value.port != u32::MAX).then_some(value.port);
158 let pathname_start = (value.pathname_start != u32::MAX).then_some(value.pathname_start);
159 let search_start = (value.search_start != u32::MAX).then_some(value.search_start);
160 let hash_start = (value.hash_start != u32::MAX).then_some(value.hash_start);
161 Self {
162 protocol_end: value.protocol_end,
163 username_end: value.username_end,
164 host_start: value.host_start,
165 host_end: value.host_end,
166 port,
167 pathname_start,
168 search_start,
169 hash_start,
170 }
171 }
172}
173
174#[derive(Eq)]
176pub struct Url(*mut ffi::ada_url);
177
178impl Clone for Url {
181 fn clone(&self) -> Self {
182 unsafe { ffi::ada_copy(self.0).into() }
183 }
184}
185
186impl Drop for Url {
187 fn drop(&mut self) {
188 unsafe { ffi::ada_free(self.0) }
189 }
190}
191
192impl From<*mut ffi::ada_url> for Url {
193 fn from(value: *mut ffi::ada_url) -> Self {
194 Self(value)
195 }
196}
197
198type SetterResult = Result<(), ()>;
199
200#[inline]
201const fn setter_result(successful: bool) -> SetterResult {
202 if successful { Ok(()) } else { Err(()) }
203}
204
205impl Url {
206 pub fn parse<Input>(input: Input, base: Option<&str>) -> Result<Self, ParseUrlError<Input>>
215 where
216 Input: AsRef<str>,
217 {
218 let url_aggregator = match base {
219 Some(base) => unsafe {
220 ffi::ada_parse_with_base(
221 input.as_ref().as_ptr().cast(),
222 input.as_ref().len(),
223 base.as_ptr().cast(),
224 base.len(),
225 )
226 },
227 None => unsafe { ffi::ada_parse(input.as_ref().as_ptr().cast(), input.as_ref().len()) },
228 };
229
230 if unsafe { ffi::ada_is_valid(url_aggregator) } {
231 Ok(url_aggregator.into())
232 } else {
233 unsafe { ffi::ada_free(url_aggregator) };
238 Err(ParseUrlError { input })
239 }
240 }
241
242 #[must_use]
252 pub fn can_parse(input: &str, base: Option<&str>) -> bool {
253 unsafe {
254 if let Some(base) = base {
255 ffi::ada_can_parse_with_base(
256 input.as_ptr().cast(),
257 input.len(),
258 base.as_ptr().cast(),
259 base.len(),
260 )
261 } else {
262 ffi::ada_can_parse(input.as_ptr().cast(), input.len())
263 }
264 }
265 }
266
267 #[must_use]
269 pub fn host_type(&self) -> HostType {
270 HostType::from(unsafe { ffi::ada_get_host_type(self.0) })
271 }
272
273 #[must_use]
275 pub fn scheme_type(&self) -> SchemeType {
276 SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) })
277 }
278
279 #[must_use]
290 #[cfg(feature = "std")]
291 pub fn origin(&self) -> String {
292 unsafe { ffi::ada_get_origin(self.0) }.to_string()
293 }
294
295 #[must_use]
299 pub fn href(&self) -> &str {
300 unsafe { ffi::ada_get_href(self.0) }.as_str()
301 }
302
303 #[allow(clippy::result_unit_err)]
313 pub fn set_href(&mut self, input: &str) -> SetterResult {
314 setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) })
315 }
316
317 #[must_use]
328 pub fn username(&self) -> &str {
329 unsafe { ffi::ada_get_username(self.0) }.as_str()
330 }
331
332 #[allow(clippy::result_unit_err)]
342 pub fn set_username(&mut self, input: Option<&str>) -> SetterResult {
343 setter_result(unsafe {
344 ffi::ada_set_username(
345 self.0,
346 input.unwrap_or("").as_ptr().cast(),
347 input.map_or(0, str::len),
348 )
349 })
350 }
351
352 #[must_use]
363 pub fn password(&self) -> &str {
364 unsafe { ffi::ada_get_password(self.0) }.as_str()
365 }
366
367 #[allow(clippy::result_unit_err)]
377 pub fn set_password(&mut self, input: Option<&str>) -> SetterResult {
378 setter_result(unsafe {
379 ffi::ada_set_password(
380 self.0,
381 input.unwrap_or("").as_ptr().cast(),
382 input.map_or(0, str::len),
383 )
384 })
385 }
386
387 #[must_use]
401 pub fn port(&self) -> &str {
402 unsafe { ffi::ada_get_port(self.0) }.as_str()
403 }
404
405 #[allow(clippy::result_unit_err)]
415 pub fn set_port(&mut self, input: Option<&str>) -> SetterResult {
416 if let Some(value) = input {
417 setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) })
418 } else {
419 unsafe { ffi::ada_clear_port(self.0) }
420 Ok(())
421 }
422 }
423
424 #[must_use]
442 pub fn hash(&self) -> &str {
443 unsafe { ffi::ada_get_hash(self.0) }.as_str()
444 }
445
446 pub fn set_hash(&mut self, input: Option<&str>) {
456 match input {
457 Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) },
458 None => unsafe { ffi::ada_clear_hash(self.0) },
459 }
460 }
461
462 #[must_use]
473 pub fn host(&self) -> &str {
474 unsafe { ffi::ada_get_host(self.0) }.as_str()
475 }
476
477 #[allow(clippy::result_unit_err)]
487 pub fn set_host(&mut self, input: Option<&str>) -> SetterResult {
488 setter_result(unsafe {
489 ffi::ada_set_host(
490 self.0,
491 input.unwrap_or("").as_ptr().cast(),
492 input.map_or(0, str::len),
493 )
494 })
495 }
496
497 #[must_use]
512 pub fn hostname(&self) -> &str {
513 unsafe { ffi::ada_get_hostname(self.0) }.as_str()
514 }
515
516 #[allow(clippy::result_unit_err)]
526 pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult {
527 setter_result(unsafe {
528 ffi::ada_set_hostname(
529 self.0,
530 input.unwrap_or("").as_ptr().cast(),
531 input.map_or(0, str::len),
532 )
533 })
534 }
535
536 #[must_use]
547 pub fn pathname(&self) -> &str {
548 unsafe { ffi::ada_get_pathname(self.0) }.as_str()
549 }
550
551 #[allow(clippy::result_unit_err)]
561 pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult {
562 setter_result(unsafe {
563 ffi::ada_set_pathname(
564 self.0,
565 input.unwrap_or("").as_ptr().cast(),
566 input.map_or(0, str::len),
567 )
568 })
569 }
570
571 #[must_use]
585 pub fn search(&self) -> &str {
586 unsafe { ffi::ada_get_search(self.0) }.as_str()
587 }
588
589 pub fn set_search(&mut self, input: Option<&str>) {
599 match input {
600 Some(value) => unsafe {
601 ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len());
602 },
603 None => unsafe { ffi::ada_clear_search(self.0) },
604 }
605 }
606
607 #[must_use]
618 pub fn protocol(&self) -> &str {
619 unsafe { ffi::ada_get_protocol(self.0) }.as_str()
620 }
621
622 #[allow(clippy::result_unit_err)]
632 pub fn set_protocol(&mut self, input: &str) -> SetterResult {
633 setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) })
634 }
635
636 #[must_use]
638 pub fn has_credentials(&self) -> bool {
639 unsafe { ffi::ada_has_credentials(self.0) }
640 }
641
642 #[must_use]
644 pub fn has_empty_hostname(&self) -> bool {
645 unsafe { ffi::ada_has_empty_hostname(self.0) }
646 }
647
648 #[must_use]
650 pub fn has_hostname(&self) -> bool {
651 unsafe { ffi::ada_has_hostname(self.0) }
652 }
653
654 #[must_use]
656 pub fn has_non_empty_username(&self) -> bool {
657 unsafe { ffi::ada_has_non_empty_username(self.0) }
658 }
659
660 #[must_use]
662 pub fn has_non_empty_password(&self) -> bool {
663 unsafe { ffi::ada_has_non_empty_password(self.0) }
664 }
665
666 #[must_use]
668 pub fn has_port(&self) -> bool {
669 unsafe { ffi::ada_has_port(self.0) }
670 }
671
672 #[must_use]
674 pub fn has_password(&self) -> bool {
675 unsafe { ffi::ada_has_password(self.0) }
676 }
677
678 #[must_use]
680 pub fn has_hash(&self) -> bool {
681 unsafe { ffi::ada_has_hash(self.0) }
682 }
683
684 #[must_use]
686 pub fn has_search(&self) -> bool {
687 unsafe { ffi::ada_has_search(self.0) }
688 }
689
690 #[must_use]
694 pub fn as_str(&self) -> &str {
695 self.href()
696 }
697
698 #[must_use]
700 pub fn components(&self) -> UrlComponents {
701 unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into()
702 }
703}
704
705#[cfg(feature = "serde")]
709impl serde::Serialize for Url {
710 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
711 where
712 S: serde::Serializer,
713 {
714 serializer.serialize_str(self.as_str())
715 }
716}
717
718#[cfg(feature = "serde")]
722#[cfg(feature = "std")]
723impl<'de> serde::Deserialize<'de> for Url {
724 fn deserialize<D>(deserializer: D) -> Result<Url, D::Error>
725 where
726 D: serde::Deserializer<'de>,
727 {
728 use serde::de::{Error, Unexpected, Visitor};
729
730 struct UrlVisitor;
731
732 impl Visitor<'_> for UrlVisitor {
733 type Value = Url;
734
735 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
736 formatter.write_str("a string representing an URL")
737 }
738
739 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
740 where
741 E: Error,
742 {
743 Url::parse(s, None).map_err(|err| {
744 let err_s = std::format!("{}", err);
745 Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
746 })
747 }
748 }
749
750 deserializer.deserialize_str(UrlVisitor)
751 }
752}
753
754unsafe impl Send for Url {}
756
757unsafe impl Sync for Url {}
759
760impl PartialEq for Url {
762 fn eq(&self, other: &Self) -> bool {
763 self.href() == other.href()
764 }
765}
766
767impl PartialOrd for Url {
768 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
769 Some(self.cmp(other))
770 }
771}
772
773impl Ord for Url {
774 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
775 self.href().cmp(other.href())
776 }
777}
778
779impl hash::Hash for Url {
780 fn hash<H: hash::Hasher>(&self, state: &mut H) {
781 self.href().hash(state);
782 }
783}
784
785impl borrow::Borrow<str> for Url {
786 fn borrow(&self) -> &str {
787 self.href()
788 }
789}
790
791impl AsRef<[u8]> for Url {
792 fn as_ref(&self) -> &[u8] {
793 self.href().as_bytes()
794 }
795}
796
797#[cfg(feature = "std")]
798impl From<Url> for String {
799 fn from(val: Url) -> Self {
800 val.href().to_owned()
801 }
802}
803
804impl fmt::Debug for Url {
805 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806 f.debug_struct("Url")
807 .field("href", &self.href())
808 .field("components", &self.components())
809 .finish()
810 }
811}
812
813impl<'input> TryFrom<&'input str> for Url {
814 type Error = ParseUrlError<&'input str>;
815
816 fn try_from(value: &'input str) -> Result<Self, Self::Error> {
817 Self::parse(value, None)
818 }
819}
820
821#[cfg(feature = "std")]
822impl TryFrom<String> for Url {
823 type Error = ParseUrlError<String>;
824
825 fn try_from(value: String) -> Result<Self, Self::Error> {
826 Self::parse(value, None)
827 }
828}
829
830#[cfg(feature = "std")]
831impl<'input> TryFrom<&'input String> for Url {
832 type Error = ParseUrlError<&'input String>;
833
834 fn try_from(value: &'input String) -> Result<Self, Self::Error> {
835 Self::parse(value, None)
836 }
837}
838
839impl ops::Deref for Url {
840 type Target = str;
841 fn deref(&self) -> &Self::Target {
842 self.href()
843 }
844}
845
846impl AsRef<str> for Url {
847 fn as_ref(&self) -> &str {
848 self.href()
849 }
850}
851
852impl fmt::Display for Url {
853 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854 f.write_str(self.href())
855 }
856}
857
858#[cfg(feature = "std")]
859impl core::str::FromStr for Url {
860 type Err = ParseUrlError<Box<str>>;
861
862 fn from_str(s: &str) -> Result<Self, Self::Err> {
863 Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError {
864 input: input.into(),
865 })
866 }
867}
868
869#[cfg(test)]
870mod test {
871 use super::*;
872
873 #[test]
874 fn should_display_serialization() {
875 let tests = [
876 ("http://example.com/", "http://example.com/"),
877 ("HTTP://EXAMPLE.COM", "http://example.com/"),
878 ("http://user:pwd@domain.com", "http://user:pwd@domain.com/"),
879 (
880 "HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2",
881 "http://example.com/FOO/BAR?K1=V1&K2=V2",
882 ),
883 (
884 "http://example.com/🦀/❤️/",
885 "http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/",
886 ),
887 (
888 "https://example.org/hello world.html",
889 "https://example.org/hello%20world.html",
890 ),
891 (
892 "https://三十六計.org/走為上策/",
893 "https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/",
894 ),
895 ];
896 for (value, expected) in tests {
897 let url = Url::parse(value, None).expect("Should have parsed url");
898 assert_eq!(url.as_str(), expected);
899 }
900 }
901
902 #[test]
903 fn try_from_ok() {
904 let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2");
905 #[cfg(feature = "std")]
906 std::dbg!(&url);
907 let url = url.unwrap();
908 assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2");
909 assert_eq!(
910 url,
911 Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(),
912 );
913 }
914
915 #[test]
916 fn try_from_err() {
917 let url = Url::try_from("this is not a url");
918 #[cfg(feature = "std")]
919 std::dbg!(&url);
920 let error = url.unwrap_err();
921 #[cfg(feature = "std")]
922 assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#);
923 assert_eq!(error.input, "this is not a url");
924 }
925
926 #[test]
927 fn should_compare_urls() {
928 let tests = [
929 ("http://example.com/", "http://example.com/", true),
930 ("http://example.com/", "https://example.com/", false),
931 ("http://example.com#", "https://example.com/#", false),
932 ("http://example.com", "https://example.com#", false),
933 (
934 "https://user:pwd@example.com",
935 "https://user:pwd@example.com",
936 true,
937 ),
938 ];
939 for (left, right, expected) in tests {
940 let left_url = Url::parse(left, None).expect("Should have parsed url");
941 let right_url = Url::parse(right, None).expect("Should have parsed url");
942 assert_eq!(
943 left_url == right_url,
944 expected,
945 "left: {left}, right: {right}, expected: {expected}",
946 );
947 }
948 }
949 #[test]
950 fn should_order_alphabetically() {
951 let left = Url::parse("https://example.com/", None).expect("Should have parsed url");
952 let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url");
953 assert!(left < right);
954 let left = Url::parse("https://c.tld/", None).expect("Should have parsed url");
955 let right = Url::parse("https://a.tld/", None).expect("Should have parsed url");
956 assert!(right < left);
957 }
958
959 #[test]
960 fn should_parse_simple_url() {
961 let mut out = Url::parse(
962 "https://username:password@google.com:9090/search?query#hash",
963 None,
964 )
965 .expect("Should have parsed a simple url");
966
967 #[cfg(feature = "std")]
968 assert_eq!(out.origin(), "https://google.com:9090");
969
970 assert_eq!(
971 out.href(),
972 "https://username:password@google.com:9090/search?query#hash"
973 );
974
975 assert_eq!(out.scheme_type(), SchemeType::Https);
976
977 out.set_username(Some("new-username")).unwrap();
978 assert_eq!(out.username(), "new-username");
979
980 out.set_password(Some("new-password")).unwrap();
981 assert_eq!(out.password(), "new-password");
982
983 out.set_port(Some("4242")).unwrap();
984 assert_eq!(out.port(), "4242");
985 out.set_port(None).unwrap();
986 assert_eq!(out.port(), "");
987
988 out.set_hash(Some("#new-hash"));
989 assert_eq!(out.hash(), "#new-hash");
990
991 out.set_host(Some("yagiz.co:9999")).unwrap();
992 assert_eq!(out.host(), "yagiz.co:9999");
993
994 out.set_hostname(Some("domain.com")).unwrap();
995 assert_eq!(out.hostname(), "domain.com");
996
997 out.set_pathname(Some("/new-search")).unwrap();
998 assert_eq!(out.pathname(), "/new-search");
999 out.set_pathname(None).unwrap();
1000 assert_eq!(out.pathname(), "/");
1001
1002 out.set_search(Some("updated-query"));
1003 assert_eq!(out.search(), "?updated-query");
1004
1005 out.set_protocol("wss").unwrap();
1006 assert_eq!(out.protocol(), "wss:");
1007 assert_eq!(out.scheme_type(), SchemeType::Wss);
1008
1009 assert!(out.has_credentials());
1010 assert!(out.has_non_empty_username());
1011 assert!(out.has_non_empty_password());
1012 assert!(out.has_search());
1013 assert!(out.has_hash());
1014 assert!(out.has_password());
1015
1016 assert_eq!(out.host_type(), HostType::Domain);
1017 }
1018
1019 #[test]
1020 fn scheme_types() {
1021 assert_eq!(
1022 Url::parse("file:///foo/bar", None)
1023 .expect("bad url")
1024 .scheme_type(),
1025 SchemeType::File
1026 );
1027 assert_eq!(
1028 Url::parse("ws://example.com/ws", None)
1029 .expect("bad url")
1030 .scheme_type(),
1031 SchemeType::Ws
1032 );
1033 assert_eq!(
1034 Url::parse("wss://example.com/wss", None)
1035 .expect("bad url")
1036 .scheme_type(),
1037 SchemeType::Wss
1038 );
1039 assert_eq!(
1040 Url::parse("ftp://example.com/file.txt", None)
1041 .expect("bad url")
1042 .scheme_type(),
1043 SchemeType::Ftp
1044 );
1045 assert_eq!(
1046 Url::parse("http://example.com/file.txt", None)
1047 .expect("bad url")
1048 .scheme_type(),
1049 SchemeType::Http
1050 );
1051 assert_eq!(
1052 Url::parse("https://example.com/file.txt", None)
1053 .expect("bad url")
1054 .scheme_type(),
1055 SchemeType::Https
1056 );
1057 assert_eq!(
1058 Url::parse("foo://example.com", None)
1059 .expect("bad url")
1060 .scheme_type(),
1061 SchemeType::NotSpecial
1062 );
1063 }
1064
1065 #[test]
1066 fn can_parse_simple_url() {
1067 assert!(Url::can_parse("https://google.com", None));
1068 assert!(Url::can_parse("/helo", Some("https://www.google.com")));
1069 }
1070
1071 #[cfg(feature = "std")]
1072 #[cfg(feature = "serde")]
1073 #[test]
1074 fn test_serde_serialize_deserialize() {
1075 let input = "https://www.google.com";
1076 let output = "\"https://www.google.com/\"";
1077 let url = Url::parse(&input, None).unwrap();
1078 assert_eq!(serde_json::to_string(&url).unwrap(), output);
1079
1080 let deserialized: Url = serde_json::from_str(&output).unwrap();
1081 assert_eq!(deserialized.href(), "https://www.google.com/");
1082 }
1083
1084 #[test]
1085 fn should_clone() {
1086 let first = Url::parse("https://lemire.me", None).unwrap();
1087 let mut second = first.clone();
1088 second.set_href("https://yagiz.co").unwrap();
1089 assert_ne!(first.href(), second.href());
1090 assert_eq!(first.href(), "https://lemire.me/");
1091 assert_eq!(second.href(), "https://yagiz.co/");
1092 }
1093
1094 #[test]
1095 fn should_handle_empty_host() {
1096 let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap();
1098 assert_eq!(url.host(), "");
1099 assert_eq!(url.hostname(), "");
1100 }
1101}