summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSebastian Wiesner <sebastian@swsnr.de>2018-11-13 21:36:09 +0100
committerSebastian Wiesner <sebastian@swsnr.de>2018-11-13 21:44:49 +0100
commitf5f58e39d38ffbb88c3a92d4fc8088c639fb7cb8 (patch)
tree658b695a8f0e596f772d482490a36895052beb2d /src
parent26467608fc067891e513eb28061f94c96f97b501 (diff)
Always set hostname for file:// URLs
Fixes GH-42
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs5
-rw-r--r--src/terminal/mod.rs6
-rw-r--r--src/terminal/osc.rs134
3 files changed, 138 insertions, 7 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 851fed4..3c74738 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
+ );
+ }
+ }
}