use std::{cell::RefCell, io, path::Path, sync::Arc};
use {
bstr::ByteSlice,
termcolor::{HyperlinkSpec, WriteColor},
};
use crate::{hyperlink_aliases, util::DecimalFormatter};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HyperlinkConfig(Arc<HyperlinkConfigInner>);
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct HyperlinkConfigInner {
env: HyperlinkEnvironment,
format: HyperlinkFormat,
}
impl HyperlinkConfig {
pub fn new(
env: HyperlinkEnvironment,
format: HyperlinkFormat,
) -> HyperlinkConfig {
HyperlinkConfig(Arc::new(HyperlinkConfigInner { env, format }))
}
pub(crate) fn environment(&self) -> &HyperlinkEnvironment {
&self.0.env
}
pub(crate) fn format(&self) -> &HyperlinkFormat {
&self.0.format
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HyperlinkFormat {
parts: Vec<Part>,
is_line_dependent: bool,
}
impl HyperlinkFormat {
pub fn empty() -> HyperlinkFormat {
HyperlinkFormat::default()
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
pub fn into_config(self, env: HyperlinkEnvironment) -> HyperlinkConfig {
HyperlinkConfig::new(env, self)
}
pub(crate) fn is_line_dependent(&self) -> bool {
self.is_line_dependent
}
}
impl std::str::FromStr for HyperlinkFormat {
type Err = HyperlinkFormatError;
fn from_str(s: &str) -> Result<HyperlinkFormat, HyperlinkFormatError> {
use self::HyperlinkFormatErrorKind::*;
#[derive(Debug)]
enum State {
Verbatim,
VerbatimCloseVariable,
OpenVariable,
InVariable,
}
let mut builder = FormatBuilder::new();
let input = match hyperlink_aliases::find(s) {
Some(format) => format,
None => s,
};
let mut name = String::new();
let mut state = State::Verbatim;
let err = |kind| HyperlinkFormatError { kind };
for ch in input.chars() {
state = match state {
State::Verbatim => {
if ch == '{' {
State::OpenVariable
} else if ch == '}' {
State::VerbatimCloseVariable
} else {
builder.append_char(ch);
State::Verbatim
}
}
State::VerbatimCloseVariable => {
if ch == '}' {
builder.append_char('}');
State::Verbatim
} else {
return Err(err(InvalidCloseVariable));
}
}
State::OpenVariable => {
if ch == '{' {
builder.append_char('{');
State::Verbatim
} else {
name.clear();
if ch == '}' {
builder.append_var(&name)?;
State::Verbatim
} else {
name.push(ch);
State::InVariable
}
}
}
State::InVariable => {
if ch == '}' {
builder.append_var(&name)?;
State::Verbatim
} else {
name.push(ch);
State::InVariable
}
}
};
}
match state {
State::Verbatim => builder.build(),
State::VerbatimCloseVariable => Err(err(InvalidCloseVariable)),
State::OpenVariable | State::InVariable => {
Err(err(UnclosedVariable))
}
}
}
}
impl std::fmt::Display for HyperlinkFormat {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for part in self.parts.iter() {
part.fmt(f)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct HyperlinkEnvironment {
host: Option<String>,
wsl_prefix: Option<String>,
}
impl HyperlinkEnvironment {
pub fn new() -> HyperlinkEnvironment {
HyperlinkEnvironment::default()
}
pub fn host(&mut self, host: Option<String>) -> &mut HyperlinkEnvironment {
self.host = host;
self
}
pub fn wsl_prefix(
&mut self,
wsl_prefix: Option<String>,
) -> &mut HyperlinkEnvironment {
self.wsl_prefix = wsl_prefix;
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HyperlinkFormatError {
kind: HyperlinkFormatErrorKind,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum HyperlinkFormatErrorKind {
NoVariables,
NoPathVariable,
NoLineVariable,
InvalidVariable(String),
InvalidScheme,
InvalidCloseVariable,
UnclosedVariable,
}
impl std::error::Error for HyperlinkFormatError {}
impl std::fmt::Display for HyperlinkFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use self::HyperlinkFormatErrorKind::*;
match self.kind {
NoVariables => {
let aliases = hyperlink_aliases::iter()
.map(|(name, _)| name)
.collect::<Vec<&str>>()
.join(", ");
write!(
f,
"at least a {{path}} variable is required in a \
hyperlink format, or otherwise use a valid alias: {}",
aliases,
)
}
NoPathVariable => {
write!(
f,
"the {{path}} variable is required in a hyperlink format",
)
}
NoLineVariable => {
write!(
f,
"the hyperlink format contains a {{column}} variable, \
but no {{line}} variable is present",
)
}
InvalidVariable(ref name) => {
write!(
f,
"invalid hyperlink format variable: '{name}', choose \
from: path, line, column, host, wslprefix",
)
}
InvalidScheme => {
write!(
f,
"the hyperlink format must start with a valid URL scheme, \
i.e., [0-9A-Za-z+-.]+:",
)
}
InvalidCloseVariable => {
write!(
f,
"unopened variable: found '}}' without a \
corresponding '{{' preceding it",
)
}
UnclosedVariable => {
write!(
f,
"unclosed variable: found '{{' without a \
corresponding '}}' following it",
)
}
}
}
}
#[derive(Debug)]
struct FormatBuilder {
parts: Vec<Part>,
}
impl FormatBuilder {
fn new() -> FormatBuilder {
FormatBuilder { parts: vec![] }
}
fn append_slice(&mut self, text: &[u8]) -> &mut FormatBuilder {
if let Some(Part::Text(contents)) = self.parts.last_mut() {
contents.extend_from_slice(text);
} else if !text.is_empty() {
self.parts.push(Part::Text(text.to_vec()));
}
self
}
fn append_char(&mut self, ch: char) -> &mut FormatBuilder {
self.append_slice(ch.encode_utf8(&mut [0; 4]).as_bytes())
}
fn append_var(
&mut self,
name: &str,
) -> Result<&mut FormatBuilder, HyperlinkFormatError> {
let part = match name {
"host" => Part::Host,
"wslprefix" => Part::WSLPrefix,
"path" => Part::Path,
"line" => Part::Line,
"column" => Part::Column,
unknown => {
let err = HyperlinkFormatError {
kind: HyperlinkFormatErrorKind::InvalidVariable(
unknown.to_string(),
),
};
return Err(err);
}
};
self.parts.push(part);
Ok(self)
}
fn build(&self) -> Result<HyperlinkFormat, HyperlinkFormatError> {
self.validate()?;
Ok(HyperlinkFormat {
parts: self.parts.clone(),
is_line_dependent: self.parts.contains(&Part::Line),
})
}
fn validate(&self) -> Result<(), HyperlinkFormatError> {
use self::HyperlinkFormatErrorKind::*;
let err = |kind| HyperlinkFormatError { kind };
if self.parts.is_empty() {
return Ok(());
}
if self.parts.iter().all(|p| matches!(*p, Part::Text(_))) {
return Err(err(NoVariables));
}
if !self.parts.contains(&Part::Path) {
return Err(err(NoPathVariable));
}
if self.parts.contains(&Part::Column)
&& !self.parts.contains(&Part::Line)
{
return Err(err(NoLineVariable));
}
self.validate_scheme()
}
fn validate_scheme(&self) -> Result<(), HyperlinkFormatError> {
let err_invalid_scheme = HyperlinkFormatError {
kind: HyperlinkFormatErrorKind::InvalidScheme,
};
let Some(Part::Text(ref part)) = self.parts.first() else {
return Err(err_invalid_scheme);
};
let Some(colon) = part.find_byte(b':') else {
return Err(err_invalid_scheme);
};
let scheme = &part[..colon];
if scheme.is_empty() {
return Err(err_invalid_scheme);
}
let is_valid_scheme_char = |byte| match byte {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-' | b'.' => {
true
}
_ => false,
};
if !scheme.iter().all(|&b| is_valid_scheme_char(b)) {
return Err(err_invalid_scheme);
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Part {
Text(Vec<u8>),
Host,
WSLPrefix,
Path,
Line,
Column,
}
impl Part {
fn interpolate_to(
&self,
env: &HyperlinkEnvironment,
values: &Values,
dest: &mut Vec<u8>,
) {
match self {
Part::Text(ref text) => dest.extend_from_slice(text),
Part::Host => dest.extend_from_slice(
env.host.as_ref().map(|s| s.as_bytes()).unwrap_or(b""),
),
Part::WSLPrefix => dest.extend_from_slice(
env.wsl_prefix.as_ref().map(|s| s.as_bytes()).unwrap_or(b""),
),
Part::Path => dest.extend_from_slice(&values.path.0),
Part::Line => {
let line = DecimalFormatter::new(values.line.unwrap_or(1));
dest.extend_from_slice(line.as_bytes());
}
Part::Column => {
let column = DecimalFormatter::new(values.column.unwrap_or(1));
dest.extend_from_slice(column.as_bytes());
}
}
}
}
impl std::fmt::Display for Part {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Part::Text(text) => write!(f, "{}", String::from_utf8_lossy(text)),
Part::Host => write!(f, "{{host}}"),
Part::WSLPrefix => write!(f, "{{wslprefix}}"),
Part::Path => write!(f, "{{path}}"),
Part::Line => write!(f, "{{line}}"),
Part::Column => write!(f, "{{column}}"),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Values<'a> {
path: &'a HyperlinkPath,
line: Option<u64>,
column: Option<u64>,
}
impl<'a> Values<'a> {
pub(crate) fn new(path: &'a HyperlinkPath) -> Values<'a> {
Values { path, line: None, column: None }
}
pub(crate) fn line(mut self, line: Option<u64>) -> Values<'a> {
self.line = line;
self
}
pub(crate) fn column(mut self, column: Option<u64>) -> Values<'a> {
self.column = column;
self
}
}
#[derive(Clone, Debug)]
pub(crate) struct Interpolator {
config: HyperlinkConfig,
buf: RefCell<Vec<u8>>,
}
impl Interpolator {
pub(crate) fn new(config: &HyperlinkConfig) -> Interpolator {
Interpolator { config: config.clone(), buf: RefCell::new(vec![]) }
}
pub(crate) fn begin<W: WriteColor>(
&self,
values: &Values,
mut wtr: W,
) -> io::Result<InterpolatorStatus> {
if self.config.format().is_empty()
|| !wtr.supports_hyperlinks()
|| !wtr.supports_color()
{
return Ok(InterpolatorStatus::inactive());
}
let mut buf = self.buf.borrow_mut();
buf.clear();
for part in self.config.format().parts.iter() {
part.interpolate_to(self.config.environment(), values, &mut buf);
}
let spec = HyperlinkSpec::open(&buf);
wtr.set_hyperlink(&spec)?;
Ok(InterpolatorStatus { active: true })
}
pub(crate) fn finish<W: WriteColor>(
&self,
status: InterpolatorStatus,
mut wtr: W,
) -> io::Result<()> {
if !status.active {
return Ok(());
}
wtr.set_hyperlink(&HyperlinkSpec::close())
}
}
#[derive(Debug)]
pub(crate) struct InterpolatorStatus {
active: bool,
}
impl InterpolatorStatus {
#[inline]
pub(crate) fn inactive() -> InterpolatorStatus {
InterpolatorStatus { active: false }
}
}
#[derive(Clone, Debug)]
pub(crate) struct HyperlinkPath(Vec<u8>);
impl HyperlinkPath {
#[cfg(unix)]
pub(crate) fn from_path(original_path: &Path) -> Option<HyperlinkPath> {
use std::os::unix::ffi::OsStrExt;
let path = match original_path.canonicalize() {
Ok(path) => path,
Err(err) => {
log::debug!(
"hyperlink creation for {:?} failed, error occurred \
during path canonicalization: {}",
original_path,
err,
);
return None;
}
};
let bytes = path.as_os_str().as_bytes();
if !bytes.starts_with(b"/") {
log::debug!(
"hyperlink creation for {:?} failed, canonicalization \
returned {:?}, which does not start with a slash",
original_path,
path,
);
return None;
}
Some(HyperlinkPath::encode(bytes))
}
#[cfg(windows)]
pub(crate) fn from_path(original_path: &Path) -> Option<HyperlinkPath> {
const WIN32_NAMESPACE_PREFIX: &str = r"\\?\";
const UNC_PREFIX: &str = r"UNC\";
let path = match original_path.canonicalize() {
Ok(path) => path,
Err(err) => {
log::debug!(
"hyperlink creation for {:?} failed, error occurred \
during path canonicalization: {}",
original_path,
err,
);
return None;
}
};
let mut string = match path.to_str() {
Some(string) => string,
None => {
log::debug!(
"hyperlink creation for {:?} failed, path is not \
valid UTF-8",
original_path,
);
return None;
}
};
if !string.starts_with(WIN32_NAMESPACE_PREFIX) {
log::debug!(
"hyperlink creation for {:?} failed, canonicalization \
returned {:?}, which does not start with \\\\?\\",
original_path,
path,
);
return None;
}
string = &string[WIN32_NAMESPACE_PREFIX.len()..];
if string.starts_with(UNC_PREFIX) {
string = &string[(UNC_PREFIX.len() - 1)..];
}
let with_slash = format!("/{string}");
Some(HyperlinkPath::encode(with_slash.as_bytes()))
}
#[cfg(not(any(windows, unix)))]
pub(crate) fn from_path(original_path: &Path) -> Option<HyperlinkPath> {
log::debug!("hyperlinks are not supported on this platform");
None
}
fn encode(input: &[u8]) -> HyperlinkPath {
let mut result = Vec::with_capacity(input.len());
for &byte in input.iter() {
match byte {
b'0'..=b'9'
| b'A'..=b'Z'
| b'a'..=b'z'
| b'/'
| b':'
| b'-'
| b'.'
| b'_'
| b'~'
| 128.. => {
result.push(byte);
}
#[cfg(windows)]
b'\\' => {
result.push(b'/');
}
_ => {
const HEX: &[u8] = b"0123456789ABCDEF";
result.push(b'%');
result.push(HEX[(byte >> 4) as usize]);
result.push(HEX[(byte & 0xF) as usize]);
}
}
}
HyperlinkPath(result)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn build_format() {
let format = FormatBuilder::new()
.append_slice(b"foo://")
.append_slice(b"bar-")
.append_slice(b"baz")
.append_var("path")
.unwrap()
.build()
.unwrap();
assert_eq!(format.to_string(), "foo://bar-baz{path}");
assert_eq!(format.parts[0], Part::Text(b"foo://bar-baz".to_vec()));
assert!(!format.is_empty());
}
#[test]
fn build_empty_format() {
let format = FormatBuilder::new().build().unwrap();
assert!(format.is_empty());
assert_eq!(format, HyperlinkFormat::empty());
assert_eq!(format, HyperlinkFormat::default());
}
#[test]
fn handle_alias() {
assert!(HyperlinkFormat::from_str("file").is_ok());
assert!(HyperlinkFormat::from_str("none").is_ok());
assert!(HyperlinkFormat::from_str("none").unwrap().is_empty());
}
#[test]
fn parse_format() {
let format = HyperlinkFormat::from_str(
"foo://{host}/bar/{path}:{line}:{column}",
)
.unwrap();
assert_eq!(
format.to_string(),
"foo://{host}/bar/{path}:{line}:{column}"
);
assert_eq!(format.parts.len(), 8);
assert!(format.parts.contains(&Part::Path));
assert!(format.parts.contains(&Part::Line));
assert!(format.parts.contains(&Part::Column));
}
#[test]
fn parse_valid() {
assert!(HyperlinkFormat::from_str("").unwrap().is_empty());
assert_eq!(
HyperlinkFormat::from_str("foo://{path}").unwrap().to_string(),
"foo://{path}"
);
assert_eq!(
HyperlinkFormat::from_str("foo://{path}/bar").unwrap().to_string(),
"foo://{path}/bar"
);
HyperlinkFormat::from_str("f://{path}").unwrap();
HyperlinkFormat::from_str("f:{path}").unwrap();
HyperlinkFormat::from_str("f-+.:{path}").unwrap();
HyperlinkFormat::from_str("f42:{path}").unwrap();
HyperlinkFormat::from_str("42:{path}").unwrap();
HyperlinkFormat::from_str("+:{path}").unwrap();
HyperlinkFormat::from_str("F42:{path}").unwrap();
HyperlinkFormat::from_str("F42://foo{{bar}}{path}").unwrap();
}
#[test]
fn parse_invalid() {
use super::HyperlinkFormatErrorKind::*;
let err = |kind| HyperlinkFormatError { kind };
assert_eq!(
HyperlinkFormat::from_str("foo://bar").unwrap_err(),
err(NoVariables),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{line}").unwrap_err(),
err(NoPathVariable),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{path").unwrap_err(),
err(UnclosedVariable),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{path}:{column}").unwrap_err(),
err(NoLineVariable),
);
assert_eq!(
HyperlinkFormat::from_str("{path}").unwrap_err(),
err(InvalidScheme),
);
assert_eq!(
HyperlinkFormat::from_str(":{path}").unwrap_err(),
err(InvalidScheme),
);
assert_eq!(
HyperlinkFormat::from_str("f*:{path}").unwrap_err(),
err(InvalidScheme),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{bar}").unwrap_err(),
err(InvalidVariable("bar".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{}}bar}").unwrap_err(),
err(InvalidVariable("".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{b}}ar}").unwrap_err(),
err(InvalidVariable("b".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{bar}}}").unwrap_err(),
err(InvalidVariable("bar".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{{bar}").unwrap_err(),
err(InvalidCloseVariable),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{{{bar}").unwrap_err(),
err(InvalidVariable("bar".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{b{{ar}").unwrap_err(),
err(InvalidVariable("b{{ar".to_string())),
);
assert_eq!(
HyperlinkFormat::from_str("foo://{bar{{}").unwrap_err(),
err(InvalidVariable("bar{{".to_string())),
);
}
}