Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ tempfile = "3"
[profile.dist]
inherits = "release"
lto = "thin"

[target."cfg(unix)".dependencies]
libc = "0.2"
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ Markdown reference labels are not filesystem targets and are not checked.

Paths are checked literally: no automatic `.md` extension, website routing, or
build-template expansion. Generated targets must already exist when checking;
intentional broken links are reported too. There are no suppressions yet.
intentional broken links are reported too. Invalid filenames and paths that are
too long produce findings without stopping the scan. Other inspection failures,
such as permission errors, remain execution errors. There are no suppressions yet.

## Development

Expand Down
26 changes: 26 additions & 0 deletions src/rules/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub(crate) fn check(path: &Path, source: &str) -> Result<Vec<Diagnostic>, String
{
Some(format!("Target {raw_path:?} does not exist"))
}
Err(error) if invalid_path_error(&error) => {
Some(format!("Target {raw_path:?} is an invalid path: {error}"))
}
Err(error) => {
return Err(format!(
"cannot inspect link target {} from {}: {error}",
Expand Down Expand Up @@ -88,6 +91,22 @@ pub(crate) fn check(path: &Path, source: &str) -> Result<Vec<Diagnostic>, String
Ok(diagnostics)
}

fn invalid_path_error(error: &io::Error) -> bool {
if error.kind() == io::ErrorKind::InvalidInput {
return true;
}
#[cfg(unix)]
if error.raw_os_error() == Some(libc::ENAMETOOLONG) {
return true;
}
// ERROR_INVALID_NAME, ERROR_BAD_PATHNAME, ERROR_FILENAME_EXCED_RANGE.
#[cfg(windows)]
if matches!(error.raw_os_error(), Some(123 | 161 | 206)) {
return true;
}
false
}

fn has_scheme(destination: &str) -> bool {
let Some((scheme, _)) = destination.split_once(':') else {
return false;
Expand All @@ -103,6 +122,13 @@ mod tests {
use super::*;
use tempfile::TempDir;

#[test]
fn inaccessible_targets_are_not_invalid_paths() {
for kind in [io::ErrorKind::PermissionDenied, io::ErrorKind::Other] {
assert!(!invalid_path_error(&io::Error::from(kind)));
}
}

fn project() -> TempDir {
let root = tempfile::tempdir().unwrap();
fs::create_dir(root.path().join("docs")).unwrap();
Expand Down
55 changes: 55 additions & 0 deletions tests/invalid_link_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::{
fs,
path::Path,
process::{Command, Output},
};

fn run(path: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_ryni"))
.arg("check")
.arg(path)
.output()
.unwrap()
}

fn write_file(root: &Path, name: &str, content: &str) {
let path = root.join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, content).unwrap();
}

#[test]
fn invalid_link_paths_do_not_abort_remaining_links_or_files() {
let project = tempfile::tempdir().unwrap();
let root = project.path();
write_file(
root,
"a.md",
&format!("[Too long]({})\n[Missing](missing.txt)\n", "a".repeat(300)),
);
write_file(root, "z.md", "[Later](missing.txt)\n");
let output = run(root);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output.status.code(), Some(1), "{output:?}");
assert!(output.stderr.is_empty());
assert!(stdout.contains("z.md:1:1"), "{stdout}");
assert!(stdout.ends_with("Found 3 errors.\n"), "{stdout}");
}

#[test]
fn nbsp_reference_destination_does_not_abort_scan() {
let project = tempfile::tempdir().unwrap();
let root = project.path();
write_file(
root,
"a.md",
"See [#1].\n\n[#1]:\u{a0}https://example.com/pull/1\n",
);
write_file(root, "z.md", "[Later](missing.txt)\n");
let output = run(root);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output.status.code(), Some(1), "{output:?}");
assert!(output.stderr.is_empty());
assert!(stdout.contains("z.md:1:1"), "{stdout}");
assert!(stdout.ends_with("Found 2 errors.\n"), "{stdout}");
}
4 changes: 3 additions & 1 deletion website/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ Markdown reference labels are not filesystem targets and are not checked.

Paths are checked literally: no automatic `.md` extension, website routing, or
build-template expansion. Generated targets must already exist when checking;
intentional broken links are reported too. There are no suppressions yet.
intentional broken links are reported too. Invalid filenames and paths that are
too long produce findings without stopping the scan. Other inspection failures,
such as permission errors, remain execution errors. There are no suppressions yet.

Loading