Skip to content

Commit

Permalink
Auto merge of #360 - servo:unc, r=SimonSapin+nox
Browse files Browse the repository at this point in the history
Windows: implement conversion to/from UNC paths

This is a rebase of #284 with some additional fixes. Original work by @wfraser, sorry I took so long to get to this!

Fixes #284.

r? @nox for the last two commits.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/rust-url/360)
<!-- Reviewable:end -->
  • Loading branch information
bors-servo authored Jun 13, 2017
2 parents 2641e36 + d1ccb04 commit 167fa4e
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

name = "url"
# When updating version, also modify html_root_url in the lib.rs
version = "1.4.1"
version = "1.5.0"
authors = ["The rust-url developers"]

description = "URL library for Rust, based on the WHATWG URL Standard"
Expand Down
140 changes: 85 additions & 55 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ let css_url = this_document.join("../main.css").unwrap();
assert_eq!(css_url.as_str(), "http://servo.github.io/rust-url/main.css")
*/

#![doc(html_root_url = "https://docs.rs/url/1.4.0")]
#![doc(html_root_url = "https://docs.rs/url/1.5.1")]

#[cfg(feature="rustc-serialize")] extern crate rustc_serialize;
#[macro_use] extern crate matches;
Expand Down Expand Up @@ -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
///
Expand All @@ -1460,17 +1460,17 @@ impl Url {
/// ```
pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> {
let mut serialization = "file://".to_owned();
let path_start = serialization.len() as u32;
path_to_file_url_segments(path.as_ref(), &mut serialization)?;
let host_start = serialization.len() as u32;
let (host_end, host) = path_to_file_url_segments(path.as_ref(), &mut serialization)?;
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: host,
port: None,
path_start: path_start,
path_start: host_end,
query_start: None,
fragment_start: None,
})
Expand All @@ -1479,7 +1479,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.
Expand Down Expand Up @@ -1568,17 +1568,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<PathBuf, ()> {
// 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(())
}
Expand Down Expand Up @@ -1740,11 +1746,13 @@ impl serde::Deserialize for Url {
}

#[cfg(any(unix, target_os = "redox"))]
fn path_to_file_url_segments(path: &Path, serialization: &mut String) -> Result<(), ()> {
fn path_to_file_url_segments(path: &Path, serialization: &mut String)
-> Result<(u32, HostInternal), ()> {
use std::os::unix::prelude::OsStrExt;
if !path.is_absolute() {
return Err(())
}
let host_end = to_u32(serialization.len()).unwrap();
let mut empty = true;
// skip the root component
for component in path.components().skip(1) {
Expand All @@ -1757,37 +1765,50 @@ fn path_to_file_url_segments(path: &Path, serialization: &mut String) -> Result<
// An URL’s path must not be empty.
serialization.push('/');
}
Ok(())
Ok((host_end, HostInternal::None))
}

#[cfg(windows)]
fn path_to_file_url_segments(path: &Path, serialization: &mut String) -> Result<(), ()> {
fn path_to_file_url_segments(path: &Path, serialization: &mut String)
-> Result<(u32, HostInternal), ()> {
path_to_file_url_segments_windows(path, serialization)
}

// Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102
#[cfg_attr(not(windows), allow(dead_code))]
fn path_to_file_url_segments_windows(path: &Path, serialization: &mut String) -> Result<(), ()> {
fn path_to_file_url_segments_windows(path: &Path, serialization: &mut String)
-> Result<(u32, HostInternal), ()> {
use std::path::{Prefix, Component};
if !path.is_absolute() {
return Err(())
}
let mut components = path.components();
let disk = match components.next() {

let host_end;
let host_internal;
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) => {
host_end = to_u32(serialization.len()).unwrap();
host_internal = HostInternal::None;
serialization.push('/');
serialization.push(letter as char);
serialization.push(':');
},
Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => {
let host = Host::parse(server.to_str().ok_or(())?).map_err(|_| ())?;
write!(serialization, "{}", host).unwrap();
host_end = to_u32(serialization.len()).unwrap();
host_internal = host.into();
serialization.push('/');
let share = share.to_str().ok_or(())?;
serialization.extend(percent_encode(share.as_bytes(), PATH_SEGMENT_ENCODE_SET));
},
_ => 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 }
Expand All @@ -1796,15 +1817,19 @@ fn path_to_file_url_segments_windows(path: &Path, serialization: &mut String) ->
serialization.push('/');
serialization.extend(percent_encode(component.as_bytes(), PATH_SEGMENT_ENCODE_SET));
}
Ok(())
Ok((host_end, host_internal))
}

#[cfg(any(unix, target_os = "redox"))]
fn file_url_segments_to_pathbuf(segments: str::Split<char>) -> Result<PathBuf, ()> {
fn file_url_segments_to_pathbuf(host: Option<&str>, segments: str::Split<char>) -> Result<PathBuf, ()> {
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'/');
Expand All @@ -1818,37 +1843,42 @@ fn file_url_segments_to_pathbuf(segments: str::Split<char>) -> Result<PathBuf, (
}

#[cfg(windows)]
fn file_url_segments_to_pathbuf(segments: str::Split<char>) -> Result<PathBuf, ()> {
file_url_segments_to_pathbuf_windows(segments)
fn file_url_segments_to_pathbuf(host: Option<&str>, segments: str::Split<char>) -> Result<PathBuf, ()> {
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<char>) -> Result<PathBuf, ()> {
let first = segments.next().ok_or(())?;
fn file_url_segments_to_pathbuf_windows(host: Option<&str>, mut segments: str::Split<char>) -> Result<PathBuf, ()> {

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 let Some(host) = host {
r"\\".to_owned() + host
} else {
let first = 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 {
Expand Down
27 changes: 27 additions & 0 deletions tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,30 @@ fn test_origin_hash() {
assert_ne!(hash(&opaque_origin), hash(&same_opaque_origin));
assert_ne!(hash(&opaque_origin), hash(&other_opaque_origin));
}

#[test]
fn test_windows_unc_path() {
if !cfg!(windows) {
return
}

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 url = Url::from_file_path(Path::new(r"\\höst\share\path\file.txt")).unwrap();
assert_eq!(url.as_str(), "file://xn--hst-sna/share/path/file.txt");

let url = Url::from_file_path(Path::new(r"\\192.168.0.1\share\path\file.txt")).unwrap();
assert_eq!(url.host(), Some(Host::Ipv4(Ipv4Addr::new(192, 168, 0, 1))));

let path = url.to_file_path().unwrap();
assert_eq!(path.to_str(), Some(r"\\192.168.0.1\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());
}

0 comments on commit 167fa4e

Please sign in to comment.