ntfsanitise 0.3.0

Sanitise filenames for use on NTFS filesystems
Documentation
// SPDX-FileCopyrightText: 2025 Lynnesbian <lynne@bune.city>
// SPDX-License-Identifier: MIT OR Apache-2.0

#![allow(clippy::needless_doctest_main)]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "std")]
mod path;
#[cfg(test)]
mod tests;

#[cfg(feature = "std")]
pub use path::{PathBufExt, PathExt};

#[cfg(all(not(feature = "std"), feature = "alloc"))]
extern crate alloc;

#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::string::String;

/// Banned characters for NTFS filenames.
///
/// The banned character set is based on
/// [Microsoft's guidelines](https://learn.microsoft.com/en-au/windows/win32/fileio/naming-a-file#naming-conventions).
/// Characters with ASCII values of zero ([NUL](https://www.ascii-code.com/character/%E2%90%80)) through 31
/// ([UNIT SEPARATOR](https://www.ascii-code.com/character/%E2%90%9F)) are banned, as well as a few reserved
/// characters.
/// See the guidelines for further details.
pub const BANNED: &[char] = &generate_banned();

const fn generate_banned() -> [char; 41] {
	let mut banned = ['\0'; 41];
	let mut i = 0;

	// > Integer value zero, sometimes referred to as the ASCII NUL character
	// > Characters whose integer representations are in the range from 1 through 31
	// note: this includes \n, which you would probably want to ban anyway
	while i <= 31 {
		banned[i as usize] = char::from_u32(i).unwrap();
		i += 1;
	}

	// > The following reserved characters:
	let mut i = 0;
	let reserved = &['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
	while i < reserved.len() {
		banned[i + 32] = reserved[i];
		i += 1;
	}

	banned
}

/// Replaces banned characters with the given replacement.
///
/// # Example
/// ```rust
/// use ntfsanitise::sanitise;
///
/// let input = "Hello\tworld";
/// let sanitised = sanitise(input, "_");
/// assert_eq!(sanitised, String::from("Hello_world"));
/// ```
#[doc(alias = "sanitize")]
#[must_use]
#[cfg(feature = "alloc")]
pub fn sanitise(input: impl AsRef<str>, to: &str) -> String { input.as_ref().replace(BANNED, to) }

/// Checks whether the given string is "dirty" (i.e. contains banned characters).
///
/// # Example
/// ```rust
/// use ntfsanitise::is_dirty;
///
/// assert!(is_dirty("Me? Dirty?"));
/// assert!(!is_dirty("Nice and clean!"));
/// ```
#[must_use]
pub fn is_dirty(input: impl AsRef<str>) -> bool { input.as_ref().contains(BANNED) }

/// Checks whether the given string is "clean" (i.e. does not contain any banned characters).
///
/// # Example
/// ```rust
/// use ntfsanitise::is_clean;
///
/// assert!(is_clean("Nice and clean!"));
/// assert!(!is_clean("Me? Dirty?"));
/// ```
#[must_use]
pub fn is_clean(input: impl AsRef<str>) -> bool { !is_dirty(input) }