virtual-file-transfer 0.1.0

Transfer in-memory, generated, or archived data as files via Windows drag-and-drop and clipboard without staging temporary files
Documentation
use crate::{Error, FileNameErrorReason, InputError, Result};

pub(crate) fn validate_file_name(name: &str) -> Result<()> {
    if name.is_empty() {
        return Err(Error::InvalidInput(InputError::EmptyName));
    }

    if name.contains('\0') {
        return Err(Error::InvalidInput(InputError::InvalidFileName {
            name: name.to_owned(),
            reason: FileNameErrorReason::InvalidCharacter('\0'),
        }));
    }

    for component in name.split('/') {
        if component.is_empty() || matches!(component, "." | "..") {
            return Err(Error::InvalidInput(InputError::InvalidFileName {
                name: name.to_owned(),
                reason: FileNameErrorReason::InvalidComponent(component.to_owned()),
            }));
        }
    }

    #[cfg(windows)]
    crate::windows::validation::validate_file_name(name)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn accepts_valid_names() {
        for name in ["file.txt", "日本語.txt", "😀.txt", ".gitignore"] {
            assert!(validate_file_name(name).is_ok(), "{name:?}");
        }
    }

    #[test]
    fn rejects_empty_name() {
        assert!(matches!(
            validate_file_name(""),
            Err(Error::InvalidInput(InputError::EmptyName))
        ));
    }

    #[test]
    fn rejects_invalid_names() {
        assert!(matches!(
            validate_file_name("a\0b.txt"),
            Err(Error::InvalidInput(InputError::InvalidFileName {
                reason: FileNameErrorReason::InvalidCharacter('\0'),
                ..
            }))
        ));

        for name in [".", ".."] {
            assert!(
                matches!(
                    validate_file_name(name),
                    Err(Error::InvalidInput(InputError::InvalidFileName {
                        reason: FileNameErrorReason::InvalidComponent(_),
                        ..
                    }))
                ),
                "{name:?}"
            );
        }
    }

    #[test]
    fn rejects_invalid_relative_paths() {
        for name in [
            ".",
            "..",
            "a/../b.txt",
            "a/./b.txt",
            "a//b.txt",
            "/a.txt",
            "a/",
        ] {
            assert!(
                matches!(
                    validate_file_name(name),
                    Err(Error::InvalidInput(InputError::InvalidFileName {
                        reason: FileNameErrorReason::InvalidComponent(_),
                        ..
                    }))
                ),
                "{name:?}"
            );
        }
    }

    #[test]
    fn accepts_valid_relative_paths() {
        for name in [
            "file.txt",
            "folder/file.txt",
            "folder/sub/file.txt",
            ".gitignore",
        ] {
            assert!(validate_file_name(name).is_ok(), "{name:?}");
        }
    }
}