diff --git a/README.md b/README.md index 198a9a705b..0fa4666a66 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,9 @@ Options: Basically, the base URL option resolves links as if the local files were hosted at the given base URL address. + The provided base URL value must either be a URL (with scheme) or an absolute path. + Note that certain URL schemes cannot be used as a base, e.g., `data` and `mailto`. + --root-dir Root directory to use when checking absolute links in local files. This option is required if absolute links appear in local files, otherwise those links will be diff --git a/lychee-bin/src/options.rs b/lychee-bin/src/options.rs index 4a4a019a26..50e1732db8 100644 --- a/lychee-bin/src/options.rs +++ b/lychee-bin/src/options.rs @@ -750,7 +750,10 @@ of the base URL. For example, a base URL of `https://example.com/dir/page/` and a link of `a` would resolve to `https://example.com/dir/page/a`. Basically, the base URL option resolves links as if the local files were hosted -at the given base URL address." +at the given base URL address. + +The provided base URL value must either be a URL (with scheme) or an absolute path. +Note that certain URL schemes cannot be used as a base, e.g., `data` and `mailto`." )] #[serde(default)] pub(crate) base_url: Option, diff --git a/lychee-bin/src/parse.rs b/lychee-bin/src/parse.rs index 5ecf433396..bb412fecbb 100644 --- a/lychee-bin/src/parse.rs +++ b/lychee-bin/src/parse.rs @@ -13,8 +13,20 @@ pub(crate) fn parse_remaps(remaps: &[String]) -> Result { .context("Remaps must be of the form ' ' (separated by whitespace)") } -pub(crate) fn parse_base(src: &str) -> Result { - Base::try_from(src) +pub(crate) fn parse_base(src: &str) -> Result { + match Base::try_from(src) { + Ok(x) => Ok(x), + Err(e) => { + // if context is defined, clap displays only the context string in + // argument parse errors. to keep the message from within InvalidBase, + // we need to retain it manually. + let message = format!( + "{e}. See `--help` for more information. If you want to resolve \ + root-relative links in local files, also see `--root-dir`." + ); + Err(e).context(message) + } + } } #[cfg(test)] diff --git a/lychee-lib/src/types/base.rs b/lychee-lib/src/types/base.rs index c2ec3b56d3..5fd770031f 100644 --- a/lychee-lib/src/types/base.rs +++ b/lychee-lib/src/types/base.rs @@ -56,12 +56,24 @@ impl TryFrom<&str> for Base { if url.cannot_be_a_base() { return Err(ErrorKind::InvalidBase( value.to_string(), - "The given URL cannot be a base".to_string(), + "The given URL cannot be used as a base URL".to_string(), )); } return Ok(Self::Remote(url)); } - Ok(Self::Local(PathBuf::from(value))) + + // require absolute paths in `Base::Local`. a local non-relative base is + // basically useless because it cannot be used to join URLs and will + // cause InvalidBaseJoin. + let path = PathBuf::from(value); + if path.is_absolute() { + Ok(Self::Local(path)) + } else { + Err(ErrorKind::InvalidBase( + value.to_string(), + "Base must either be a URL (with scheme) or an absolute local path".to_string(), + )) + } } } @@ -96,7 +108,7 @@ mod test_base { #[test] fn test_valid_local_path_string_as_base() -> Result<()> { - let cases = vec!["/tmp/lychee", "/tmp/lychee/", "tmp/lychee/"]; + let cases = vec!["/tmp/lychee", "/tmp/lychee/"]; for case in cases { assert_eq!(Base::try_from(case)?, Base::Local(PathBuf::from(case))); @@ -104,6 +116,15 @@ mod test_base { Ok(()) } + #[test] + fn test_invalid_local_path_string_as_base() { + let cases = vec!["a", "tmp/lychee/", "example.com", "../nonlocal"]; + + for case in cases { + assert!(Base::try_from(case).is_err()); + } + } + #[test] fn test_valid_local() -> Result<()> { let dir = tempfile::tempdir().unwrap();