ntfsanitise 0.3.0

Sanitise filenames for use on NTFS filesystems
Documentation
use std::path::Component;

use crate::{is_clean, is_dirty, sanitise};

/// Extension trait for [`std::path::Path`].
pub trait PathExt {
	/// Returns `true` if the path does not contain [banned characters](crate::BANNED).
	///
	/// # Example
	/// ```rust
	/// use ntfsanitise::PathExt;
	/// use std::path::PathBuf;
	///
	/// assert!(PathBuf::from("/a/clean/path").is_clean());
	/// ```
	fn is_clean(&self) -> bool;

	/// Returns `true` if the path contains [banned characters](crate::BANNED).
	///
	/// # Example
	/// ```rust
	/// use ntfsanitise::PathExt;
	/// use std::path::PathBuf;
	///
	/// assert!(PathBuf::from("/a/dirty?/p<a>th").is_dirty())
	/// ```
	fn is_dirty(&self) -> bool;

	/// Returns `true` if the path's file name does not contain [banned characters](crate::BANNED), or if the path has
	/// no file name.
	///
	/// # Example
	/// ```rust
	/// use ntfsanitise::PathExt;
	/// use std::path::PathBuf;
	///
	/// assert!(PathBuf::from("/my/clean_file.name").has_clean_file_name());
	/// assert!(PathBuf::from("/no/file/name/").has_clean_file_name());
	/// assert!(PathBuf::from("/dirty<path>/with/clean_file.name").has_clean_file_name());
	/// ```
	fn has_clean_file_name(&self) -> bool;

	/// Returns `true` if the path's file name contains [banned characters](crate::BANNED).
	///
	/// # Example
	/// ```rust
	/// use ntfsanitise::PathExt;
	/// use std::path::PathBuf;
	///
	/// assert!(PathBuf::from("/my/dirty<file>.name").has_dirty_file_name());
	/// ```
	fn has_dirty_file_name(&self) -> bool;
}

impl PathExt for std::path::Path {
	fn is_clean(&self) -> bool { self.components().all(|c| component_is_clean(&c)) }

	fn is_dirty(&self) -> bool { self.components().any(|c| !component_is_clean(&c)) }

	fn has_clean_file_name(&self) -> bool {
		self.file_name().is_none_or(|file_name| is_clean(file_name.to_string_lossy()))
	}

	fn has_dirty_file_name(&self) -> bool {
		self.file_name().is_some_and(|file_name| is_dirty(file_name.to_string_lossy()))
	}
}

/// Extension trait for [`std::path::PathBuf`].
pub trait PathBufExt {
	/// Replaces [banned characters](crate::BANNED) in the file name with the given replacement.
	///
	/// Returns `None` if the path does not have a file name, `Some(true)` if the file name was sanitised, or
	/// `Some(false)` if the file name could not be sanitised due to containing invalid UTF-8.
	///
	/// # Example
	/// ```rust
	/// use ntfsanitise::{PathExt, PathBufExt};
	/// use std::ffi::OsString;
	/// use std::path::PathBuf;
	///
	/// let mut path = PathBuf::from("/example/<name>.txt");
	/// assert!(path.is_dirty());
	///
	/// path.sanitise_filename("_");
	/// let expected = OsString::from("_name_.txt");
	/// let expected = expected.as_os_str();
	///
	/// assert_eq!(path.file_name(), Some(expected));
	/// assert!(path.is_clean());
	/// ```
	#[doc(alias = "sanitize_filename")]
	fn sanitise_filename(&mut self, to: &str) -> Option<bool>;
}

impl PathBufExt for std::path::PathBuf {
	fn sanitise_filename(&mut self, to: &str) -> Option<bool> {
		if let Some(file_name) = self.file_name() {
			if let Some(file_name) = file_name.to_str() {
				self.set_file_name(sanitise(file_name, to));
				Some(true)
			} else {
				Some(false)
			}
		} else {
			None
		}
	}
}

fn component_is_clean(component: &Component) -> bool {
	match component {
		Component::Normal(component) => is_clean(component.to_string_lossy()),
		_ => true,
	}
}