blob: 8185518c85c6bb1a3d8202219974c46891001332 [file] [log] [blame]
use crate::core::{char, cmp, num, str};
#[cfg(feature = "lfn")]
use crate::core::{iter, slice};
use crate::io;
use crate::io::prelude::*;
use crate::io::{ErrorKind, SeekFrom};
#[cfg(all(not(feature = "std"), feature = "alloc", feature = "lfn"))]
use alloc::vec::Vec;
use crate::dir_entry::{
DirEntry, DirEntryData, DirEntryEditor, DirFileEntryData, DirLfnEntryData, FileAttributes,
ShortName, DIR_ENTRY_SIZE,
};
#[cfg(feature = "lfn")]
use crate::dir_entry::{LFN_ENTRY_LAST_FLAG, LFN_PART_LEN};
use crate::dir_entry::{SFN_PADDING, SFN_SIZE};
use crate::error::FatfsError;
use crate::file::File;
use crate::fs::{DiskSlice, FileSystem, FsIoAdapter, OemCpConverter, ReadWriteSeek};
use crate::time::{Date, DateTime, TimeProvider};
use std::cell::{RefCell, RefMut};
pub(crate) enum DirRawStream<'fs, IO: ReadWriteSeek, TP, OCC> {
File(Option<File<'fs, IO, TP, OCC>>),
Root(DiskSlice<FsIoAdapter<'fs, IO, TP, OCC>>),
}
impl<IO: ReadWriteSeek, TP, OCC> DirRawStream<'_, IO, TP, OCC> {
fn abs_pos(&self) -> Result<Option<u64>, FatfsError> {
match self {
DirRawStream::File(file) => {
if let Some(ref file) = file {
Ok(file.abs_pos()?)
} else {
Ok(None)
}
}
DirRawStream::Root(slice) => Ok(Some(slice.abs_pos())),
}
}
fn first_cluster(&self) -> Option<u32> {
match self {
DirRawStream::File(file) => file.as_ref().and_then(|f| f.first_cluster()),
DirRawStream::Root(_) => None,
}
}
fn entry_mut(&mut self) -> Option<&mut DirEntryEditor> {
match self {
DirRawStream::File(file) => file.as_mut().and_then(|f| f.editor_mut()),
DirRawStream::Root(_) => None,
}
}
fn entry(&self) -> Option<&DirEntryEditor> {
match self {
DirRawStream::File(file) => file.as_ref().and_then(|f| f.editor()),
DirRawStream::Root(_) => None,
}
}
fn is_deleted(&self) -> bool {
match self {
DirRawStream::File(file) => file.as_ref().map_or(true, |f| f.is_deleted()),
DirRawStream::Root(_) => false,
}
}
}
impl<IO: ReadWriteSeek, TP: TimeProvider, OCC> Read for DirRawStream<'_, IO, TP, OCC> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
DirRawStream::File(Some(file)) => file.read(buf),
DirRawStream::File(None) => {
Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"))
}
DirRawStream::Root(raw) => raw.read(buf),
}
}
}
impl<IO: ReadWriteSeek, TP: TimeProvider, OCC> Write for DirRawStream<'_, IO, TP, OCC> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
DirRawStream::File(Some(file)) => file.write(buf),
DirRawStream::File(None) => {
Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"))
}
DirRawStream::Root(raw) => raw.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
DirRawStream::File(Some(file)) => file.flush(),
DirRawStream::File(None) => {
Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"))
}
DirRawStream::Root(raw) => raw.flush(),
}
}
}
impl<IO: ReadWriteSeek, TP, OCC> Seek for DirRawStream<'_, IO, TP, OCC> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
DirRawStream::File(Some(file)) => file.seek(pos),
DirRawStream::File(None) => {
Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"))
}
DirRawStream::Root(raw) => raw.seek(pos),
}
}
}
/// Ensures the filename has no trailing spaces or dots.
pub fn validate_filename<'a>(name: &'a str) -> io::Result<()> {
if name == "." || name == ".." {
return Ok(());
}
// Disallow trailing dots or spaces.
// Trailing dots are ignored by Windows and Linux, and trailing spaces are ignored by Windows.
// We error so that opening a file with a given name will always mean that a file with that
// name exists.
// We also disallow empty filenames.
if name.ends_with(&['.', ' '][..]) || name.is_empty() {
return Err(io::Error::new(ErrorKind::InvalidInput, "Illegal filename"));
}
Ok(())
}
fn split_path<'a>(path: &'a str) -> io::Result<(&'a str, Option<&'a str>)> {
// remove trailing slash and split into 2 components - top-most parent and rest
let mut path_split = path.trim_matches('/').splitn(2, '/');
let comp = path_split.next().unwrap(); // SAFE: splitn always returns at least one element
validate_filename(comp)?;
let rest_opt = path_split.next();
Ok((comp, rest_opt))
}
enum DirEntryOrShortName<'fs, IO: ReadWriteSeek, TP, OCC> {
DirEntry(DirEntry<'fs, IO, TP, OCC>),
ShortName([u8; SFN_SIZE]),
}
/// A FAT filesystem directory.
///
/// This struct is created by the `open_dir` or `create_dir` methods on `Dir`.
/// The root directory is returned by the `root_dir` method on `FileSystem`.
pub struct Dir<'fs, IO: ReadWriteSeek, TP, OCC> {
stream: RefCell<DirRawStream<'fs, IO, TP, OCC>>,
fs: &'fs FileSystem<IO, TP, OCC>,
is_root: bool,
}
impl<'fs, IO: ReadWriteSeek, TP, OCC> Dir<'fs, IO, TP, OCC> {
pub(crate) fn new(
stream: DirRawStream<'fs, IO, TP, OCC>,
fs: &'fs FileSystem<IO, TP, OCC>,
is_root: bool,
) -> Self {
Dir { stream: RefCell::new(stream), fs, is_root }
}
/// Creates directory entries iterator.
pub fn iter<'a>(&'a self) -> DirIter<'a, 'fs, IO, TP, OCC> {
DirIter::new(&self.stream, self.fs, true)
}
}
impl<'fs, IO: ReadWriteSeek, TP: TimeProvider, OCC: OemCpConverter> Dir<'fs, IO, TP, OCC> {
fn find_entry(
&self,
name: &str,
is_dir: Option<bool>,
mut short_name_gen: Option<&mut ShortNameGenerator>,
) -> io::Result<DirEntry<'fs, IO, TP, OCC>> {
for r in self.iter() {
let e = r?;
// compare name ignoring case
if e.eq_name(name) {
// check if file or directory is expected
if is_dir.is_some() && Some(e.is_dir()) != is_dir {
let error =
if e.is_dir() { FatfsError::IsDirectory } else { FatfsError::NotDirectory };
return Err(io::Error::new(ErrorKind::Other, error));
}
return Ok(e);
}
// update short name generator state
if let Some(ref mut gen) = short_name_gen {
gen.add_existing(e.raw_short_name());
}
}
Err(io::Error::new(ErrorKind::NotFound, "No such file or directory"))
}
pub(crate) fn find_volume_entry(&self) -> io::Result<Option<DirEntry<'fs, IO, TP, OCC>>> {
for r in DirIter::new(&self.stream, self.fs, false) {
let e = r?;
if e.data.is_volume() {
return Ok(Some(e));
}
}
Ok(None)
}
fn check_for_existence(
&self,
name: &str,
is_dir: Option<bool>,
) -> io::Result<DirEntryOrShortName<'fs, IO, TP, OCC>> {
let mut short_name_gen = ShortNameGenerator::new(name);
loop {
let r = self.find_entry(name, is_dir, Some(&mut short_name_gen));
match r {
Err(ref err) if err.kind() == ErrorKind::NotFound => {}
// other error
Err(err) => return Err(err),
// directory already exists - return it
Ok(e) => return Ok(DirEntryOrShortName::DirEntry(e)),
};
if let Ok(name) = short_name_gen.generate() {
return Ok(DirEntryOrShortName::ShortName(name));
}
short_name_gen.next_iteration();
}
}
/// Set modification datetime for this directory.
pub fn set_created(&mut self, date_time: DateTime) {
match self.stream.borrow_mut().entry_mut() {
Some(e) => e.set_created(date_time),
None => {}
}
}
/// Set access date for this directory.
pub fn set_accessed(&mut self, date: Date) {
match self.stream.borrow_mut().entry_mut() {
Some(e) => e.set_accessed(date),
None => {}
}
}
/// Set modification datetime for this directory.
pub fn set_modified(&mut self, date_time: DateTime) {
match self.stream.borrow_mut().entry_mut() {
Some(e) => e.set_modified(date_time),
None => {}
}
}
/// Get the access time of this directory.
pub fn accessed(&self) -> Date {
match self.stream.borrow().entry() {
Some(ref e) => e.inner().accessed(),
None => Date::decode(0),
}
}
/// Get the creation time of this directory.
pub fn created(&self) -> DateTime {
match self.stream.borrow().entry() {
Some(ref e) => e.inner().created(),
None => DateTime::decode(0, 0, 0),
}
}
/// Get the modification time of this directory.
pub fn modified(&self) -> DateTime {
match self.stream.borrow().entry() {
Some(ref e) => e.inner().modified(),
None => DateTime::decode(0, 0, 0),
}
}
/// Opens existing subdirectory.
///
/// `path` is a '/' separated directory path relative to self directory.
pub fn open_dir(&self, path: &str) -> io::Result<Self> {
trace!("open_dir {}", path);
let (name, rest_opt) = split_path(path)?;
let e = self.find_entry(name, Some(true), None)?;
match rest_opt {
Some(rest) => e.to_dir().open_dir(rest),
None => Ok(e.to_dir()),
}
}
/// Opens existing file.
///
/// `path` is a '/' separated file path relative to self directory.
pub fn open_file(&self, path: &str) -> io::Result<File<'fs, IO, TP, OCC>> {
trace!("open_file {}", path);
// traverse path
let (name, rest_opt) = split_path(path)?;
if let Some(rest) = rest_opt {
let e = self.find_entry(name, Some(true), None)?;
return e.to_dir().open_file(rest);
}
// convert entry to a file
let e = self.find_entry(name, Some(false), None)?;
Ok(e.to_file())
}
/// Creates new or opens existing file=.
///
/// `path` is a '/' separated file path relative to self directory.
/// File is never truncated when opening. It can be achieved by calling `File::truncate` method after opening.
pub fn create_file(&self, path: &str) -> io::Result<File<'fs, IO, TP, OCC>> {
trace!("create_file {}", path);
// traverse path
let (name, rest_opt) = split_path(path)?;
if let Some(rest) = rest_opt {
return self.find_entry(name, Some(true), None)?.to_dir().create_file(rest);
}
if self.stream.borrow().is_deleted() {
return Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"));
}
// this is final filename in the path
let r = self.check_for_existence(name, Some(false))?;
match r {
// file does not exist - create it
DirEntryOrShortName::ShortName(short_name) => {
let sfn_entry =
self.create_sfn_entry(short_name, FileAttributes::from_bits_truncate(0), None);
Ok(self.write_entry(name, sfn_entry)?.to_file())
}
// file already exists - return it
DirEntryOrShortName::DirEntry(e) => Ok(e.to_file()),
}
}
/// Creates new directory or opens existing.
///
/// `path` is a '/' separated path relative to self directory.
pub fn create_dir(&self, path: &str) -> io::Result<Self> {
trace!("create_dir {}", path);
// traverse path
let (name, rest_opt) = split_path(path)?;
if let Some(rest) = rest_opt {
return self.find_entry(name, Some(true), None)?.to_dir().create_dir(rest);
}
if self.stream.borrow().is_deleted() {
return Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"));
}
// this is final filename in the path
let r = self.check_for_existence(name, Some(true))?;
match r {
// directory does not exist - create it
DirEntryOrShortName::ShortName(short_name) => {
let transaction = self.fs.begin_transaction().unwrap();
// alloc cluster for directory data
let cluster = self.fs.alloc_cluster(None, true)?;
// create entry in parent directory
let sfn_entry =
self.create_sfn_entry(short_name, FileAttributes::DIRECTORY, Some(cluster));
let entry = self.write_entry(name, sfn_entry)?;
let dir = entry.to_dir();
// create special entries "." and ".."
let dot_sfn = ShortNameGenerator::generate_dot();
let sfn_entry = self.create_sfn_entry(
dot_sfn,
FileAttributes::DIRECTORY,
entry.first_cluster(),
);
dir.write_entry(".", sfn_entry)?;
let dotdot_sfn = ShortNameGenerator::generate_dotdot();
let sfn_entry = self.create_sfn_entry(
dotdot_sfn,
FileAttributes::DIRECTORY,
self.cluster_for_child_dotdot_entry(),
);
dir.write_entry("..", sfn_entry)?;
self.fs.commit(transaction)?;
Ok(dir)
}
// directory already exists - return it
DirEntryOrShortName::DirEntry(e) => Ok(e.to_dir()),
}
}
pub fn is_empty(&self) -> io::Result<bool> {
trace!("is_empty");
// check if directory contains no files
for r in self.iter() {
let e = r?;
let name = e.short_file_name_as_bytes();
// ignore special entries "." and ".."
if name != b"." && name != b".." {
return Ok(false);
}
}
Ok(true)
}
/// Removes existing file or directory.
///
/// `path` is a '/' separated file path relative to self directory.
/// Make sure there is no reference to this file (no File instance) or filesystem corruption
/// can happen.
pub fn remove(&self, path: &str) -> io::Result<()> {
trace!("remove {}", path);
// traverse path
let (name, rest_opt) = split_path(path)?;
if let Some(rest) = rest_opt {
let e = self.find_entry(name, Some(true), None)?;
return e.to_dir().remove(rest);
}
// in case of directory check if it is empty
let e = self.find_entry(name, None, None)?;
if e.is_dir() && !e.to_dir().is_empty()? {
return Err(io::Error::new(ErrorKind::Other, FatfsError::DirectoryNotEmpty));
}
// free data
if let Some(n) = e.first_cluster() {
self.fs.free_cluster_chain(n)?;
}
// free long and short name entries
let mut stream = self.stream.borrow_mut();
stream.seek(SeekFrom::Start(e.offset_range.0 as u64))?;
let num = (e.offset_range.1 - e.offset_range.0) as usize / DIR_ENTRY_SIZE as usize;
for _ in 0..num {
let mut data = DirEntryData::deserialize(&mut *stream)?;
trace!("removing dir entry {:?}", data);
data.set_deleted();
stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?;
data.serialize(&mut *stream)?;
}
Ok(())
}
fn unlink_file_on_disk(&self, offset_range: (u64, u64)) -> io::Result<()> {
let mut stream = self.stream.borrow_mut();
stream.seek(io::SeekFrom::Start(offset_range.0))?;
let num = (offset_range.1 - offset_range.0) as usize / DIR_ENTRY_SIZE as usize;
for _ in 0..num {
let mut data = DirEntryData::deserialize(&mut *stream)?;
trace!("removing dir entry {:?}", data);
data.set_deleted();
stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?;
data.serialize(&mut *stream)?;
}
Ok(())
}
/// Unlink the given File from this directory.
///
/// Will cause filesystem corruption if the file is not actually in this directory,
/// but continuing to use the file is legal. It will be completely removed from the filesystem
/// once it is dropped.
pub fn unlink_file(&self, file: &mut File<IO, TP, OCC>) -> io::Result<()> {
let entry = file
.editor()
.ok_or(io::Error::new(ErrorKind::InvalidInput, "Can't delete file with no dirent"))?;
self.unlink_file_on_disk(entry.offset_range)?;
file.mark_deleted();
Ok(())
}
/// Unlink the given Dir from this directory. Returns an error if the Dir is not empty.
///
/// Will cause filesystem corruption if the file is not actually in this directory.
/// The directory's clusters will be removed from the disk.
pub fn unlink_dir(&self, dir: &mut Dir<IO, TP, OCC>) -> io::Result<()> {
if !dir.is_empty()? {
return Err(io::Error::new(ErrorKind::Other, FatfsError::DirectoryNotEmpty));
}
match &mut *dir.stream.borrow_mut() {
DirRawStream::File(ref mut option) => {
if let Some(mut file) = option.take() {
self.unlink_file(&mut file)?;
file.purge()
} else {
// Directory has already been deleted.
return Err(io::Error::new(ErrorKind::NotFound, "Not found"));
}
}
DirRawStream::Root(_) => Err(io::Error::new(
ErrorKind::InvalidInput,
"Root directory is not child of any directory",
)),
}
}
/// Renames or moves existing file or directory.
///
/// `src_path` is a '/' separated source file path relative to self directory.
/// `dst_path` is a '/' separated destination file path relative to `dst_dir`.
/// `dst_dir` can be set to self directory if rename operation without moving is needed.
/// Make sure there is no reference to this file (no File instance) or filesystem corruption
/// can happen.
pub fn rename(
&self,
src_path: &str,
dst_dir: &Dir<IO, TP, OCC>,
dst_path: &str,
) -> io::Result<()> {
trace!("rename {} {}", src_path, dst_path);
// traverse source path
let (name, rest_opt) = split_path(src_path)?;
if let Some(rest) = rest_opt {
let e = self.find_entry(name, Some(true), None)?;
return e.to_dir().rename(rest, dst_dir, dst_path);
}
// traverse destination path
let (name, rest_opt) = split_path(dst_path)?;
if let Some(rest) = rest_opt {
let e = dst_dir.find_entry(name, Some(true), None)?;
return self.rename(src_path, &e.to_dir(), rest);
}
// move/rename file
self.rename_internal(src_path, dst_dir, dst_path)
}
/// Same as rename, but supports renaming over an existing file.
pub fn rename_over_file(
&self,
src_path: &str,
dst_dir: &Dir<IO, TP, OCC>,
dst_path: &str,
file: &mut File<IO, TP, OCC>,
) -> io::Result<()> {
let transaction = self.fs.begin_transaction().unwrap();
let entry = file
.editor()
.ok_or(io::Error::new(ErrorKind::InvalidInput, "Can't delete file with no dirent"))?;
dst_dir.unlink_file_on_disk(entry.offset_range)?;
self.rename(src_path, dst_dir, dst_path)?;
self.fs.commit(transaction)?;
file.mark_deleted();
Ok(())
}
/// Same as rename but supports renaming over an existing directory (which must be empty).
pub fn rename_over_dir(
&self,
src_path: &str,
dst_dir: &Dir<IO, TP, OCC>,
dst_path: &str,
dir: &mut Dir<IO, TP, OCC>,
) -> io::Result<()> {
let transaction = self.fs.begin_transaction().unwrap();
if !dir.is_empty()? {
return Err(io::Error::new(ErrorKind::Other, FatfsError::DirectoryNotEmpty));
}
let file;
let mut stream = dir.stream.borrow_mut();
match &mut *stream {
DirRawStream::File(ref mut option) => {
file = option.as_mut().ok_or(io::Error::new(ErrorKind::NotFound, "Not found"))?;
let entry = file.editor().ok_or(io::Error::new(
ErrorKind::InvalidInput,
"Can't delete file with no dirent",
))?;
dst_dir.unlink_file_on_disk(entry.offset_range)?;
if let Some(cluster) = file.first_cluster() {
self.fs.free_cluster_chain(cluster)?;
}
}
DirRawStream::Root(_) => {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Root directory is not child of any directory",
))
}
};
self.rename(src_path, dst_dir, dst_path)?;
self.fs.commit(transaction)?;
file.mark_deleted_and_purged();
Ok(())
}
fn rename_internal(
&self,
src_name: &str,
dst_dir: &Dir<IO, TP, OCC>,
dst_name: &str,
) -> io::Result<()> {
trace!("rename_internal {} {}", src_name, dst_name);
if dst_dir.stream.borrow().is_deleted() {
return Err(io::Error::new(
ErrorKind::NotFound,
"Destination directory has been deleted",
));
}
// find existing file
let e = self.find_entry(src_name, None, None)?;
// check if destionation filename is unused
let r = dst_dir.check_for_existence(dst_name, None)?;
let short_name = match r {
DirEntryOrShortName::DirEntry(ref dst_e) => {
// check if source and destination entry is the same
if !e.is_same_entry(dst_e) {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
"Destination file already exists",
));
}
// if names are exactly the same, we don't need to do anything.
if src_name == dst_name {
return Ok(());
}
// otherwise, we'll rewrite the LFN below.
dst_e.raw_short_name().clone()
}
DirEntryOrShortName::ShortName(short_name) => short_name,
};
// free long and short name entries
{
let mut stream = self.stream.borrow_mut();
stream.seek(SeekFrom::Start(e.offset_range.0 as u64))?;
let num = (e.offset_range.1 - e.offset_range.0) as usize / DIR_ENTRY_SIZE as usize;
for _ in 0..num {
let mut data = DirEntryData::deserialize(&mut *stream)?;
trace!("removing LFN entry {:?}", data);
data.set_deleted();
stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?;
data.serialize(&mut *stream)?;
}
}
// save new directory entry
let sfn_entry = e.data.renamed(short_name);
let dir_entry = dst_dir.write_entry(dst_name, sfn_entry)?;
// if renaming a directory, then we need to update the '..' entry
if dir_entry.is_dir()
&& self.cluster_for_child_dotdot_entry() != dst_dir.cluster_for_child_dotdot_entry()
{
let dir = dir_entry.to_dir();
let mut iter = dir.iter();
let error = || io::Error::new(ErrorKind::Other, "Missing expected .. entry");
// The dotdot entry should be the second entry.
let _ = iter.next().ok_or_else(error)??;
let entry = iter.next().ok_or_else(error)??;
if entry.short_file_name_as_bytes() != b".." {
return Err(error());
}
let mut editor = entry.editor();
editor.set_first_cluster(dst_dir.cluster_for_child_dotdot_entry(), self.fs.fat_type());
editor.flush(self.fs)?;
}
Ok(())
}
fn find_free_entries(
&self,
num_entries: usize,
) -> io::Result<RefMut<'_, DirRawStream<'fs, IO, TP, OCC>>> {
let mut stream = self.stream.borrow_mut();
stream.seek(io::SeekFrom::Start(0))?;
let mut first_free = 0;
let mut num_free = 0;
let mut i = 0;
loop {
let raw_entry = DirEntryData::deserialize(&mut *stream)?;
if raw_entry.is_end() {
// first unused entry - all remaining space can be used
if num_free == 0 {
first_free = i;
}
stream.seek(io::SeekFrom::Start(first_free as u64 * DIR_ENTRY_SIZE))?;
return Ok(stream);
} else if raw_entry.is_deleted() {
// free entry - calculate number of free entries in a row
if num_free == 0 {
first_free = i;
}
num_free += 1;
if num_free == num_entries {
// enough space for new file
stream.seek(io::SeekFrom::Start(first_free as u64 * DIR_ENTRY_SIZE))?;
return Ok(stream);
}
} else {
// used entry - start counting from 0
num_free = 0;
}
i += 1;
}
}
fn create_sfn_entry(
&self,
short_name: [u8; SFN_SIZE],
attrs: FileAttributes,
first_cluster: Option<u32>,
) -> DirFileEntryData {
let mut raw_entry = DirFileEntryData::new(short_name, attrs);
raw_entry.set_first_cluster(first_cluster, self.fs.fat_type());
let now = self.fs.options.time_provider.get_current_date_time();
raw_entry.set_created(now);
raw_entry.set_accessed(now.date);
raw_entry.set_modified(now);
raw_entry
}
#[cfg(feature = "lfn")]
fn encode_lfn_utf16(name: &str) -> LfnBuffer {
LfnBuffer::from_ucs2_units(name.encode_utf16())
}
#[cfg(not(feature = "lfn"))]
fn encode_lfn_utf16(_name: &str) -> LfnBuffer {
LfnBuffer {}
}
fn alloc_and_write_lfn_entries(
&self,
lfn_utf16: &LfnBuffer,
short_name: &[u8; SFN_SIZE],
) -> io::Result<(RefMut<'_, DirRawStream<'fs, IO, TP, OCC>>, u64)> {
// get short name checksum
let lfn_chsum = lfn_checksum(short_name);
// create LFN entries generator
let lfn_iter = LfnEntriesGenerator::new(lfn_utf16.as_ucs2_units(), lfn_chsum);
// find space for new entries (multiple LFN entries and 1 SFN entry)
let num_entries = lfn_iter.len() + 1;
let mut stream = self.find_free_entries(num_entries)?;
let start_pos = stream.seek(io::SeekFrom::Current(0))?;
// write LFN entries before SFN entry
for lfn_entry in lfn_iter {
lfn_entry.serialize(&mut *stream)?;
}
Ok((stream, start_pos))
}
fn write_entry(
&self,
name: &str,
raw_entry: DirFileEntryData,
) -> io::Result<DirEntry<'fs, IO, TP, OCC>> {
trace!("write_entry {}", name);
// check if name doesn't contain unsupported characters
let long_name_required = validate_long_name(name, raw_entry.name())?;
// start a transaction so that if things go wrong (e.g. we run out of space), we can easily
// revert and leave the file system in a good state.
let transaction = self.fs.begin_transaction();
let (mut stream, start_pos, lfn_utf16) = if long_name_required {
// convert long name to UTF-16
let lfn_utf16 = Self::encode_lfn_utf16(name);
// write LFN entries
let (stream, start_pos) =
self.alloc_and_write_lfn_entries(&lfn_utf16, raw_entry.name())?;
(stream, start_pos, lfn_utf16)
} else {
let mut stream = self.find_free_entries(1)?;
let start_pos = stream.seek(io::SeekFrom::Current(0))?;
(stream, start_pos, LfnBuffer::new())
};
// write short name entry
raw_entry.serialize(&mut *stream)?;
if let Some(transaction) = transaction {
self.fs.commit(transaction)?;
}
let end_pos = stream.seek(io::SeekFrom::Current(0))?;
let abs_pos = stream.abs_pos()?.map(|p| p - DIR_ENTRY_SIZE);
// return new logical entry descriptor
let short_name = ShortName::new(raw_entry.name());
Ok(DirEntry {
data: raw_entry,
short_name,
#[cfg(feature = "lfn")]
lfn_utf16,
fs: self.fs,
entry_pos: abs_pos.unwrap(), // SAFE: abs_pos is absent only for empty file
offset_range: (start_pos, end_pos),
})
}
fn cluster_for_child_dotdot_entry(&self) -> Option<u32> {
if self.is_root {
None
} else {
self.stream.borrow().first_cluster()
}
}
/// Flushes the directory entry in the parent if not the root.
pub fn flush_dir_entry(&mut self) -> std::io::Result<()> {
match &mut *self.stream.borrow_mut() {
DirRawStream::File(Some(file)) => file.flush_dir_entry(),
DirRawStream::File(None) => {
Err(io::Error::new(ErrorKind::NotFound, "Directory has been deleted"))
}
DirRawStream::Root(_) => {
Err(io::Error::new(ErrorKind::Other, "Cannot flush root directroy entry"))
}
}
}
}
/// An iterator over the directory entries.
///
/// This struct is created by the `iter` method on `Dir`.
pub struct DirIter<'a, 'fs, IO: ReadWriteSeek, TP, OCC> {
stream: &'a RefCell<DirRawStream<'fs, IO, TP, OCC>>,
fs: &'fs FileSystem<IO, TP, OCC>,
skip_volume: bool,
err: bool,
}
impl<'a, 'fs, IO: ReadWriteSeek, TP, OCC> DirIter<'a, 'fs, IO, TP, OCC> {
fn new(
stream: &'a RefCell<DirRawStream<'fs, IO, TP, OCC>>,
fs: &'fs FileSystem<IO, TP, OCC>,
skip_volume: bool,
) -> Self {
stream.borrow_mut().seek(SeekFrom::Start(0)).unwrap();
DirIter { stream, fs, skip_volume, err: false }
}
}
impl<'fs, IO: ReadWriteSeek, TP: TimeProvider, OCC> DirIter<'_, 'fs, IO, TP, OCC> {
fn should_skip_entry(&self, raw_entry: &DirEntryData) -> bool {
if raw_entry.is_deleted() {
return true;
}
match raw_entry {
DirEntryData::File(sfn_entry) => self.skip_volume && sfn_entry.is_volume(),
_ => false,
}
}
fn read_dir_entry(&mut self) -> io::Result<Option<DirEntry<'fs, IO, TP, OCC>>> {
trace!("read_dir_entry");
let mut lfn_builder = LongNameBuilder::new();
let mut stream = self.stream.borrow_mut();
let mut offset = stream.seek(SeekFrom::Current(0))?;
let mut begin_offset = offset;
loop {
let raw_entry = DirEntryData::deserialize(&mut *stream)?;
offset += DIR_ENTRY_SIZE;
// Check if this is end of dir
if raw_entry.is_end() {
return Ok(None);
}
// Check if this is deleted or volume ID entry
if self.should_skip_entry(&raw_entry) {
trace!("skip entry");
lfn_builder.clear();
begin_offset = offset;
continue;
}
match raw_entry {
DirEntryData::File(data) => {
// Get entry position on volume
let abs_pos = stream.abs_pos()?.map(|p| p - DIR_ENTRY_SIZE);
// Check if LFN checksum is valid
lfn_builder.validate_chksum(data.name());
// Return directory entry
let short_name = ShortName::new(data.name());
trace!("file entry {:?}", data.name());
return Ok(Some(DirEntry {
data,
short_name,
#[cfg(feature = "lfn")]
lfn_utf16: lfn_builder.into_buf(),
fs: self.fs,
entry_pos: abs_pos.unwrap(), // SAFE: abs_pos is empty only for empty file
offset_range: (begin_offset, offset),
}));
}
DirEntryData::Lfn(data) => {
// Append to LFN buffer
trace!("lfn entry");
lfn_builder.process(&data);
}
}
}
}
}
impl<'b, IO: ReadWriteSeek, TP: TimeProvider, OCC> Iterator for DirIter<'_, 'b, IO, TP, OCC> {
type Item = io::Result<DirEntry<'b, IO, TP, OCC>>;
fn next(&mut self) -> Option<Self::Item> {
if self.err {
return None;
}
let r = self.read_dir_entry();
match r {
Ok(Some(e)) => Some(Ok(e)),
Ok(None) => None,
Err(err) => {
self.err = true;
Some(Err(err))
}
}
}
}
fn validate_long_name(name: &str, short_name: &[u8; SFN_SIZE]) -> io::Result<bool> {
// check if length is valid
if name.is_empty() {
return Err(io::Error::new(ErrorKind::Other, FatfsError::FileNameEmpty));
}
if name.len() > 255 {
return Err(io::Error::new(ErrorKind::Other, FatfsError::FileNameTooLong));
}
let mut long_name_required = name.len() > 12;
let mut short_name_index: usize = 0;
// check if there are only valid characters
for c in name.chars() {
match c {
'A'..='Z' | '0'..='9' => {}
'$' | '%' | '\'' | '-' | '_' | '@' | '~' | '`' | '!' | '(' | ')' | '{' | '}' | '^'
| '#' | '&' | '.' => {}
'a'..='z' | '\u{80}'..='\u{10FFFF}' | ' ' | '+' | ',' | ';' | '=' | '[' | ']' => {
long_name_required = true
}
_ => return Err(io::Error::new(ErrorKind::Other, FatfsError::FileNameBadCharacter)),
}
if !long_name_required {
long_name_required = match short_name_index {
0..=7 => {
if c as u8 == short_name[short_name_index] {
short_name_index += 1;
false
} else if c == '.' {
short_name_index = 9;
false
} else {
true
}
}
8 => {
short_name_index = 9;
c != '.'
}
9..=11 => {
short_name_index += 1;
c as u8 != short_name[short_name_index - 2]
}
_ => true,
};
}
}
Ok(long_name_required)
}
fn lfn_checksum(short_name: &[u8; SFN_SIZE]) -> u8 {
let mut chksum = num::Wrapping(0u8);
for b in short_name {
chksum = (chksum << 7) + (chksum >> 1) + num::Wrapping(*b);
}
chksum.0
}
#[cfg(all(feature = "lfn", feature = "alloc"))]
#[derive(Clone)]
pub(crate) struct LfnBuffer {
ucs2_units: Vec<u16>,
}
#[cfg(not(feature = "alloc"))]
const MAX_LFN_LEN: usize = 256;
#[cfg(all(feature = "lfn", not(feature = "alloc")))]
#[derive(Clone)]
pub(crate) struct LfnBuffer {
ucs2_units: [u16; MAX_LFN_LEN],
len: usize,
}
#[cfg(all(feature = "lfn", feature = "alloc"))]
impl LfnBuffer {
fn new() -> Self {
LfnBuffer { ucs2_units: Vec::<u16>::new() }
}
fn from_ucs2_units<I>(usc2_units: I) -> Self
where
I: Iterator<Item = u16>,
{
LfnBuffer { ucs2_units: usc2_units.collect() }
}
fn clear(&mut self) {
self.ucs2_units.clear();
}
pub(crate) fn len(&self) -> usize {
self.ucs2_units.len()
}
fn set_len(&mut self, len: usize) {
self.ucs2_units.resize(len, 0u16);
}
pub(crate) fn as_ucs2_units(&self) -> &[u16] {
&self.ucs2_units
}
}
#[cfg(all(feature = "lfn", not(feature = "alloc")))]
impl LfnBuffer {
fn new() -> Self {
LfnBuffer { ucs2_units: [0u16; MAX_LFN_LEN], len: 0 }
}
fn from_ucs2_units<I>(usc2_units: I) -> Self
where
I: Iterator<Item = u16>,
{
let mut lfn = LfnBuffer { ucs2_units: [0u16; MAX_LFN_LEN], len: 0 };
for (i, usc2_unit) in usc2_units.enumerate() {
lfn.ucs2_units[i] = usc2_unit;
}
lfn
}
fn clear(&mut self) {
self.ucs2_units = [0u16; MAX_LFN_LEN];
self.len = 0;
}
pub(crate) fn len(&self) -> usize {
self.len
}
fn set_len(&mut self, len: usize) {
self.len = len;
}
pub(crate) fn as_ucs2_units(&self) -> &[u16] {
&self.ucs2_units
}
}
#[cfg(not(feature = "lfn"))]
#[derive(Clone)]
pub(crate) struct LfnBuffer {}
#[cfg(not(feature = "lfn"))]
impl LfnBuffer {
fn new() -> Self {
LfnBuffer {}
}
pub(crate) fn as_ucs2_units(&self) -> &[u16] {
&[]
}
}
#[cfg(feature = "lfn")]
struct LongNameBuilder {
buf: LfnBuffer,
chksum: u8,
index: u8,
}
#[cfg(feature = "lfn")]
impl LongNameBuilder {
fn new() -> Self {
LongNameBuilder { buf: LfnBuffer::new(), chksum: 0, index: 0 }
}
fn clear(&mut self) {
self.buf.clear();
self.index = 0;
}
fn into_buf(mut self) -> LfnBuffer {
// Check if last processed entry had index 1
if self.index == 1 {
self.truncate();
} else if !self.is_empty() {
warn!("unfinished LFN sequence {}", self.index);
self.clear();
}
self.buf
}
fn truncate(&mut self) {
// Truncate 0 and 0xFFFF characters from LFN buffer
let mut lfn_len = self.buf.len();
while lfn_len > 0 {
match self.buf.ucs2_units[lfn_len - 1] {
0xFFFF | 0 => lfn_len -= 1,
_ => break,
}
}
self.buf.set_len(lfn_len);
}
fn is_empty(&self) -> bool {
// Check if any LFN entry has been processed
// Note: index 0 is not a valid index in LFN and can be seen only after struct initialization
self.index == 0
}
fn process(&mut self, data: &DirLfnEntryData) {
let is_last = (data.order() & LFN_ENTRY_LAST_FLAG) != 0;
let index = data.order() & 0x1F;
if index == 0 {
// Corrupted entry
warn!("corrupted lfn entry! {:x}", data.order());
self.clear();
return;
}
if is_last {
// last entry is actually first entry in stream
self.index = index;
self.chksum = data.checksum();
self.buf.set_len(index as usize * LFN_PART_LEN);
} else if self.index == 0 || index != self.index - 1 || data.checksum() != self.chksum {
// Corrupted entry
warn!(
"corrupted lfn entry! {:x} {:x} {:x} {:x}",
data.order(),
self.index,
data.checksum(),
self.chksum
);
self.clear();
return;
} else {
// Decrement LFN index only for non-last entries
self.index -= 1;
}
let pos = LFN_PART_LEN * (index - 1) as usize;
// copy name parts into LFN buffer
data.copy_name_to_slice(&mut self.buf.ucs2_units[pos..pos + 13]);
}
fn validate_chksum(&mut self, short_name: &[u8; SFN_SIZE]) {
if self.is_empty() {
// Nothing to validate - no LFN entries has been processed
return;
}
let chksum = lfn_checksum(short_name);
if chksum != self.chksum {
warn!("checksum mismatch {:x} {:x} {:?}", chksum, self.chksum, short_name);
self.clear();
}
}
}
// Dummy implementation for non-alloc build
#[cfg(not(feature = "lfn"))]
struct LongNameBuilder {}
#[cfg(not(feature = "lfn"))]
impl LongNameBuilder {
fn new() -> Self {
LongNameBuilder {}
}
fn clear(&mut self) {}
fn into_vec(self) {}
fn truncate(&mut self) {}
fn process(&mut self, _data: &DirLfnEntryData) {}
fn validate_chksum(&mut self, _short_name: &[u8; SFN_SIZE]) {}
}
#[cfg(feature = "lfn")]
struct LfnEntriesGenerator<'a> {
name_parts_iter: iter::Rev<slice::Chunks<'a, u16>>,
checksum: u8,
index: usize,
num: usize,
ended: bool,
}
#[cfg(feature = "lfn")]
impl<'a> LfnEntriesGenerator<'a> {
fn new(name_utf16: &'a [u16], checksum: u8) -> Self {
let num_entries = (name_utf16.len() + LFN_PART_LEN - 1) / LFN_PART_LEN;
// create generator using reverse iterator over chunks - first chunk can be shorter
LfnEntriesGenerator {
checksum,
name_parts_iter: name_utf16.chunks(LFN_PART_LEN).rev(),
index: 0,
num: num_entries,
ended: false,
}
}
}
#[cfg(feature = "lfn")]
impl Iterator for LfnEntriesGenerator<'_> {
type Item = DirLfnEntryData;
fn next(&mut self) -> Option<Self::Item> {
if self.ended {
return None;
}
// get next part from reverse iterator
match self.name_parts_iter.next() {
Some(ref name_part) => {
let lfn_index = self.num - self.index;
let mut order = lfn_index as u8;
if self.index == 0 {
// this is last name part (written as first)
order |= LFN_ENTRY_LAST_FLAG;
}
debug_assert!(order > 0);
// name is padded with ' '
const LFN_PADDING: u16 = 0xFFFF;
let mut lfn_part = [LFN_PADDING; LFN_PART_LEN];
lfn_part[..name_part.len()].copy_from_slice(&name_part);
if name_part.len() < LFN_PART_LEN {
// name is only zero-terminated if its length is not multiplicity of LFN_PART_LEN
lfn_part[name_part.len()] = 0;
}
// create and return new LFN entry
let mut lfn_entry = DirLfnEntryData::new(order, self.checksum);
lfn_entry.copy_name_from_slice(&lfn_part);
self.index += 1;
Some(lfn_entry)
}
None => {
// end of name
self.ended = true;
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.name_parts_iter.size_hint()
}
}
// name_parts_iter is ExactSizeIterator so size_hint returns one limit
#[cfg(feature = "lfn")]
impl ExactSizeIterator for LfnEntriesGenerator<'_> {}
// Dummy implementation for non-alloc build
#[cfg(not(feature = "lfn"))]
struct LfnEntriesGenerator {}
#[cfg(not(feature = "lfn"))]
impl LfnEntriesGenerator {
fn new(_name_utf16: &[u16], _checksum: u8) -> Self {
LfnEntriesGenerator {}
}
}
#[cfg(not(feature = "lfn"))]
impl Iterator for LfnEntriesGenerator {
type Item = DirLfnEntryData;
fn next(&mut self) -> Option<Self::Item> {
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(0))
}
}
#[cfg(not(feature = "lfn"))]
impl ExactSizeIterator for LfnEntriesGenerator {}
#[derive(Default, Debug, Clone)]
struct ShortNameGenerator {
chksum: u16,
long_prefix_bitmap: u16,
prefix_chksum_bitmap: u16,
name_fits: bool,
lossy_conv: bool,
exact_match: bool,
basename_len: u8,
short_name: [u8; SFN_SIZE],
}
impl ShortNameGenerator {
fn new(name: &str) -> Self {
// padded by ' '
let mut short_name = [SFN_PADDING; SFN_SIZE];
// find extension after last dot
// Note: short file name cannot start with the extension
let (basename_len, name_fits, lossy_conv) = match name.rfind('.') {
Some(0) | None => {
// file starts with a '.', or it has no extension - copy name and leave extension
// empty.
let (basename_len, basename_fits, basename_lossy) =
Self::copy_short_name_part(&mut short_name[0..8], &name);
(basename_len, basename_fits, basename_lossy)
}
Some(dot_index) => {
// extension found - copy parts before and after dot
let (basename_len, basename_fits, basename_lossy) =
Self::copy_short_name_part(&mut short_name[0..8], &name[..dot_index]);
// This is safe, because '.' is always exactly one byte - we don't risk
// accidentally indexing mid-character.
let (_, ext_fits, ext_lossy) =
Self::copy_short_name_part(&mut short_name[8..11], &name[dot_index + 1..]);
(basename_len, basename_fits && ext_fits, basename_lossy || ext_lossy)
}
};
let chksum = Self::checksum(&name);
Self {
short_name,
chksum,
name_fits,
lossy_conv,
basename_len: basename_len as u8,
..Default::default()
}
}
fn generate_dot() -> [u8; SFN_SIZE] {
let mut short_name = [SFN_PADDING; SFN_SIZE];
short_name[0] = b'.';
short_name
}
fn generate_dotdot() -> [u8; SFN_SIZE] {
let mut short_name = [SFN_PADDING; SFN_SIZE];
short_name[0] = b'.';
short_name[1] = b'.';
short_name
}
fn copy_short_name_part(dst: &mut [u8], src: &str) -> (usize, bool, bool) {
let mut dst_pos = 0;
let mut lossy_conv = false;
for c in src.chars() {
if dst_pos == dst.len() {
// result buffer is full
return (dst_pos, false, lossy_conv);
}
// Make sure character is allowed in 8.3 name
let fixed_c = match c {
// strip spaces and dots
' ' | '.' => {
lossy_conv = true;
continue;
}
// copy allowed characters
'A'..='Z' | 'a'..='z' | '0'..='9' => c,
'!' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '-' | '@' | '^' | '_' | '`'
| '{' | '}' | '~' => c,
// replace disallowed characters by underscore
_ => '_',
};
// Update 'lossy conversion' flag
lossy_conv = lossy_conv || (fixed_c != c);
// short name is always uppercase
let upper = fixed_c.to_ascii_uppercase();
dst[dst_pos] = upper as u8; // SAFE: upper is in range 0x20-0x7F
dst_pos += 1;
}
(dst_pos, true, lossy_conv)
}
fn add_existing(&mut self, short_name: &[u8; SFN_SIZE]) {
// check for exact match collision
if short_name == &self.short_name {
self.exact_match = true;
}
// check for long prefix form collision (TEXTFI~1.TXT)
let long_prefix_len = cmp::min(self.basename_len, 6) as usize;
let num_suffix = if short_name[long_prefix_len] == b'~' {
(short_name[long_prefix_len + 1] as char).to_digit(10)
} else {
None
};
let long_prefix_matches =
short_name[..long_prefix_len] == self.short_name[..long_prefix_len];
let ext_matches = short_name[8..] == self.short_name[8..];
if long_prefix_matches && num_suffix.is_some() && ext_matches {
let num = num_suffix.unwrap(); // SAFE: checked in if condition
self.long_prefix_bitmap |= 1 << num;
}
// check for short prefix + checksum form collision (TE021F~1.TXT)
let short_prefix_len = cmp::min(self.basename_len, 2) as usize;
let num_suffix = if short_name[short_prefix_len + 4] == b'~' {
(short_name[short_prefix_len + 4 + 1] as char).to_digit(10)
} else {
None
};
let short_prefix_matches =
short_name[..short_prefix_len] == self.short_name[..short_prefix_len];
if short_prefix_matches && num_suffix.is_some() && ext_matches {
let chksum_res = str::from_utf8(&short_name[short_prefix_len..short_prefix_len + 4])
.map(|s| u16::from_str_radix(s, 16));
if chksum_res == Ok(Ok(self.chksum)) {
let num = num_suffix.unwrap(); // SAFE: checked in if condition
self.prefix_chksum_bitmap |= 1 << num;
}
}
}
fn checksum(name: &str) -> u16 {
// BSD checksum algorithm
let mut chksum = num::Wrapping(0u16);
for c in name.chars() {
chksum = (chksum >> 1) + (chksum << 15) + num::Wrapping(c as u16);
}
chksum.0
}
fn generate(&self) -> io::Result<[u8; SFN_SIZE]> {
if !self.lossy_conv && self.name_fits && !self.exact_match {
// If there was no lossy conversion and name fits into
// 8.3 convention and there is no collision return it as is
return Ok(self.short_name);
}
// Try using long 6-characters prefix
for i in 1..5 {
if self.long_prefix_bitmap & (1 << i) == 0 {
return Ok(self.build_prefixed_name(i, false));
}
}
// Try prefix with checksum
for i in 1..10 {
if self.prefix_chksum_bitmap & (1 << i) == 0 {
return Ok(self.build_prefixed_name(i, true));
}
}
// Too many collisions - fail
Err(io::Error::new(ErrorKind::AlreadyExists, "short name already exists"))
}
fn next_iteration(&mut self) {
// Try different checksum in next iteration
self.chksum = (num::Wrapping(self.chksum) + num::Wrapping(1)).0;
// Zero bitmaps
self.long_prefix_bitmap = 0;
self.prefix_chksum_bitmap = 0;
}
fn build_prefixed_name(&self, num: u32, with_chksum: bool) -> [u8; SFN_SIZE] {
let mut buf = [SFN_PADDING; SFN_SIZE];
let prefix_len = if with_chksum {
let prefix_len = cmp::min(self.basename_len as usize, 2);
buf[..prefix_len].copy_from_slice(&self.short_name[..prefix_len]);
buf[prefix_len..prefix_len + 4].copy_from_slice(&Self::u16_to_u8_array(self.chksum));
prefix_len + 4
} else {
let prefix_len = cmp::min(self.basename_len as usize, 6);
buf[..prefix_len].copy_from_slice(&self.short_name[..prefix_len]);
prefix_len
};
buf[prefix_len] = b'~';
buf[prefix_len + 1] = char::from_digit(num, 10).unwrap() as u8; // SAFE
buf[8..].copy_from_slice(&self.short_name[8..]);
buf
}
fn u16_to_u8_array(x: u16) -> [u8; 4] {
let c1 = char::from_digit((x as u32 >> 12) & 0xF, 16).unwrap().to_ascii_uppercase() as u8;
let c2 = char::from_digit((x as u32 >> 8) & 0xF, 16).unwrap().to_ascii_uppercase() as u8;
let c3 = char::from_digit((x as u32 >> 4) & 0xF, 16).unwrap().to_ascii_uppercase() as u8;
let c4 = char::from_digit((x as u32 >> 0) & 0xF, 16).unwrap().to_ascii_uppercase() as u8;
[c1, c2, c3, c4]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_path() {
assert_eq!(split_path("aaa/bbb/ccc"), Ok(("aaa", Some("bbb/ccc"))));
assert_eq!(split_path("aaa/bbb"), Ok(("aaa", Some("bbb"))));
assert_eq!(split_path("aaa"), Ok(("aaa", None)));
}
#[test]
fn test_generate_short_name() {
assert_eq!(&ShortNameGenerator::new("Foo").generate().unwrap(), b"FOO ");
assert_eq!(&ShortNameGenerator::new("Foo.b").generate().unwrap(), b"FOO B ");
assert_eq!(&ShortNameGenerator::new("Foo.baR").generate().unwrap(), b"FOO BAR");
assert_eq!(&ShortNameGenerator::new("Foo+1.baR").generate().unwrap(), b"FOO_1~1 BAR");
assert_eq!(&ShortNameGenerator::new("ver +1.2.text").generate().unwrap(), b"VER_12~1TEX");
assert_eq!(&ShortNameGenerator::new(".bashrc.swp").generate().unwrap(), b"BASHRC~1SWP");
assert_eq!(&ShortNameGenerator::new(".foo").generate().unwrap(), b"FOO~1 ");
}
#[test]
fn test_short_name_checksum_overflow() {
ShortNameGenerator::checksum("\u{FF5A}\u{FF5A}\u{FF5A}\u{FF5A}");
}
#[test]
fn test_lfn_checksum_overflow() {
lfn_checksum(&[
0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8,
]);
}
#[test]
fn test_generate_short_name_collisions_long() {
let mut buf: [u8; SFN_SIZE];
let mut gen = ShortNameGenerator::new("TextFile.Mine.txt");
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TEXTFI~1TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TEXTFI~2TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TEXTFI~3TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TEXTFI~4TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TE527D~1TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TE527D~2TXT");
for i in 3..10 {
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, format!("TE527D~{}TXT", i).as_bytes());
}
gen.add_existing(&buf);
assert!(gen.generate().is_err());
gen.next_iteration();
for _i in 0..4 {
buf = gen.generate().unwrap();
gen.add_existing(&buf);
}
buf = gen.generate().unwrap();
assert_eq!(&buf, b"TE527E~1TXT");
}
#[test]
fn test_generate_short_name_collisions_short() {
let mut buf: [u8; SFN_SIZE];
let mut gen = ShortNameGenerator::new("x.txt");
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X~1 TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X~2 TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X~3 TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X~4 TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X40DA~1 TXT");
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, b"X40DA~2 TXT");
}
#[test]
fn test_long_name_requires_long_name() {
assert_eq!(
validate_long_name("01234567.1234", b"01234567123").expect("should be valid"),
true
);
}
#[test]
fn test_dot_and_dotdot_does_not_require_long_name() {
assert_eq!(validate_long_name(".", b". ").expect("should be valid"), false);
assert_eq!(validate_long_name("..", b".. ").expect("should be valid"), false);
}
#[test]
fn test_lowercase_and_special_requires_long_name() {
assert_eq!(validate_long_name("abc", b"abc ").expect("should be valid"), true);
assert_eq!(validate_long_name(",", b", ").expect("should be valid"), true);
assert_eq!(
validate_long_name("FOO\u{100}", b"FOO ").expect("should be valid"),
true
);
}
#[test]
fn test_name_with_extension_does_not_require_long_name() {
assert_eq!(validate_long_name("NAME.EXT", b"NAME EXT").expect("should be valid"), false);
}
#[test]
fn test_name_with_long_extension_does_require_long_name() {
assert_eq!(validate_long_name("NAME.EXT1", b"NAME EXT").expect("should be valid"), true);
}
}