1#![cfg_attr(not(feature = "std"), no_std)]
44
45pub mod 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 Err(ParseUrlError { input })
234 }
235 }
236
237 #[must_use]
247 pub fn can_parse(input: &str, base: Option<&str>) -> bool {
248 unsafe {
249 if let Some(base) = base {
250 ffi::ada_can_parse_with_base(
251 input.as_ptr().cast(),
252 input.len(),
253 base.as_ptr().cast(),
254 base.len(),
255 )
256 } else {
257 ffi::ada_can_parse(input.as_ptr().cast(), input.len())
258 }
259 }
260 }
261
262 #[must_use]
264 pub fn host_type(&self) -> HostType {
265 HostType::from(unsafe { ffi::ada_get_host_type(self.0) })
266 }
267
268 #[must_use]
270 pub fn scheme_type(&self) -> SchemeType {
271 SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) })
272 }
273
274 #[must_use]
285 #[cfg(feature = "std")]
286 pub fn origin(&self) -> String {
287 unsafe { ffi::ada_get_origin(self.0) }.to_string()
288 }
289
290 #[must_use]
294 pub fn href(&self) -> &str {
295 unsafe { ffi::ada_get_href(self.0) }.as_str()
296 }
297
298 #[allow(clippy::result_unit_err)]
308 pub fn set_href(&mut self, input: &str) -> SetterResult {
309 setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) })
310 }
311
312 #[must_use]
323 pub fn username(&self) -> &str {
324 unsafe { ffi::ada_get_username(self.0) }.as_str()
325 }
326
327 #[allow(clippy::result_unit_err)]
337 pub fn set_username(&mut self, input: Option<&str>) -> SetterResult {
338 setter_result(unsafe {
339 ffi::ada_set_username(
340 self.0,
341 input.unwrap_or("").as_ptr().cast(),
342 input.map_or(0, str::len),
343 )
344 })
345 }
346
347 #[must_use]
358 pub fn password(&self) -> &str {
359 unsafe { ffi::ada_get_password(self.0) }.as_str()
360 }
361
362 #[allow(clippy::result_unit_err)]
372 pub fn set_password(&mut self, input: Option<&str>) -> SetterResult {
373 setter_result(unsafe {
374 ffi::ada_set_password(
375 self.0,
376 input.unwrap_or("").as_ptr().cast(),
377 input.map_or(0, str::len),
378 )
379 })
380 }
381
382 #[must_use]
396 pub fn port(&self) -> &str {
397 unsafe { ffi::ada_get_port(self.0) }.as_str()
398 }
399
400 #[allow(clippy::result_unit_err)]
410 pub fn set_port(&mut self, input: Option<&str>) -> SetterResult {
411 if let Some(value) = input {
412 setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) })
413 } else {
414 unsafe { ffi::ada_clear_port(self.0) }
415 Ok(())
416 }
417 }
418
419 #[must_use]
437 pub fn hash(&self) -> &str {
438 unsafe { ffi::ada_get_hash(self.0) }.as_str()
439 }
440
441 pub fn set_hash(&mut self, input: Option<&str>) {
451 match input {
452 Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) },
453 None => unsafe { ffi::ada_clear_hash(self.0) },
454 }
455 }
456
457 #[must_use]
468 pub fn host(&self) -> &str {
469 unsafe { ffi::ada_get_host(self.0) }.as_str()
470 }
471
472 #[allow(clippy::result_unit_err)]
482 pub fn set_host(&mut self, input: Option<&str>) -> SetterResult {
483 setter_result(unsafe {
484 ffi::ada_set_host(
485 self.0,
486 input.unwrap_or("").as_ptr().cast(),
487 input.map_or(0, str::len),
488 )
489 })
490 }
491
492 #[must_use]
507 pub fn hostname(&self) -> &str {
508 unsafe { ffi::ada_get_hostname(self.0) }.as_str()
509 }
510
511 #[allow(clippy::result_unit_err)]
521 pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult {
522 setter_result(unsafe {
523 ffi::ada_set_hostname(
524 self.0,
525 input.unwrap_or("").as_ptr().cast(),
526 input.map_or(0, str::len),
527 )
528 })
529 }
530
531 #[must_use]
542 pub fn pathname(&self) -> &str {
543 unsafe { ffi::ada_get_pathname(self.0) }.as_str()
544 }
545
546 #[allow(clippy::result_unit_err)]
556 pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult {
557 setter_result(unsafe {
558 ffi::ada_set_pathname(
559 self.0,
560 input.unwrap_or("").as_ptr().cast(),
561 input.map_or(0, str::len),
562 )
563 })
564 }
565
566 #[must_use]
580 pub fn search(&self) -> &str {
581 unsafe { ffi::ada_get_search(self.0) }.as_str()
582 }
583
584 pub fn set_search(&mut self, input: Option<&str>) {
594 match input {
595 Some(value) => unsafe {
596 ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len());
597 },
598 None => unsafe { ffi::ada_clear_search(self.0) },
599 }
600 }
601
602 #[must_use]
613 pub fn protocol(&self) -> &str {
614 unsafe { ffi::ada_get_protocol(self.0) }.as_str()
615 }
616
617 #[allow(clippy::result_unit_err)]
627 pub fn set_protocol(&mut self, input: &str) -> SetterResult {
628 setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) })
629 }
630
631 #[must_use]
633 pub fn has_credentials(&self) -> bool {
634 unsafe { ffi::ada_has_credentials(self.0) }
635 }
636
637 #[must_use]
639 pub fn has_empty_hostname(&self) -> bool {
640 unsafe { ffi::ada_has_empty_hostname(self.0) }
641 }
642
643 #[must_use]
645 pub fn has_hostname(&self) -> bool {
646 unsafe { ffi::ada_has_hostname(self.0) }
647 }
648
649 #[must_use]
651 pub fn has_non_empty_username(&self) -> bool {
652 unsafe { ffi::ada_has_non_empty_username(self.0) }
653 }
654
655 #[must_use]
657 pub fn has_non_empty_password(&self) -> bool {
658 unsafe { ffi::ada_has_non_empty_password(self.0) }
659 }
660
661 #[must_use]
663 pub fn has_port(&self) -> bool {
664 unsafe { ffi::ada_has_port(self.0) }
665 }
666
667 #[must_use]
669 pub fn has_password(&self) -> bool {
670 unsafe { ffi::ada_has_password(self.0) }
671 }
672
673 #[must_use]
675 pub fn has_hash(&self) -> bool {
676 unsafe { ffi::ada_has_hash(self.0) }
677 }
678
679 #[must_use]
681 pub fn has_search(&self) -> bool {
682 unsafe { ffi::ada_has_search(self.0) }
683 }
684
685 #[must_use]
689 pub fn as_str(&self) -> &str {
690 self.href()
691 }
692
693 #[must_use]
695 pub fn components(&self) -> UrlComponents {
696 unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into()
697 }
698}
699
700#[cfg(feature = "serde")]
704impl serde::Serialize for Url {
705 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
706 where
707 S: serde::Serializer,
708 {
709 serializer.serialize_str(self.as_str())
710 }
711}
712
713#[cfg(feature = "serde")]
717#[cfg(feature = "std")]
718impl<'de> serde::Deserialize<'de> for Url {
719 fn deserialize<D>(deserializer: D) -> Result<Url, D::Error>
720 where
721 D: serde::Deserializer<'de>,
722 {
723 use serde::de::{Error, Unexpected, Visitor};
724
725 struct UrlVisitor;
726
727 impl Visitor<'_> for UrlVisitor {
728 type Value = Url;
729
730 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
731 formatter.write_str("a string representing an URL")
732 }
733
734 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
735 where
736 E: Error,
737 {
738 Url::parse(s, None).map_err(|err| {
739 let err_s = std::format!("{}", err);
740 Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
741 })
742 }
743 }
744
745 deserializer.deserialize_str(UrlVisitor)
746 }
747}
748
749unsafe impl Send for Url {}
751
752unsafe impl Sync for Url {}
754
755impl PartialEq for Url {
757 fn eq(&self, other: &Self) -> bool {
758 self.href() == other.href()
759 }
760}
761
762impl PartialOrd for Url {
763 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
764 Some(self.cmp(other))
765 }
766}
767
768impl Ord for Url {
769 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
770 self.href().cmp(other.href())
771 }
772}
773
774impl hash::Hash for Url {
775 fn hash<H: hash::Hasher>(&self, state: &mut H) {
776 self.href().hash(state);
777 }
778}
779
780impl borrow::Borrow<str> for Url {
781 fn borrow(&self) -> &str {
782 self.href()
783 }
784}
785
786impl AsRef<[u8]> for Url {
787 fn as_ref(&self) -> &[u8] {
788 self.href().as_bytes()
789 }
790}
791
792#[cfg(feature = "std")]
793impl From<Url> for String {
794 fn from(val: Url) -> Self {
795 val.href().to_owned()
796 }
797}
798
799impl fmt::Debug for Url {
800 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801 f.debug_struct("Url")
802 .field("href", &self.href())
803 .field("components", &self.components())
804 .finish()
805 }
806}
807
808impl<'input> TryFrom<&'input str> for Url {
809 type Error = ParseUrlError<&'input str>;
810
811 fn try_from(value: &'input str) -> Result<Self, Self::Error> {
812 Self::parse(value, None)
813 }
814}
815
816#[cfg(feature = "std")]
817impl TryFrom<String> for Url {
818 type Error = ParseUrlError<String>;
819
820 fn try_from(value: String) -> Result<Self, Self::Error> {
821 Self::parse(value, None)
822 }
823}
824
825#[cfg(feature = "std")]
826impl<'input> TryFrom<&'input String> for Url {
827 type Error = ParseUrlError<&'input String>;
828
829 fn try_from(value: &'input String) -> Result<Self, Self::Error> {
830 Self::parse(value, None)
831 }
832}
833
834impl ops::Deref for Url {
835 type Target = str;
836 fn deref(&self) -> &Self::Target {
837 self.href()
838 }
839}
840
841impl AsRef<str> for Url {
842 fn as_ref(&self) -> &str {
843 self.href()
844 }
845}
846
847impl fmt::Display for Url {
848 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849 f.write_str(self.href())
850 }
851}
852
853#[cfg(feature = "std")]
854impl core::str::FromStr for Url {
855 type Err = ParseUrlError<Box<str>>;
856
857 fn from_str(s: &str) -> Result<Self, Self::Err> {
858 Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError {
859 input: input.into(),
860 })
861 }
862}
863
864#[cfg(test)]
865mod test {
866 use super::*;
867
868 #[test]
869 fn should_display_serialization() {
870 let tests = [
871 ("http://example.com/", "http://example.com/"),
872 ("HTTP://EXAMPLE.COM", "http://example.com/"),
873 ("http://user:pwd@domain.com", "http://user:pwd@domain.com/"),
874 (
875 "HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2",
876 "http://example.com/FOO/BAR?K1=V1&K2=V2",
877 ),
878 (
879 "http://example.com/🦀/❤️/",
880 "http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/",
881 ),
882 (
883 "https://example.org/hello world.html",
884 "https://example.org/hello%20world.html",
885 ),
886 (
887 "https://三十六計.org/走為上策/",
888 "https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/",
889 ),
890 ];
891 for (value, expected) in tests {
892 let url = Url::parse(value, None).expect("Should have parsed url");
893 assert_eq!(url.as_str(), expected);
894 }
895 }
896
897 #[test]
898 fn try_from_ok() {
899 let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2");
900 #[cfg(feature = "std")]
901 std::dbg!(&url);
902 let url = url.unwrap();
903 assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2");
904 assert_eq!(
905 url,
906 Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(),
907 );
908 }
909
910 #[test]
911 fn try_from_err() {
912 let url = Url::try_from("this is not a url");
913 #[cfg(feature = "std")]
914 std::dbg!(&url);
915 let error = url.unwrap_err();
916 #[cfg(feature = "std")]
917 assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#);
918 assert_eq!(error.input, "this is not a url");
919 }
920
921 #[test]
922 fn should_compare_urls() {
923 let tests = [
924 ("http://example.com/", "http://example.com/", true),
925 ("http://example.com/", "https://example.com/", false),
926 ("http://example.com#", "https://example.com/#", false),
927 ("http://example.com", "https://example.com#", false),
928 (
929 "https://user:pwd@example.com",
930 "https://user:pwd@example.com",
931 true,
932 ),
933 ];
934 for (left, right, expected) in tests {
935 let left_url = Url::parse(left, None).expect("Should have parsed url");
936 let right_url = Url::parse(right, None).expect("Should have parsed url");
937 assert_eq!(
938 left_url == right_url,
939 expected,
940 "left: {left}, right: {right}, expected: {expected}",
941 );
942 }
943 }
944 #[test]
945 fn should_order_alphabetically() {
946 let left = Url::parse("https://example.com/", None).expect("Should have parsed url");
947 let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url");
948 assert!(left < right);
949 let left = Url::parse("https://c.tld/", None).expect("Should have parsed url");
950 let right = Url::parse("https://a.tld/", None).expect("Should have parsed url");
951 assert!(right < left);
952 }
953
954 #[test]
955 fn should_parse_simple_url() {
956 let mut out = Url::parse(
957 "https://username:password@google.com:9090/search?query#hash",
958 None,
959 )
960 .expect("Should have parsed a simple url");
961
962 #[cfg(feature = "std")]
963 assert_eq!(out.origin(), "https://google.com:9090");
964
965 assert_eq!(
966 out.href(),
967 "https://username:password@google.com:9090/search?query#hash"
968 );
969
970 assert_eq!(out.scheme_type(), SchemeType::Https);
971
972 out.set_username(Some("new-username")).unwrap();
973 assert_eq!(out.username(), "new-username");
974
975 out.set_password(Some("new-password")).unwrap();
976 assert_eq!(out.password(), "new-password");
977
978 out.set_port(Some("4242")).unwrap();
979 assert_eq!(out.port(), "4242");
980 out.set_port(None).unwrap();
981 assert_eq!(out.port(), "");
982
983 out.set_hash(Some("#new-hash"));
984 assert_eq!(out.hash(), "#new-hash");
985
986 out.set_host(Some("yagiz.co:9999")).unwrap();
987 assert_eq!(out.host(), "yagiz.co:9999");
988
989 out.set_hostname(Some("domain.com")).unwrap();
990 assert_eq!(out.hostname(), "domain.com");
991
992 out.set_pathname(Some("/new-search")).unwrap();
993 assert_eq!(out.pathname(), "/new-search");
994 out.set_pathname(None).unwrap();
995 assert_eq!(out.pathname(), "/");
996
997 out.set_search(Some("updated-query"));
998 assert_eq!(out.search(), "?updated-query");
999
1000 out.set_protocol("wss").unwrap();
1001 assert_eq!(out.protocol(), "wss:");
1002 assert_eq!(out.scheme_type(), SchemeType::Wss);
1003
1004 assert!(out.has_credentials());
1005 assert!(out.has_non_empty_username());
1006 assert!(out.has_non_empty_password());
1007 assert!(out.has_search());
1008 assert!(out.has_hash());
1009 assert!(out.has_password());
1010
1011 assert_eq!(out.host_type(), HostType::Domain);
1012 }
1013
1014 #[test]
1015 fn scheme_types() {
1016 assert_eq!(
1017 Url::parse("file:///foo/bar", None)
1018 .expect("bad url")
1019 .scheme_type(),
1020 SchemeType::File
1021 );
1022 assert_eq!(
1023 Url::parse("ws://example.com/ws", None)
1024 .expect("bad url")
1025 .scheme_type(),
1026 SchemeType::Ws
1027 );
1028 assert_eq!(
1029 Url::parse("wss://example.com/wss", None)
1030 .expect("bad url")
1031 .scheme_type(),
1032 SchemeType::Wss
1033 );
1034 assert_eq!(
1035 Url::parse("ftp://example.com/file.txt", None)
1036 .expect("bad url")
1037 .scheme_type(),
1038 SchemeType::Ftp
1039 );
1040 assert_eq!(
1041 Url::parse("http://example.com/file.txt", None)
1042 .expect("bad url")
1043 .scheme_type(),
1044 SchemeType::Http
1045 );
1046 assert_eq!(
1047 Url::parse("https://example.com/file.txt", None)
1048 .expect("bad url")
1049 .scheme_type(),
1050 SchemeType::Https
1051 );
1052 assert_eq!(
1053 Url::parse("foo://example.com", None)
1054 .expect("bad url")
1055 .scheme_type(),
1056 SchemeType::NotSpecial
1057 );
1058 }
1059
1060 #[test]
1061 fn can_parse_simple_url() {
1062 assert!(Url::can_parse("https://google.com", None));
1063 assert!(Url::can_parse("/helo", Some("https://www.google.com")));
1064 }
1065
1066 #[cfg(feature = "std")]
1067 #[cfg(feature = "serde")]
1068 #[test]
1069 fn test_serde_serialize_deserialize() {
1070 let input = "https://www.google.com";
1071 let output = "\"https://www.google.com/\"";
1072 let url = Url::parse(&input, None).unwrap();
1073 assert_eq!(serde_json::to_string(&url).unwrap(), output);
1074
1075 let deserialized: Url = serde_json::from_str(&output).unwrap();
1076 assert_eq!(deserialized.href(), "https://www.google.com/");
1077 }
1078
1079 #[test]
1080 fn should_clone() {
1081 let first = Url::parse("https://lemire.me", None).unwrap();
1082 let mut second = first.clone();
1083 second.set_href("https://yagiz.co").unwrap();
1084 assert_ne!(first.href(), second.href());
1085 assert_eq!(first.href(), "https://lemire.me/");
1086 assert_eq!(second.href(), "https://yagiz.co/");
1087 }
1088
1089 #[test]
1090 fn should_handle_empty_host() {
1091 let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap();
1093 assert_eq!(url.host(), "");
1094 assert_eq!(url.hostname(), "");
1095 }
1096}