rust-fatfs/src/dir.rs
Rafał Harabień 2304b13ec4 Take immutable self reference in Dir methods
Dir methods do not change Dir object itself. They usually clone inner
stream so no change really happens self struct. Underlying partition is
modified but it does not have to affect API. For example see
std::fs::File::set_len. This change greatly simplifies rename API usage.
2018-06-12 00:07:30 +02:00

883 lines
33 KiB
Rust

#[cfg(feature = "alloc")]
use core::{slice, iter};
use core::{str, char, cmp};
use io::prelude::*;
use io;
use io::{ErrorKind, SeekFrom};
use fs::{FileSystemRef, DiskSlice};
use file::File;
use dir_entry::{DirEntry, DirEntryData, DirFileEntryData, DirLfnEntryData, FileAttributes, ShortName, DIR_ENTRY_SIZE};
#[cfg(feature = "alloc")]
use dir_entry::{LFN_PART_LEN, LFN_ENTRY_LAST_FLAG};
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::Vec;
#[derive(Clone)]
pub(crate) enum DirRawStream<'a, 'b: 'a> {
File(File<'a, 'b>),
Root(DiskSlice<'a, 'b>),
}
impl <'a, 'b> DirRawStream<'a, 'b> {
fn abs_pos(&self) -> Option<u64> {
match self {
&DirRawStream::File(ref file) => file.abs_pos(),
&DirRawStream::Root(ref slice) => Some(slice.abs_pos()),
}
}
fn first_cluster(&self) -> Option<u32> {
match self {
&DirRawStream::File(ref file) => file.first_cluster(),
&DirRawStream::Root(_) => None,
}
}
}
impl <'a, 'b> Read for DirRawStream<'a, 'b> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
&mut DirRawStream::File(ref mut file) => file.read(buf),
&mut DirRawStream::Root(ref mut raw) => raw.read(buf),
}
}
}
impl <'a, 'b> Write for DirRawStream<'a, 'b> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
&mut DirRawStream::File(ref mut file) => file.write(buf),
&mut DirRawStream::Root(ref mut raw) => raw.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
&mut DirRawStream::File(ref mut file) => file.flush(),
&mut DirRawStream::Root(ref mut raw) => raw.flush(),
}
}
}
impl <'a, 'b> Seek for DirRawStream<'a, 'b> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
&mut DirRawStream::File(ref mut file) => file.seek(pos),
&mut DirRawStream::Root(ref mut raw) => raw.seek(pos),
}
}
}
fn split_path<'c>(path: &'c str) -> (&'c str, Option<&'c 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
let rest_opt = path_split.next();
(comp, rest_opt)
}
/// FAT directory
#[derive(Clone)]
pub struct Dir<'a, 'b: 'a> {
stream: DirRawStream<'a, 'b>,
fs: FileSystemRef<'a, 'b>,
}
impl <'a, 'b> Dir<'a, 'b> {
pub(crate) fn new(stream: DirRawStream<'a, 'b>, fs: FileSystemRef<'a, 'b>) -> Self {
Dir { stream, fs }
}
/// Creates directory entries iterator
pub fn iter(&self) -> DirIter<'a, 'b> {
DirIter {
stream: self.stream.clone(),
fs: self.fs.clone(),
err: false,
}
}
fn find_entry(&self, name: &str, is_dir: Option<bool>, mut short_name_gen: Option<&mut ShortNameGenerator>) -> io::Result<DirEntry<'a, 'b>> {
for r in self.iter() {
let e = r?;
// compare name ignoring case
if e.file_name().eq_ignore_ascii_case(name) || e.short_file_name().eq_ignore_ascii_case(name) {
// check if file or directory is expected
if is_dir.is_some() && Some(e.is_dir()) != is_dir {
return Err(io::Error::new(ErrorKind::NotFound, "unexpected file type in a path"))
}
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, "file not found"))
}
/// Opens existing directory
pub fn open_dir(&self, path: &str) -> io::Result<Self> {
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.
pub fn open_file(&self, path: &str) -> io::Result<File<'a, 'b>> {
// 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 file or opens existing without truncating.
pub fn create_file(&self, path: &str) -> io::Result<File<'a, 'b>> {
// 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);
}
// this is final filename in the path
let mut short_name_gen = ShortNameGenerator::new(name);
let r = self.find_entry(name, Some(false), Some(&mut short_name_gen));
match r {
// file does not exist - create it
Err(ref err) if err.kind() == ErrorKind::NotFound => {
let short_name = short_name_gen.generate()?;
let sfn_entry = self.create_sfn_entry(short_name, FileAttributes::from_bits_truncate(0), None);
Ok(self.write_entry(name, sfn_entry)?.to_file())
},
// other error
Err(err) => Err(err),
// file already exists - return it
Ok(e) => Ok(e.to_file()),
}
}
/// Creates new directory or opens existing.
pub fn create_dir(&self, path: &str) -> io::Result<Self> {
// 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);
}
// this is final filename in the path
let mut short_name_gen = ShortNameGenerator::new(name);
let r = self.find_entry(name, Some(false), Some(&mut short_name_gen));
match r {
// directory does not exist - create it
Err(ref err) if err.kind() == ErrorKind::NotFound => {
// alloc cluster for directory data
let cluster = self.fs.alloc_cluster(None)?;
// create entry in parent directory
let short_name = short_name_gen.generate()?;
let sfn_entry = self.create_sfn_entry(short_name, FileAttributes::DIRECTORY, Some(cluster));
let entry = self.write_entry(name, sfn_entry)?;
let mut dir = entry.to_dir();
// create special entries "." and ".."
let dot_sfn = ShortNameGenerator::new(".").generate().unwrap();
let sfn_entry = self.create_sfn_entry(dot_sfn, FileAttributes::DIRECTORY, entry.first_cluster());
dir.write_entry(".", sfn_entry)?;
let dotdot_sfn = ShortNameGenerator::new("..").generate().unwrap();
let sfn_entry = self.create_sfn_entry(dotdot_sfn, FileAttributes::DIRECTORY, self.stream.first_cluster());
dir.write_entry("..", sfn_entry)?;
Ok(dir)
},
// other error
Err(err) => Err(err),
// directory already exists - return it
Ok(e) => Ok(e.to_dir()),
}
}
fn is_empty(&self) -> io::Result<bool> {
// check if directory contains no files
for r in self.iter() {
let e = r?;
let name = e.file_name();
// ignore special entries "." and ".."
if name != "." && name != ".." {
return Ok(false);
}
}
Ok(true)
}
/// Removes existing file or 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<()> {
// 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);
}
trace!("removing {}", path);
// 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::NotFound, "removing non-empty directory is denied"));
}
// 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.clone();
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_free();
stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?;
data.serialize(&mut stream)?;
}
Ok(())
}
/// Renames or moves existing file or directory.
///
/// Destination directory can be cloned source directory in case of rename without moving operation.
/// 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, dst_path: &str) -> io::Result<()> {
// 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, &mut e.to_dir(), rest);
}
// move/rename file
self.rename_internal(src_path, dst_dir, dst_path)
}
fn rename_internal(&self, src_name: &str, dst_dir: &Dir, dst_name: &str) -> io::Result<()> {
trace!("moving {} to {}", src_name, dst_name);
// find existing file
let e = self.find_entry(src_name, None, None)?;
// free long and short name entries
let mut stream = self.stream.clone();
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_free();
stream.seek(SeekFrom::Current(-(DIR_ENTRY_SIZE as i64)))?;
data.serialize(&mut stream)?;
}
// check if destionation filename is unused
let mut short_name_gen = ShortNameGenerator::new(dst_name);
let r = dst_dir.find_entry(dst_name, None, Some(&mut short_name_gen));
if r.is_ok() {
return Err(io::Error::new(ErrorKind::AlreadyExists, "destination file already exists"))
}
// save new directory entry
let short_name = short_name_gen.generate()?;
let sfn_entry = e.data.renamed(short_name);
dst_dir.write_entry(dst_name, sfn_entry)?;
Ok(())
}
fn find_free_entries(&self, num_entries: usize) -> io::Result<DirRawStream<'a, 'b>> {
let mut stream = self.stream.clone();
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_free() {
// 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;
}
}
#[cfg(feature = "alloc")]
fn create_lfn_entries(&self, name: &str, short_name: &[u8]) -> io::Result<(DirRawStream<'a, 'b>, u64)> {
// get short name checksum
let lfn_chsum = lfn_checksum(&short_name);
// convert long name to UTF-16
let lfn_utf16 = name.encode_utf16().collect::<Vec<u16>>();
let lfn_iter = LfnEntriesGenerator::new(&lfn_utf16, lfn_chsum);
// find space for new entries
let num_entries = lfn_iter.len() + 1; // multiple lfn entries + one file entry
let mut stream = self.find_free_entries(num_entries)?;
let start_pos = stream.seek(io::SeekFrom::Current(0))?;
// write LFN entries first
for lfn_entry in lfn_iter {
lfn_entry.serialize(&mut stream)?;
}
Ok((stream, start_pos))
}
#[cfg(not(feature = "alloc"))]
fn create_lfn_entries(&self, _name: &str, _short_name: &[u8]) -> io::Result<(DirRawStream<'a, 'b>, u64)> {
let mut stream = self.find_free_entries(1)?;
let start_pos = stream.seek(io::SeekFrom::Current(0))?;
Ok((stream, start_pos))
}
fn create_sfn_entry(&self, short_name: [u8; 11], 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());
raw_entry.reset_created();
raw_entry.reset_accessed();
raw_entry.reset_modified();
raw_entry
}
fn write_entry(&self, name: &str, raw_entry: DirFileEntryData) -> io::Result<DirEntry<'a, 'b>> {
trace!("write_entry {}", name);
// check if name doesn't contain unsupported characters
validate_long_name(name)?;
// generate long entries
let (mut stream, start_pos) = self.create_lfn_entries(&name, raw_entry.name())?;
// write short name entry
raw_entry.serialize(&mut stream)?;
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());
return Ok(DirEntry {
data: raw_entry,
short_name,
#[cfg(feature = "alloc")]
lfn: Vec::new(),
fs: self.fs,
entry_pos: abs_pos.unwrap(), // SAFE: abs_pos is absent only for empty file
offset_range: (start_pos, end_pos),
});
}
}
/// Directory entries iterator.
#[derive(Clone)]
pub struct DirIter<'a, 'b: 'a> {
stream: DirRawStream<'a, 'b>,
fs: FileSystemRef<'a, 'b>,
err: bool,
}
impl <'a, 'b> DirIter<'a, 'b> {
fn read_dir_entry(&mut self) -> io::Result<Option<DirEntry<'a, 'b>>> {
#[cfg(feature = "alloc")]
let mut lfn_buf = LongNameBuilder::new();
let mut offset = self.stream.seek(SeekFrom::Current(0))?;
let mut begin_offset = offset;
loop {
let raw_entry = DirEntryData::deserialize(&mut self.stream)?;
offset += DIR_ENTRY_SIZE;
match raw_entry {
DirEntryData::File(data) => {
// Check if this is end of dif
if data.is_end() {
return Ok(None);
}
// Check if this is deleted or volume ID entry
if data.is_free() || data.is_volume() {
#[cfg(feature = "alloc")]
lfn_buf.clear();
begin_offset = offset;
continue;
}
// Get entry position on volume
let abs_pos = self.stream.abs_pos().map(|p| p - DIR_ENTRY_SIZE);
// Check if LFN checksum is valid
#[cfg(feature = "alloc")]
lfn_buf.validate_chksum(data.name());
// Return directory entry
let short_name = ShortName::new(data.name());
return Ok(Some(DirEntry {
data,
short_name,
#[cfg(feature = "alloc")]
lfn: lfn_buf.to_vec(),
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) => {
// Check if this is deleted entry
if data.is_free() {
#[cfg(feature = "alloc")]
lfn_buf.clear();
begin_offset = offset;
continue;
}
// Append to LFN buffer
#[cfg(feature = "alloc")]
lfn_buf.process(&data);
}
}
}
}
}
impl <'a, 'b> Iterator for DirIter<'a, 'b> {
type Item = io::Result<DirEntry<'a, 'b>>;
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) -> io::Result<()> {
// check if length is valid
if name.len() == 0 {
return Err(io::Error::new(ErrorKind::InvalidInput, "filename cannot be empty"));
}
if name.len() > 255 {
return Err(io::Error::new(ErrorKind::InvalidInput, "filename is too long"));
}
// check if there are only valid characters
for c in name.chars() {
match c {
'a'...'z' | 'A'...'Z' | '0'...'9' | '\u{80}'...'\u{FFFF}' |
'$' | '%' | '\'' | '-' | '_' | '@' | '~' | '`' | '!' | '(' | ')' | '{' | '}' |
'.' | ' ' | '+' | ',' | ';' | '=' | '[' | ']' => {},
_ => return Err(io::Error::new(ErrorKind::InvalidInput, "invalid character in filename")),
}
}
Ok(())
}
#[cfg(feature = "alloc")]
fn lfn_checksum(short_name: &[u8]) -> u8 {
let mut chksum = 0u8;
for i in 0..11 {
chksum = (((chksum & 1) << 7) as u16 + (chksum >> 1) as u16 + short_name[i] as u16) as u8;
}
chksum
}
#[cfg(feature = "alloc")]
struct LongNameBuilder {
buf: Vec<u16>,
chksum: u8,
index: u8,
}
#[cfg(feature = "alloc")]
impl LongNameBuilder {
fn new() -> Self {
LongNameBuilder {
buf: Vec::<u16>::new(),
chksum: 0,
index: 0,
}
}
fn clear(&mut self) {
self.buf.clear();
self.index = 0;
}
fn to_vec(mut self) -> Vec<u16> {
if self.index == 1 {
self.truncate();
self.buf
} else {
warn!("unfinished LFN sequence {}", self.index);
Vec::<u16>::new()
}
}
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[lfn_len-1] {
0xFFFF | 0 => lfn_len -= 1,
_ => break,
}
}
self.buf.truncate(lfn_len);
}
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!("currupted 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.resize(index as usize * LFN_PART_LEN, 0);
} else if self.index == 0 || index != self.index - 1 || data.checksum() != self.chksum {
// Corrupted entry
warn!("currupted 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[pos..pos+13]);
}
fn validate_chksum(&mut self, short_name: &[u8]) {
let chksum = lfn_checksum(short_name);
if chksum != self.chksum {
warn!("checksum mismatch {:x} {:x} {:?}", chksum, self.chksum, short_name);
self.clear();
}
}
}
#[cfg(feature = "alloc")]
struct LfnEntriesGenerator<'a> {
name_parts_iter: iter::Rev<slice::Chunks<'a, u16>>,
checksum: u8,
index: usize,
num: usize,
ended: bool,
}
#[cfg(feature = "alloc")]
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 = "alloc")]
impl<'a> Iterator for LfnEntriesGenerator<'a> {
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 ' '
let mut lfn_part = [0xFFFFu16; 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 = "alloc")]
impl<'a> ExactSizeIterator for LfnEntriesGenerator<'a> {}
#[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; 11],
}
impl ShortNameGenerator {
fn new(name: &str) -> Self {
// padded by ' '
let mut short_name = [0x20u8; 11];
// find extension after last dot
let (basename_len, name_fits, lossy_conv) = match name.rfind('.') {
Some(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[..index]);
let (_, ext_fits, ext_lossy) = Self::copy_short_name_part(&mut short_name[8..11], &name[index+1..]);
(basename_len, basename_fits && ext_fits, basename_lossy || ext_lossy)
},
None => {
// 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)
}
};
let chksum = Self::checksum(name);
Self {
short_name, chksum, name_fits, lossy_conv,
basename_len: basename_len as u8,
..Default::default()
}
}
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,
// 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; 11]) {
// 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 prefix_len = cmp::min(self.basename_len, 6) as usize;
let num_suffix = if short_name[prefix_len] as char == '~' { (short_name[prefix_len+1] as char).to_digit(10) } else { None };
let ext_matches = short_name[8..] == self.short_name[8..];
if short_name[..prefix_len] == self.short_name[..prefix_len] && num_suffix.is_some() && ext_matches {
let num = num_suffix.unwrap(); // SAFE
self.long_prefix_bitmap |= 1 << num;
}
// check for short prefix + checksum form collision (TE021F~1.TXT)
let prefix_len = cmp::min(self.basename_len, 2) as usize;
let num_suffix = if short_name[prefix_len+4] as char == '~' { (short_name[prefix_len+4+1] as char).to_digit(10) } else { None };
if short_name[..prefix_len] == self.short_name[..prefix_len] && num_suffix.is_some() && ext_matches {
let chksum_res = str::from_utf8(&short_name[prefix_len..prefix_len+4]).map(|s| u16::from_str_radix(s, 16));
if chksum_res == Ok(Ok(self.chksum)) {
let num = num_suffix.unwrap(); // SAFE
self.prefix_chksum_bitmap |= 1 << num;
}
}
}
fn checksum(name: &str) -> u16 {
// BSD checksum algorithm
let mut chksum = 0u16;
for c in name.chars() {
chksum = (chksum >> 1) + ((chksum & 1) << 15) + (c as u16);
}
chksum
}
fn generate(&self) -> io::Result<[u8; 11]> {
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 build_prefixed_name(&self, num: u32, with_chksum: bool) -> [u8; 11] {
let mut buf = [0x20u8; 11];
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] = '~' as u8;
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;
return [c1, c2, c3, c4]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_split_path() {
assert_eq!(split_path("aaa/bbb/ccc"), ("aaa", Some("bbb/ccc")));
assert_eq!(split_path("aaa/bbb"), ("aaa", Some("bbb")));
assert_eq!(split_path("aaa"), ("aaa", None));
}
#[test]
fn test_generate_short_name() {
assert_eq!(&ShortNameGenerator::new("Foo").generate().unwrap(), "FOO ".as_bytes());
assert_eq!(&ShortNameGenerator::new("Foo.b").generate().unwrap(), "FOO B ".as_bytes());
assert_eq!(&ShortNameGenerator::new("Foo.baR").generate().unwrap(), "FOO BAR".as_bytes());
assert_eq!(&ShortNameGenerator::new("Foo+1.baR").generate().unwrap(), "FOO_1~1 BAR".as_bytes());
assert_eq!(&ShortNameGenerator::new("ver +1.2.text").generate().unwrap(), "VER_12~1TEX".as_bytes());
assert_eq!(&ShortNameGenerator::new(".bashrc.swp").generate().unwrap(), "BASHRC~1SWP".as_bytes());
}
#[test]
fn test_generate_short_name_collisions_long() {
let mut buf: [u8; 11];
let mut gen = ShortNameGenerator::new("TextFile.Mine.txt");
buf = gen.generate().unwrap();
assert_eq!(&buf, "TEXTFI~1TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "TEXTFI~2TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "TEXTFI~3TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "TEXTFI~4TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "TE527D~1TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "TE527D~2TXT".as_bytes());
}
#[test]
fn test_generate_short_name_collisions_short() {
let mut buf: [u8; 11];
let mut gen = ShortNameGenerator::new("x.txt");
buf = gen.generate().unwrap();
assert_eq!(&buf, "X TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X~1 TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X~2 TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X~3 TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X~4 TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X40DA~1 TXT".as_bytes());
gen.add_existing(&buf);
buf = gen.generate().unwrap();
assert_eq!(&buf, "X40DA~2 TXT".as_bytes());
}
}