diff --git a/src/lib.rs b/src/lib.rs index f0b7f974..a4dd2890 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1438,7 +1438,7 @@ impl Url { /// Convert a file name as `std::path::Path` into an URL in the `file` scheme. /// /// This returns `Err` if the given path is not absolute or, - /// on Windows, if the prefix is not a disk prefix (e.g. `C:`). + /// on Windows, if the prefix is not a disk prefix (e.g. `C:`) or a UNC prefix (`\\`). /// /// # Examples /// @@ -1460,17 +1460,24 @@ impl Url { /// ``` pub fn from_file_path>(path: P) -> Result { let mut serialization = "file://".to_owned(); - let path_start = serialization.len() as u32; + let host_start = serialization.len() as u32; path_to_file_url_segments(path.as_ref(), &mut serialization)?; + + let host_end = if serialization.starts_with("file:///") { + host_start + } else { + host_start + serialization[host_start as usize ..].find('/').unwrap_or(0) as u32 + }; + Ok(Url { serialization: serialization, scheme_end: "file".len() as u32, - username_end: path_start, - host_start: path_start, - host_end: path_start, - host: HostInternal::None, + username_end: host_start, + host_start: host_start, + host_end: host_end, + host: if host_start == host_end { HostInternal::None } else { HostInternal::Domain }, port: None, - path_start: path_start, + path_start: host_end, query_start: None, fragment_start: None, }) @@ -1479,7 +1486,7 @@ impl Url { /// Convert a directory name as `std::path::Path` into an URL in the `file` scheme. /// /// This returns `Err` if the given path is not absolute or, - /// on Windows, if the prefix is not a disk prefix (e.g. `C:`). + /// on Windows, if the prefix is not a disk prefix (e.g. `C:`) or a UNC prefix (`\\`). /// /// Compared to `from_file_path`, this ensure that URL’s the path has a trailing slash /// so that the entire path is considered when using this URL as a base URL. @@ -1568,17 +1575,23 @@ impl Url { /// let path = url.to_file_path(); /// ``` /// - /// Returns `Err` if the host is neither empty nor `"localhost"`, + /// Returns `Err` if the host is neither empty nor `"localhost"` (except on Windows, where + /// `file:` URLs may have a non-local host), /// or if `Path::new_opt()` returns `None`. /// (That is, if the percent-decoded path contains a NUL byte or, /// for a Windows path, is not UTF-8.) #[inline] pub fn to_file_path(&self) -> Result { - // FIXME: Figure out what to do w.r.t host. - if matches!(self.host(), None | Some(Host::Domain("localhost"))) { - if let Some(segments) = self.path_segments() { - return file_url_segments_to_pathbuf(segments) - } + if let Some(segments) = self.path_segments() { + let host = match self.host() { + None | Some(Host::Domain("localhost")) => None, + Some(_) if cfg!(windows) && self.scheme() == "file" => { + Some(&self.serialization[self.host_start as usize .. self.host_end as usize]) + }, + _ => return Err(()) + }; + + return file_url_segments_to_pathbuf(host, segments); } Err(()) } @@ -1773,21 +1786,24 @@ fn path_to_file_url_segments_windows(path: &Path, serialization: &mut String) -> return Err(()) } let mut components = path.components(); - let disk = match components.next() { + + match components.next() { Some(Component::Prefix(ref p)) => match p.kind() { - Prefix::Disk(byte) => byte, - Prefix::VerbatimDisk(byte) => byte, - _ => return Err(()), + Prefix::Disk(letter) | Prefix::VerbatimDisk(letter) => { + serialization.push('/'); + serialization.push(letter as char); + serialization.push(':'); + }, + Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => { + serialization.push_str(try!(server.to_str().ok_or(()))); + serialization.push('/'); + serialization.push_str(try!(share.to_str().ok_or(()))); + }, + _ => return Err(()) }, - // FIXME: do something with UNC and other prefixes? _ => return Err(()) - }; - - // Start with the prefix, e.g. "C:" - serialization.push('/'); - serialization.push(disk as char); - serialization.push(':'); + } for component in components { if component == Component::RootDir { continue } @@ -1800,11 +1816,15 @@ fn path_to_file_url_segments_windows(path: &Path, serialization: &mut String) -> } #[cfg(any(unix, target_os = "redox"))] -fn file_url_segments_to_pathbuf(segments: str::Split) -> Result { +fn file_url_segments_to_pathbuf(host: Option<&str>, segments: str::Split) -> Result { use std::ffi::OsStr; use std::os::unix::prelude::OsStrExt; use std::path::PathBuf; + if host.is_some() { + return Err(()); + } + let mut bytes = Vec::new(); for segment in segments { bytes.push(b'/'); @@ -1818,37 +1838,41 @@ fn file_url_segments_to_pathbuf(segments: str::Split) -> Result) -> Result { - file_url_segments_to_pathbuf_windows(segments) +fn file_url_segments_to_pathbuf(host: Option<&str>, segments: str::Split) -> Result { + file_url_segments_to_pathbuf_windows(host, segments) } // Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102 #[cfg_attr(not(windows), allow(dead_code))] -fn file_url_segments_to_pathbuf_windows(mut segments: str::Split) -> Result { - let first = segments.next().ok_or(())?; - - let mut string = match first.len() { - 2 => { - if !first.starts_with(parser::ascii_alpha) || first.as_bytes()[1] != b':' { - return Err(()) - } +fn file_url_segments_to_pathbuf_windows(host: Option<&str>, mut segments: str::Split) -> Result { + let mut string = if let Some(host) = host { + r"\\".to_owned() + host + } else { + let first = segments.next().ok_or(())?; + + match first.len() { + 2 => { + if !first.starts_with(parser::ascii_alpha) || first.as_bytes()[1] != b':' { + return Err(()) + } - first.to_owned() - }, + first.to_owned() + }, - 4 => { - if !first.starts_with(parser::ascii_alpha) { - return Err(()) - } - let bytes = first.as_bytes(); - if bytes[1] != b'%' || bytes[2] != b'3' || (bytes[3] != b'a' && bytes[3] != b'A') { - return Err(()) - } + 4 => { + if !first.starts_with(parser::ascii_alpha) { + return Err(()) + } + let bytes = first.as_bytes(); + if bytes[1] != b'%' || bytes[2] != b'3' || (bytes[3] != b'a' && bytes[3] != b'A') { + return Err(()) + } - first[0..1].to_owned() + ":" - }, + first[0..1].to_owned() + ":" + }, - _ => return Err(()), + _ => return Err(()), + } }; for segment in segments { diff --git a/tests/unit.rs b/tests/unit.rs index c2aa93ab..fe4fda73 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -372,3 +372,21 @@ fn define_encode_set_scopes() { m::test(); } + +#[cfg(windows)] +#[test] +fn test_windows_unc_path() { + let url = Url::from_file_path(Path::new(r"\\host\share\path\file.txt")).unwrap(); + assert_eq!(url.as_str(), "file://host/share/path/file.txt"); + + let path = url.to_file_path().unwrap(); + assert_eq!(path.to_str(), Some(r"\\host\share\path\file.txt")); + + // Another way to write these: + let url = Url::from_file_path(Path::new(r"\\?\UNC\host\share\path\file.txt")).unwrap(); + assert_eq!(url.as_str(), "file://host/share/path/file.txt"); + + // Paths starting with "\\.\" (Local Device Paths) are intentionally not supported. + let url = Url::from_file_path(Path::new(r"\\.\some\path\file.txt")); + assert!(url.is_err()); +} \ No newline at end of file