From e2979bd5881e21299170501b758a455395b3ecd6 Mon Sep 17 00:00:00 2001 From: "William R. Fraser" Date: Tue, 28 Feb 2017 01:35:24 -0800 Subject: [PATCH 1/2] Windows: implement conversion to/from UNC paths --- src/lib.rs | 109 ++++++++++++++++++++++++++++++-------------------- tests/unit.rs | 18 +++++++++ 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 49f474ee..9761587c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1437,7 +1437,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 /// @@ -1459,17 +1459,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; try!(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, }) @@ -1478,7 +1485,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. @@ -1572,6 +1579,7 @@ impl Url { /// (That is, if the percent-decoded path contains a NUL byte or, /// for a Windows path, is not UTF-8.) #[inline] + #[cfg(not(windows))] 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"))) { @@ -1582,6 +1590,16 @@ impl Url { Err(()) } + #[inline] + #[cfg(windows)] + pub fn to_file_path(&self) -> Result { + if let Some(segments) = self.path_segments() { + let host = &self.serialization[self.host_start as usize .. self.host_end as usize]; + return file_url_segments_to_pathbuf_windows(host, segments); + } + Err(()) + } + // Private helper methods: #[inline] @@ -1772,21 +1790,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 } @@ -1816,38 +1837,38 @@ fn file_url_segments_to_pathbuf(segments: str::Split) -> Result) -> Result { - file_url_segments_to_pathbuf_windows(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 = try!(segments.next().ok_or(())); +fn file_url_segments_to_pathbuf_windows(host: &str, mut segments: str::Split) -> Result { - let mut string = match first.len() { - 2 => { - if !first.starts_with(parser::ascii_alpha) || first.as_bytes()[1] != b':' { - return Err(()) - } + let mut string = if !host.is_empty() { + r"\\".to_owned() + host + } else { + let first = try!(segments.next().ok_or(())); - first.to_owned() - }, + match first.len() { + 2 => { + if !first.starts_with(parser::ascii_alpha) || first.as_bytes()[1] != b':' { + 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.to_owned() + }, - first[0..1].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(()) + } - _ => return Err(()), + first[0..1].to_owned() + ":" + }, + + _ => return Err(()), + } }; for segment in segments { diff --git a/tests/unit.rs b/tests/unit.rs index 6739956f..dc3d8521 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -342,3 +342,21 @@ fn test_set_host() { url.set_host(None).unwrap(); assert_eq!(url.as_str(), "foobar:/hello"); } + +#[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 From cd486209e2e9ff1b026d58d0bd6d271d88cfea9e Mon Sep 17 00:00:00 2001 From: "William R. Fraser" Date: Tue, 28 Feb 2017 02:05:18 -0800 Subject: [PATCH 2/2] re-structure to avoid #[cfg] on public functions also update the doc comment on `Url::to_file_path`. --- src/lib.rs | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9761587c..a29cbfe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1574,28 +1574,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] - #[cfg(not(windows))] - 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) - } - } - Err(()) - } - - #[inline] - #[cfg(windows)] pub fn to_file_path(&self) -> Result { if let Some(segments) = self.path_segments() { - let host = &self.serialization[self.host_start as usize .. self.host_end as usize]; - return file_url_segments_to_pathbuf_windows(host, 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(()) } @@ -1820,11 +1815,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'/'); @@ -1837,11 +1836,16 @@ fn file_url_segments_to_pathbuf(segments: str::Split) -> Result, 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(host: &str, mut segments: str::Split) -> Result { +fn file_url_segments_to_pathbuf_windows(host: Option<&str>, mut segments: str::Split) -> Result { - let mut string = if !host.is_empty() { + let mut string = if let Some(host) = host { r"\\".to_owned() + host } else { let first = try!(segments.next().ok_or(()));