diff options
author | Sebastian Wiesner <sebastian@swsnr.de> | 2018-11-13 21:36:09 +0100 |
---|---|---|
committer | Sebastian Wiesner <sebastian@swsnr.de> | 2018-11-13 21:44:49 +0100 |
commit | f5f58e39d38ffbb88c3a92d4fc8088c639fb7cb8 (patch) | |
tree | 658b695a8f0e596f772d482490a36895052beb2d /src | |
parent | 26467608fc067891e513eb28061f94c96f97b501 (diff) |
Always set hostname for file:// URLs
Fixes GH-42
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/terminal/mod.rs | 6 | ||||
-rw-r--r-- | src/terminal/osc.rs | 134 |
3 files changed, 138 insertions, 7 deletions
@@ -26,6 +26,9 @@ extern crate pulldown_cmark; extern crate syntect; extern crate term_size; +#[cfg(feature = "osc8_links")] +extern crate libc; + #[cfg(feature = "resources")] extern crate url; // Used by remote_resources to actually fetch remote resources over HTTP @@ -619,7 +622,7 @@ fn start_tag<'a, W: Write>(ctx: &mut Context<W>, tag: Tag<'a>) -> Result<(), Err #[cfg(feature = "osc8_links")] LinkCapability::OSC8(ref osc8) => { if let Some(url) = ctx.resources.resolve_reference(&destination) { - osc8.set_link(ctx.output.writer, url.as_str())?; + osc8.set_link_url(ctx.output.writer, url)?; ctx.links.inside_inline_link = true; } } diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index d19279e..8f5ea53 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -120,7 +120,7 @@ impl TerminalCapabilities { TerminalCapabilities { name: "iTerm2".to_string(), style: StyleCapability::Ansi(AnsiStyle), - links: LinkCapability::OSC8(self::osc::OSC8Links), + links: LinkCapability::OSC8(self::osc::OSC8Links::for_localhost()), image: ImageCapability::ITerm2(self::iterm2::ITerm2Images), marks: MarkCapability::ITerm2(self::iterm2::ITerm2Marks), } @@ -131,7 +131,7 @@ impl TerminalCapabilities { TerminalCapabilities { name: "Terminology".to_string(), style: StyleCapability::Ansi(AnsiStyle), - links: LinkCapability::OSC8(self::osc::OSC8Links), + links: LinkCapability::OSC8(self::osc::OSC8Links::for_localhost()), image: ImageCapability::Terminology(self::terminology::TerminologyImages), marks: MarkCapability::None, } @@ -142,7 +142,7 @@ impl TerminalCapabilities { TerminalCapabilities { name: "VTE 50".to_string(), style: StyleCapability::Ansi(AnsiStyle), - links: LinkCapability::OSC8(self::osc::OSC8Links), + links: LinkCapability::OSC8(self::osc::OSC8Links::for_localhost()), image: ImageCapability::None, marks: MarkCapability::None, } diff --git a/src/terminal/osc.rs b/src/terminal/osc.rs index 8e05789..ddf66bf 100644 --- a/src/terminal/osc.rs +++ b/src/terminal/osc.rs @@ -16,6 +16,9 @@ use std::io::{Result, Write}; +#[cfg(feature = "osc8_links")] +use url::{Host, Url}; + /// Write an OSC `command` to this terminal. pub fn write_osc<W: Write>(writer: &mut W, command: &str) -> Result<()> { writer.write_all(&[0x1b, 0x5d])?; @@ -24,16 +27,141 @@ pub fn write_osc<W: Write>(writer: &mut W, command: &str) -> Result<()> { Ok(()) } +/// Get the name of the local host. +/// +/// Wraps [gethostname] in a safe interface. The function doesn’t fail, because +/// POSIX does not specify any errors for [gethostname]. +/// +/// It may panic! if the internal buffer for the hostname is too small, but we +/// use a reasonably large buffer, so we consider any panics from this function +/// as bug which you should report. +#[cfg(all(unix, feature = "osc8_links"))] +pub fn gethostname() -> String { + let mut buffer = vec![0 as u8; 256]; + let returncode = + unsafe { libc::gethostname(buffer.as_mut_ptr() as *mut libc::c_char, buffer.len()) }; + if returncode != 0 { + panic!("gethostname failed! Please report an issue to <https://github.com/lunaryorn/mdcat/issues>!"); + } + let end = buffer + .iter() + .position(|&b| b == 0) + .unwrap_or_else(|| buffer.len()); + String::from_utf8_lossy(&buffer[0..end]).to_string() +} + +#[cfg(feature = "osc8_links")] +pub struct OSC8Links { + hostname: String, +} + +/// Whether the given `url` needs to get an explicit host. +/// +/// [OSC 8] links require that `file://` URLs give an explicit hostname, as +/// received by [gethostname], to disambiguate `file://` printed over SSH +/// connections. +/// +/// This function checks whether we need to explicit set the host of the given +/// `url` to the hostname of this system per `gethostname()`. We do so if `url` +/// is a `file://` URL and the host is +/// +/// * empty, +/// * `localhost`, +/// * or a IPv4/IPv6 loopback address. +/// +/// [OSC 8]: https://git.io/vd4ee +/// [gethostname]: http://pubs.opengroup.org/onlinepubs/009695399/functions/gethostname.html #[cfg(feature = "osc8_links")] -pub struct OSC8Links; +fn url_needs_explicit_host(url: &Url) -> bool { + if url.scheme() == "file" { + match url.host() { + None => true, + Some(Host::Domain("localhost")) => true, + Some(Host::Ipv4(addr)) if addr.is_loopback() => true, + Some(Host::Ipv6(addr)) if addr.is_loopback() => true, + _ => false, + } + } else { + false + } +} #[cfg(feature = "osc8_links")] impl OSC8Links { - pub fn set_link<W: Write>(&self, writer: &mut W, destination: &str) -> Result<()> { - write_osc(writer, &format!("8;;{}", destination)) + /// Create OSC 8 links support for this host. + /// + /// Queries and remembers the hostname of this system as per `gethostname()` + /// to resolve local `file://` URLs. + pub fn for_localhost() -> OSC8Links { + OSC8Links { + hostname: gethostname(), + } } + /// Set a link to the given `destination` URL for subsequent text. + /// + /// Take ownership of `destination` to resolve `file://` URLs for localhost + /// and loopback addresses, and print these with the proper hostname of the + /// local system instead to make `file://` URLs work properly over SSH. + /// + /// See <https://git.io/vd4ee#file-uris-and-the-hostname>. + pub fn set_link_url<W: Write>(&self, writer: &mut W, mut destination: Url) -> Result<()> { + if url_needs_explicit_host(&destination) { + destination.set_host(Some(&self.hostname)).unwrap(); + } + self.set_link(writer, destination.as_str()) + } + + /// Clear the current link if any. pub fn clear_link<W: Write>(&self, writer: &mut W) -> Result<()> { self.set_link(writer, "") } + + fn set_link<W: Write>(&self, writer: &mut W, destination: &str) -> Result<()> { + write_osc(writer, &format!("8;;{}", destination)) + } +} + +#[cfg(test)] +mod tests { + use std::process::*; + + #[test] + #[cfg(all(unix, feature = "osc8_links"))] + fn gethostname() { + let output = Command::new("hostname") + .output() + .expect("failed to get hostname"); + let hostname = String::from_utf8_lossy(&output.stdout); + // Convert both sides to lowercase; hostnames are case-insensitive + // anyway. + assert_eq!( + super::gethostname().to_lowercase(), + hostname.trim_end().to_lowercase() + ); + } + + #[test] + #[cfg(feature = "osc8_links")] + fn url_needs_explicit_host() { + let checks = [ + ("http://example.com/foo/bar", false), + ("file:///foo/bar", true), + ("file://localhost/foo/bar", true), + ("file://127.0.0.1/foo/bar", true), + ("file://[::1]/foo/bar", true), + ]; + + for (url, expected) in checks.into_iter() { + let parsed = super::Url::parse(url).unwrap(); + let needs_host = super::url_needs_explicit_host(&parsed); + assert!( + needs_host == *expected, + "{:?} needs host? {}, but got {}", + parsed, + expected, + needs_host + ); + } + } } |