gmi/
url.rs

1//! A URL Library made specifically for Gemini clients
2//!
3//! This is a subset of [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2)
4//!
5//! This URL library will NOT interpret anything more than
6//! what is required for a Gemini client. These things are as
7//! shown:
8//!
9//! Components that are required:
10//! - Authority
11//!   - Host
12//!   - !userinfo
13//!
14//! Additional Information:
15//! - Spaces should be percent encoded to %20
16//!
17//! Personal limitations:
18//! - IPv6 hosts are not allowed
19
20/// The main holder of a URL.
21/// This consts of 4 parts:
22/// - An optinal scheme (gemini://),
23/// - An authority (example.com:1234),
24/// - A path (hello/world.gmi)
25/// - A query (?user=23)
26///
27/// An easy way to construct the Url struct is using `try_from()`
28#[derive(Debug, Clone)]
29pub struct Url {
30    /// The scheme of the URL
31    pub scheme: Option<String>,
32    /// The authority of the URL
33    pub authority: Authority,
34    /// The path of the URL
35    pub path: Option<Path>,
36    /// The query portion of the URL
37    pub query: Option<Query>,
38}
39
40impl core::fmt::Display for Url {
41    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
42        // Print the scheme
43        match &self.scheme {
44            Some(s) => write!(f, "{}://", s)?,
45            None => (),
46        }
47        // Then the authority
48        write!(f, "{}", self.authority)?;
49        // Then the path
50        match &self.path {
51            Some(p) => write!(f, "{}", p)?,
52            None => (),
53        }
54        // Then the query
55        match &self.query {
56            Some(q) => write!(f, "{}", q)?,
57            None => (),
58        }
59        Ok(())
60    }
61}
62
63#[derive(Debug, PartialEq, Eq, Clone, Copy)]
64/// An error during the parsing of a URL
65pub enum UrlParseError {
66    /// An error occured during the parsing of the authority
67    /// part of the URL.
68    AuthorityParseError(AuthorityParseError),
69    /// The URL was empty
70    EmptyURL,
71}
72
73impl core::fmt::Display for UrlParseError {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        match self {
76            UrlParseError::AuthorityParseError(e) => write!(f, "URL parse error: {}", e),
77            UrlParseError::EmptyURL => write!(f, "URL Parse error: Empty URL"),
78        }
79    }
80}
81
82impl std::error::Error for UrlParseError {
83    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84        match self {
85            UrlParseError::AuthorityParseError(e) => Some(e),
86            UrlParseError::EmptyURL => None,
87        }
88    }
89}
90
91impl core::convert::TryFrom<&str> for Url {
92    type Error = UrlParseError;
93    /// Parse a URL from a [`&str`] object.
94    ///
95    /// # Example:
96    /// ```
97    /// # use gmi::url::Url;
98    /// # use gmi::url::UrlParseError;
99    /// # fn main() -> Result<(), UrlParseError> {
100    /// use std::convert::TryFrom;
101    /// let url = Url::try_from("gemini://example.com")?;
102    /// assert_eq!(url.scheme, Some(String::from("gemini")));
103    /// assert_eq!(url.authority.host, "example.com");
104    /// assert_eq!(url.path, None);
105    /// assert_eq!(url.query, None);
106    /// # Ok(())
107    /// # }
108    /// ```
109    fn try_from(raw_url: &str) -> Result<Self, UrlParseError> {
110        if raw_url.trim().is_empty() {
111            return Err(UrlParseError::EmptyURL);
112        }
113        let raw_url = raw_url.trim();
114        let mut scheme_ret: Option<String> = None;
115        // Check if there's a scheme
116        let remainder = match raw_url.split_once("://") {
117            Some((s, u)) => {
118                scheme_ret = Some(s.to_string());
119                u
120            }
121            None => raw_url,
122        };
123
124        // Split off the path if there is any
125        let (auth, path) = match remainder.split_once('/') {
126            Some((a, p)) => (a, Some(p)),
127            None => (remainder, None),
128        };
129
130        // Now we get the authority
131        let authority = match Authority::try_from(auth) {
132            Ok(a) => a,
133            Err(e) => return Err(UrlParseError::AuthorityParseError(e)),
134        };
135
136        // Now we see if there's a path
137        if path.is_none() {
138            // There is none, and with no path, there also is no query,
139            // so we'll just exit here
140            return Ok(Self {
141                scheme: scheme_ret,
142                authority,
143                path: None,
144                query: None,
145            });
146        }
147
148        let path = path.unwrap();
149
150        // At this point there is a path, so we'll split off a query
151        let (parsed_path, query_str) = match path.split_once('?') {
152            Some((p, q)) => (Path::from(("/".to_string() + &p).as_ref()), Some(q)),
153            None => (Path::from(("/".to_string() + &path).as_ref()), None),
154        };
155
156        // And now we get the query part, if it exists
157        let query = match query_str {
158            None => None,
159            Some(q) => Some(Query::from(q)),
160        };
161
162        Ok(Self {
163            scheme: scheme_ret,
164            authority,
165            path: Some(parsed_path),
166            query,
167        })
168    }
169}
170
171/// An authority of a URL
172///
173/// An authority of a URL is essentially the
174/// host of the URL. Think "example.com" or "127.0.0.1"
175///
176/// This can also optionally include a port number separated by a colon (:)
177///
178/// For more info see [section 3.2 of the
179/// RFC](https://datatracker.ietf.org/doc/html/rfc3986#section-3.2)
180///
181/// # Constructing the struct
182/// An easy way to construct this struct is to use `try_from()`
183///
184/// ## Example
185/// ```
186/// # use gmi::url::Authority;
187/// # fn main() -> Result<(), gmi::url::AuthorityParseError> {
188/// use std::convert::TryFrom;
189/// let auth = Authority::try_from("example.com:1963")?;
190/// assert_eq!(auth.port, Some(1963));
191/// assert_eq!(auth.host, "example.com");
192/// # Ok(())
193/// # }
194/// ```
195#[derive(Debug, Eq, PartialEq, Clone)]
196pub struct Authority {
197    /// The host portion of the authority
198    pub host: String,
199    /// The optinal port of the authority
200    pub port: Option<u16>,
201}
202
203impl core::fmt::Display for Authority {
204    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
205        write!(
206            f,
207            "{}{}",
208            self.host,
209            match self.port {
210                Some(p) => String::from(":") + &p.to_string(),
211                None => String::from(""),
212            }
213        )
214    }
215}
216
217#[derive(Debug, Eq, PartialEq, Clone, Copy)]
218/// A parsing error for the Authority
219///
220/// This enum contains various possible errors that can occur while
221/// parsing an authority
222pub enum AuthorityParseError {
223    /// Occurs when a port cannot be parsed
224    InvalidPort,
225    /// Occurs when the authority is completely
226    /// missing a host in the first place
227    MissingHost,
228}
229
230impl core::fmt::Display for AuthorityParseError {
231    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
232        match self {
233            AuthorityParseError::InvalidPort => write!(f, "Error parsing authority: Invalid port"),
234            AuthorityParseError::MissingHost => write!(f, "Error parsing authority: Missing host"),
235        }
236    }
237}
238
239impl std::error::Error for AuthorityParseError {}
240
241impl core::convert::TryFrom<&str> for Authority {
242    type Error = AuthorityParseError;
243    fn try_from(s: &str) -> Result<Self, AuthorityParseError> {
244        let host;
245        let mut port = None;
246        // Split once by colon
247        if let Some(new_host) = s.split_once(':') {
248            // Add the host
249            host = String::from(new_host.0);
250            // Get the port
251            if !new_host.1.is_empty() {
252                port = Some({
253                    match new_host.1.parse::<u16>() {
254                        Err(_) => return Err(AuthorityParseError::InvalidPort),
255                        Ok(n) => n,
256                    }
257                })
258            }
259        } else {
260            // The entire thing should be the host so let's do that
261            host = s.to_string();
262        }
263        if host.is_empty() {
264            return Err(AuthorityParseError::MissingHost);
265        }
266        Ok(Self { host, port })
267    }
268}
269
270/// The path part of the URL.
271///
272/// This part is optional in a URL and specifies the specific resource to access
273///
274/// This implementation is based on the API of [`std::path::Path`]
275///
276/// # Constructing this struct
277/// You can just use the `from()` implementations for this. You can also just
278/// construct it from its raw parts, although I do not recommend it.
279///
280/// ## Example:
281///```
282/// # use gmi::url::Path;
283/// # fn main() {
284/// let path = Path::from("/help/me");
285/// assert_eq!(path.to_string(), "/help/me");
286/// # }
287/// ```
288
289#[derive(Debug, PartialEq, Eq, Clone)]
290pub struct Path {
291    /// The raw path string of the URL
292    pub raw_path: String,
293}
294
295impl core::fmt::Display for Path {
296    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297        write!(f, "{}", self.raw_path)
298    }
299}
300
301impl From<&str> for Path {
302    fn from(s: &str) -> Self {
303        Self {
304            raw_path: s.to_owned(),
305        }
306    }
307}
308
309impl Path {
310    /// Returns a path with the parent part
311    ///
312    /// # Example
313    /// ```
314    /// # fn main() {
315    /// use gmi::url::Path;
316    /// assert_eq!(Path::from("/hi/hello/").parent(), Some(Path::from("/hi/")));
317    /// # }
318    /// ```
319    pub fn parent(&self) -> Option<Self> {
320        // If the path is the root, there is no parent
321        if self.raw_path == "/" {
322            return None;
323        }
324        // If the path is empty, there is no parent
325        if self.raw_path == "" {
326            return None;
327        }
328        let raw_path = {
329            // Check if the path ends in a slash
330            if self.raw_path.ends_with('/') {
331                self.raw_path[..self.raw_path.len() - 1].to_owned()
332            } else {
333                self.raw_path.clone()
334            }
335        };
336        // Split on the final / and give that back
337        match raw_path.rsplit_once('/') {
338            None => None,
339            Some((parent,_)) => Some({
340                let mut raw_path = parent.to_owned();
341                raw_path.push('/');
342                Self { raw_path }
343            }),
344        }
345    }
346
347    /// Returns true if the path is an absolute path
348    ///
349    /// # Example
350    /// ```
351    /// # use gmi::url::Path;
352    /// # fn main() {
353    /// let path = Path::from("/a/good/path");
354    /// assert!(path.is_absolute());
355    /// # }
356    /// ```
357    pub fn is_absolute(&self) -> bool {
358        // This is actually pretty simple. A path is absolute if it begins with a slash
359        self.raw_path.starts_with('/')
360    }
361
362    /// Returns true if the path is a relative path
363    ///
364    /// # Example
365    /// ```
366    /// # use gmi::url::Path;
367    /// # fn main() {
368    /// let path = Path::from("a/relative/path");
369    /// assert!(path.is_relative());
370    /// # }
371    /// ```
372    pub fn is_relative(&self) -> bool {
373        !self.is_absolute()
374    }
375
376    /// Returns the file name with no associated hierarchy
377    ///
378    /// # Example
379    /// ```
380    /// # fn main() {
381    /// use gmi::url::Path;
382    /// assert_eq!(Path::from("/help/me.hi").file_name(), Some("me.hi"));
383    /// # }
384    /// ```
385    pub fn file_name(&self) -> Option<&str> {
386        // Is the path empty?
387        if self.raw_path.trim().is_empty() {
388            return None;
389        }
390        // Does the path end in a slash?
391        if self.raw_path.trim().ends_with('/') {
392            return None;
393        }
394
395        return Some(self.raw_path.trim().rsplit_once('/').unwrap().1);
396    }
397
398    /// Creates a new [`Path`] with a different [`Path`] adjoined to `self`.
399    ///
400    /// # Example
401    /// ```
402    /// # fn main() {
403    /// use gmi::url::Path;
404    /// assert_eq!(Path::from("/help/").merge_path(&Path::from("me")), Path::from("/help/me"));
405    /// # }
406    /// ```
407    pub fn merge_path(&self, other_path: &Self) -> Self {
408        // If the other path is empty, we don't need to do anything
409        if other_path.raw_path.trim().is_empty() {
410            return self.clone();
411        }
412        // If the other path is an absolute path, there is no merging and the
413        // other path completely takes over
414        if other_path.is_absolute() {
415            Self {
416                raw_path: other_path.to_string(),
417            }
418        } else {
419            // The other path is relative, so we'll just append the other path to this path.
420            // If this path is empty, we can just set the path to the other path, but absolute.
421            if self.raw_path.trim().is_empty() {
422                let mut new_path = String::from("/");
423                new_path.push_str(&other_path.raw_path);
424                return Self { raw_path: new_path };
425            }
426
427            // If the path ends in a slash, it's already a directory and we can just append our new
428            // path to this path
429            if self.raw_path.ends_with('/') {
430                let mut new_path = self.clone();
431                new_path.raw_path.push_str(&other_path.raw_path);
432                return new_path;
433            }
434
435            // The path ends in some file name, so we'll take that off the path, and then put this
436            // new path on top of it
437            let path = self.raw_path.trim().rsplit_once('/').unwrap().0;
438            let mut new_raw_path = String::from(path);
439            new_raw_path.push('/');
440            new_raw_path.push_str(&other_path.raw_path);
441            Self {
442                raw_path: new_raw_path,
443            }
444        }
445    }
446
447    /// Removes any relative dot pathing from the path.
448    ///
449    /// # Example
450    /// ```
451    /// # use gmi::url::Path;
452    /// # fn main() {
453    /// let mut path = Path::from("/a/dotted/../path/./with/stuff");
454    /// path.dedotify();
455    /// assert_eq!(path.to_string(), "/a/path/with/stuff");
456    /// # }
457    pub fn dedotify(&mut self) {
458        // Input buffer
459        let mut input = self.raw_path.clone();
460        let input_ends_with_slash = input.ends_with('/');
461        // Output buffer
462        let mut output = String::new();
463        while !input.is_empty() && input != "/" {
464            // A. If input buffer starts with "../" or "./" remove them
465            if input.starts_with("../") || input.starts_with("./") {
466                input = input
467                    .trim_start_matches("../")
468                    .trim_start_matches("./")
469                    .to_string();
470            }
471            // B. If input starts with "/./", "/." replace with /
472            if input.starts_with("/./") {
473                input = input.replacen("/./", "/", 1);
474            }
475            if input.starts_with("/.") && !input.starts_with("/..") {
476                input = input.replacen("/.", "/", 1);
477            }
478            // C. If input starts with "/../" or "/.." then replace with "/" and remove
479            // previous path segment from output buffer
480            if input.starts_with("/../") {
481                input = input.replacen("/../", "/", 1);
482                let output_split = output.rsplit_once('/').unwrap_or(("", ""));
483                let output_split = if output_split.1 == "" {
484                    output_split.0.rsplit_once('/').unwrap_or(("", ""))
485                } else {
486                    output_split
487                };
488                output = String::from(output_split.0);
489            }
490            if input.starts_with("/..") {
491                input = input.replacen("/..", "/", 1);
492                let output_split = output.rsplit_once('/').unwrap_or(("", ""));
493                let output_split = if output_split.1 == "" {
494                    output_split.0.rsplit_once('/').unwrap_or(("", ""))
495                } else {
496                    output_split
497                };
498                output = String::from(output_split.0);
499            }
500
501            // D. If the input buffer only consists of "." or ".." then remove it
502            if input == "." || input == ".." {
503                input = String::new();
504            }
505
506            // E. Move the first path segment of the input buffer to the end of the
507            // output buffer and remove it from the input buffer
508            if input.starts_with('/') {
509                input = (&input[1..]).to_string();
510                output.push('/');
511            }
512            let input_clone = input.clone();
513            let (input_left, input_right) = input.split_once('/').unwrap_or((&input_clone, ""));
514            output.push_str(input_left);
515            let mut new_input = String::from('/');
516            new_input.push_str(input_right);
517            input = new_input;
518            println!("out: {}, in: {}", output, input);
519        }
520        if input_ends_with_slash && !output.ends_with('/') {
521            output.push('/');
522        }
523        self.raw_path = output;
524    }
525}
526
527/// The query part of the URL.
528///
529/// This part is optional in a URL and consists of "fragments"
530/// The first query part is the first fragment, and each fragment following is separated by the
531/// character '#'
532#[derive(Debug, Eq, PartialEq, Clone)]
533pub struct Query {
534    /// The various fragments of the query portion of the URL
535    pub fragments: Vec<String>,
536}
537
538impl core::fmt::Display for Query {
539    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
540        write!(f, "?{}", self.fragments[0])?;
541        for p in self.fragments[1..].iter() {
542            write!(f, "#{}", p)?;
543        }
544        Ok(())
545    }
546}
547
548impl From<&str> for Query {
549    /// # NOTE
550    ///
551    /// See [`parse_str`](Query::parse_str) for various
552    /// parsing infos
553    fn from(s: &str) -> Self {
554        Self::parse_str(s)
555    }
556}
557
558impl Query {
559    /// Parses a query part into separate parts. You can also use [`Query::from()`]
560    ///
561    /// This requires the query part to be already separated from the URL. The str will not start
562    /// with a '?', and if it does, the first fragment will contain it
563    ///
564    /// # Examples:
565    /// ```
566    /// # use gmi::url::Query;
567    /// # fn main() {
568    /// let query = Query::parse_str("test#query");
569    /// assert_eq!(query.fragments[0], "test");
570    /// assert_eq!(query.fragments[1], "query");
571    /// # }
572    pub fn parse_str(raw_query: &str) -> Self {
573        Self {
574            fragments: raw_query.split('#').map(|s| String::from(s)).collect(),
575        }
576    }
577}
578
579/// Returns if a specific character is part of the reserved
580/// characters list of the URL
581///
582/// See [section 2.2 of RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) for
583/// more details
584///
585/// # Example
586/// ```
587/// # use gmi::url;
588/// # fn main() {
589/// assert!(url::is_reserved_char('!'));
590/// assert!(!url::is_reserved_char('c'));
591/// # }
592/// ```
593pub fn is_reserved_char(c: char) -> bool {
594    // All alphanumeric characters are unreserved
595    if c.is_alphanumeric() {
596        return false;
597    }
598    match c {
599        '-' | '.' | '_' | '~' => false,
600        _ => true,
601    }
602}
603
604/// Percent encodes any reserved characters in a string
605///
606/// See [section 2.1 of RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1) for
607/// more details
608///
609/// # Example
610/// ```
611/// # use gmi::url;
612/// # fn main() {
613/// let percent_encoding = url::percent_encode_reserved_characters("this is a test");
614/// assert_eq!(percent_encoding, "this%20is%20a%20test");
615/// # }
616/// ```
617pub fn percent_encode_reserved_characters(data: &str) -> String {
618    let mut ret = String::new();
619    for c in data.chars() {
620        if is_reserved_char(c) {
621            ret.push_str(&percent_encode(c));
622        } else {
623            ret.push(c);
624        }
625    }
626    ret
627}
628
629/// Percent encodes a singular character
630/// See [`percent_encode_reserved_characters`] for more info on this
631///
632/// # Example:
633/// ```
634/// # use gmi::url;
635/// # fn main() {
636/// assert_eq!(url::percent_encode('!'), "%21");
637/// # }
638pub fn percent_encode(c: char) -> String {
639    let mut ret = String::new();
640    let mut buf = [0; 4];
641    c.encode_utf8(&mut buf);
642    for i in 0..c.len_utf8() {
643        ret.push_str(&format!("%{:02x}", buf[i]));
644    }
645    ret
646}
647
648#[cfg(test)]
649mod test {
650    //    use crate::url::*;
651    mod bare_fns {
652        use crate::url::*;
653        #[test]
654        fn test_percent_encode() {
655            assert_eq!(percent_encode(' '), "%20");
656            assert_eq!(percent_encode('か'), "%e3%81%8b");
657        }
658        #[test]
659        fn test_percent_encode_reserved_characters() {
660            assert_eq!(
661                percent_encode_reserved_characters("this is a test"),
662                "this%20is%20a%20test"
663            );
664        }
665    }
666    mod authority {
667        use std::convert::TryFrom;
668        use crate::url::*;
669        #[test]
670        fn authority_parse_str_simple() {
671            assert_eq!(
672                Authority::try_from("example.com"),
673                Ok(Authority {
674                    host: "example.com".to_string(),
675                    port: None,
676                })
677            );
678        }
679        #[test]
680        fn authority_parse_str_with_port() {
681            assert_eq!(
682                Authority::try_from("example.com:1234"),
683                Ok(Authority {
684                    host: "example.com".to_string(),
685                    port: Some(1234),
686                })
687            );
688        }
689        #[test]
690        fn authority_parse_str_invalid_port() {
691            assert_eq!(
692                Authority::try_from("example.com:fjdklg"),
693                Err(AuthorityParseError::InvalidPort)
694            );
695        }
696        #[test]
697        fn authority_parse_str_missing_host() {
698            assert_eq!(
699                Authority::try_from(""),
700                Err(AuthorityParseError::MissingHost)
701            );
702            assert_eq!(
703                Authority::try_from(":1323"),
704                Err(AuthorityParseError::MissingHost)
705            );
706        }
707    }
708    mod query {
709        use crate::url::*;
710        #[test]
711        fn query_test() {
712            let q = Query::from("this=test#is_this");
713            assert_eq!(q.fragments, vec!["this=test", "is_this"]);
714        }
715    }
716
717    mod path {
718        use crate::url::*;
719        #[test]
720        fn path_parent() {
721            let path = Path::from("/just/a/test/path.txt");
722            let parent = path.parent().unwrap();
723            assert_eq!(parent.raw_path, "/just/a/test/");
724        }
725        #[test]
726        fn path_ancestors() {
727            /*
728            let path = Path::from("/just/a/test/path.txt");
729            let mut ancestors = path.ancestors();
730            assert_eq!(ancestors.next(), Some(Path::from("/just/a/test/")));
731            assert_eq!(ancestors.next(), Some(Path::from("/just/a/")));
732            assert_eq!(ancestors.next(), Some(Path::from("/just/")));
733            assert_eq!(ancestors.next(), Some(Path::from("/")));
734            assert_eq!(ancestors.next(), None);
735            */
736        }
737        #[test]
738        fn path_merge() {
739            let path_ending_file = Path::from("/a/test/path");
740            let path_ending_dir = Path::from("/a/test/path/");
741            let empty_path = Path::from("");
742            let root_path = Path::from("/");
743            let new_relative_path = Path::from("with/the/new/part");
744            let new_absolute_path = Path::from("/this/is/the/new/part/");
745            assert_eq!(
746                path_ending_file.merge_path(&new_relative_path).raw_path,
747                "/a/test/with/the/new/part"
748            );
749            assert_eq!(
750                path_ending_dir.merge_path(&new_relative_path).raw_path,
751                "/a/test/path/with/the/new/part"
752            );
753
754            assert_eq!(
755                path_ending_file.merge_path(&new_absolute_path).raw_path,
756                "/this/is/the/new/part/"
757            );
758            assert_eq!(
759                path_ending_dir.merge_path(&new_absolute_path).raw_path,
760                "/this/is/the/new/part/"
761            );
762
763            assert_eq!(
764                path_ending_file.merge_path(&empty_path).raw_path,
765                "/a/test/path"
766            );
767            assert_eq!(
768                path_ending_dir.merge_path(&empty_path).raw_path,
769                "/a/test/path/"
770            );
771
772            assert_eq!(path_ending_file.merge_path(&root_path).raw_path, "/");
773            assert_eq!(path_ending_dir.merge_path(&root_path).raw_path, "/");
774
775            assert_eq!(
776                empty_path.merge_path(&new_relative_path).raw_path,
777                "/with/the/new/part"
778            );
779            assert_eq!(
780                empty_path.merge_path(&new_absolute_path).raw_path,
781                "/this/is/the/new/part/"
782            );
783
784            assert_eq!(root_path.merge_path(&empty_path).raw_path, "/");
785            assert_eq!(
786                root_path.merge_path(&new_relative_path).raw_path,
787                "/with/the/new/part"
788            );
789            assert_eq!(
790                root_path.merge_path(&new_absolute_path).raw_path,
791                "/this/is/the/new/part/"
792            );
793        }
794
795        #[test]
796        fn dedotify() {
797            std::thread::sleep(std::time::Duration::from_secs(1));
798            let mut p = Path::from("help/");
799            p.dedotify();
800            assert_eq!(p.raw_path, "help/");
801        }
802    }
803}