use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
pub const SEPARATORS: [char; 2] = ['/', '\\'];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileNameRefusal {
Empty,
Nul,
LeadingSeparator,
Separator,
DotSegment,
DrivePrefix,
}
impl FileNameRefusal {
pub const ALL: [Self; 6] = [
Self::Empty,
Self::Nul,
Self::LeadingSeparator,
Self::Separator,
Self::DotSegment,
Self::DrivePrefix,
];
#[must_use]
pub const fn detail(self) -> &'static str {
match self {
Self::Empty => "it is empty",
Self::Nul => "it contains a NUL byte",
Self::LeadingSeparator => "it begins with a path separator",
Self::Separator => "it contains a path separator",
Self::DotSegment => "it carries a `.` or `..` segment",
Self::DrivePrefix => "it carries a drive prefix",
}
}
}
impl fmt::Display for FileNameRefusal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.detail())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileNameError {
refusal: FileNameRefusal,
value: String,
}
impl FileNameError {
#[must_use]
pub const fn refusal(&self) -> FileNameRefusal {
self.refusal
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for FileNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{:?} is no file name: {}",
self.value, self.refusal
)
}
}
impl core::error::Error for FileNameError {}
fn has_drive_prefix(value: &str) -> bool {
let mut characters = value.chars();
let Some(first) = characters.next() else {
return false;
};
first.is_ascii_alphabetic() && characters.next() == Some(':')
}
fn refusal_for(value: &str) -> Option<FileNameRefusal> {
if value.is_empty() {
return Some(FileNameRefusal::Empty);
}
if value.contains('\0') {
return Some(FileNameRefusal::Nul);
}
if value.starts_with(SEPARATORS) {
return Some(FileNameRefusal::LeadingSeparator);
}
if value.contains(SEPARATORS) {
return Some(FileNameRefusal::Separator);
}
if value
.split(SEPARATORS)
.any(|segment| segment == "." || segment == "..")
{
return Some(FileNameRefusal::DotSegment);
}
if has_drive_prefix(value) {
return Some(FileNameRefusal::DrivePrefix);
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileName(String);
impl FileName {
pub fn new(value: impl Into<String>) -> Result<Self, FileNameError> {
let value = value.into();
match refusal_for(&value) {
Some(refusal) => Err(FileNameError { refusal, value }),
None => Ok(Self(value)),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for FileName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for FileName {
type Err = FileNameError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::new(text)
}
}
impl Serialize for FileName {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for FileName {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Self::new(text).map_err(D::Error::custom)
}
}
impl JsonSchema for FileName {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("FileName")
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "string",
"title": "FileName",
"description": "A file name a printer's own file API can be asked for: no path \
separator, no NUL byte, no `.` or `..` segment, no drive prefix, \
and not empty.",
"minLength": 1,
"pattern": "^[^/\\\\\u{0}]+$",
"not": { "pattern": "^([.]{1,2}$|[A-Za-z]:)" }
})
}
}