From 35d03daae909049214989365123ab2d5d4db8100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Harabie=C5=84?= Date: Thu, 10 May 2018 15:00:59 +0200 Subject: [PATCH] Support no_std without alloc --- Cargo.toml | 8 ++++-- README.md | 6 ++++- build-nostd.sh | 2 ++ src/dir.rs | 58 +++++++++++++++++++++++++++++++++-------- src/dir_entry.rs | 67 ++++++++++++++++++++++++++++++++++++++++-------- src/fs.rs | 19 +++++++++++++- src/lib.rs | 9 ++++++- 7 files changed, 142 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 057a09b..62692e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,15 +18,19 @@ exclude = [ travis-ci = { repository = "rafalh/rust-fatfs" } [features] +# Use Rust std library std = [] -default = ["chrono", "std"] +# Use dynamic allocation - required for LFN support +alloc = ["core_io/collections"] +# Default features +default = ["chrono", "std", "alloc"] [dependencies] byteorder = "1" bitflags = "1.0" log = "0.4" chrono = { version = "0.4", optional = true } -core_io = { version = "0.1", features = ["collections"], optional = true } +core_io = { version = "0.1", optional = true } [dev-dependencies] env_logger = "0.5" diff --git a/README.md b/README.md index 7159eb4..0ceb81a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,11 @@ Put this in your `Cargo.toml`: [dependencies] fatfs = { version = "0.2", features = ["core_io"], default-features = false } -Note: alloc crate is required at the moment so when std is disabled, custom allocator has to be provided. +Note: LFN support requires `alloc` feature and makes use of `alloc` crate. +You may have to provide a memory allocator implementation. + +For building in `no_std` mode nightly Rust version compatible with current `core_io` crate is required. +See date string in `core_io` dependency version. License ------- diff --git a/build-nostd.sh b/build-nostd.sh index bda3f82..9dbd382 100755 --- a/build-nostd.sh +++ b/build-nostd.sh @@ -1,2 +1,4 @@ #!/bin/sh +set -e cargo build --no-default-features --features core_io +cargo build --no-default-features --features core_io,alloc diff --git a/src/dir.rs b/src/dir.rs index de51ce2..66b4e78 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -1,5 +1,5 @@ -use core::slice; -use core::iter; +#[cfg(feature = "alloc")] +use core::{slice, iter}; use io::prelude::*; use io; @@ -7,10 +7,12 @@ use io::{ErrorKind, SeekFrom}; use fs::{FileSystemRef, DiskSlice}; use file::File; -use dir_entry::{DirEntry, DirEntryData, DirFileEntryData, DirLfnEntryData, FileAttributes, - DIR_ENTRY_SIZE, LFN_PART_LEN, LFN_ENTRY_LAST_FLAG}; +use dir_entry::{DirEntry, DirEntryData, DirFileEntryData, DirLfnEntryData, FileAttributes, ShortName, DIR_ENTRY_SIZE}; -#[cfg(not(feature = "std"))] +#[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)] @@ -254,13 +256,11 @@ impl <'a, 'b> Dir<'a, 'b> { } } - fn create_entry(&mut self, name: &str, attrs: FileAttributes, first_cluster: Option) -> io::Result> { - trace!("create_entry {}", name); - // check if name doesn't contain unsupported characters - validate_long_name(name)?; - // generate short name and long entries - let short_name = generate_short_name(name); + #[cfg(feature = "alloc")] + fn create_lfn_entries(&mut 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::>(); let lfn_iter = LfnEntriesGenerator::new(&lfn_utf16, lfn_chsum); // find space for new entries @@ -271,6 +271,23 @@ impl <'a, 'b> Dir<'a, 'b> { for lfn_entry in lfn_iter { lfn_entry.serialize(&mut stream)?; } + Ok((stream, start_pos)) + } + #[cfg(not(feature = "alloc"))] + fn create_lfn_entries(&mut 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_entry(&mut self, name: &str, attrs: FileAttributes, first_cluster: Option) -> io::Result> { + trace!("create_entry {}", name); + // check if name doesn't contain unsupported characters + validate_long_name(name)?; + // generate short name + let short_name = generate_short_name(name); + // generate long entries + let (mut stream, start_pos) = self.create_lfn_entries(&name, &short_name)?; // create and write short name entry let mut raw_entry = DirFileEntryData::new(short_name, attrs); raw_entry.set_first_cluster(first_cluster, self.fs.fat_type()); @@ -281,8 +298,11 @@ impl <'a, 'b> Dir<'a, 'b> { 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 @@ -301,6 +321,7 @@ pub struct DirIter<'a, 'b: 'a> { impl <'a, 'b> DirIter<'a, 'b> { fn read_dir_entry(&mut self) -> io::Result>> { + #[cfg(feature = "alloc")] let mut lfn_buf = LongNameBuilder::new(); let mut offset = self.stream.seek(SeekFrom::Current(0))?; let mut begin_offset = offset; @@ -315,6 +336,7 @@ impl <'a, 'b> DirIter<'a, 'b> { } // 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; @@ -322,9 +344,14 @@ impl <'a, 'b> DirIter<'a, 'b> { // 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 @@ -334,11 +361,13 @@ impl <'a, 'b> DirIter<'a, 'b> { 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); } } @@ -420,6 +449,7 @@ fn validate_long_name(name: &str) -> io::Result<()> { Ok(()) } +#[cfg(feature = "alloc")] fn lfn_checksum(short_name: &[u8]) -> u8 { let mut chksum = 0u8; for i in 0..11 { @@ -428,12 +458,14 @@ fn lfn_checksum(short_name: &[u8]) -> u8 { chksum } +#[cfg(feature = "alloc")] struct LongNameBuilder { buf: Vec, chksum: u8, index: u8, } +#[cfg(feature = "alloc")] impl LongNameBuilder { fn new() -> LongNameBuilder { LongNameBuilder { @@ -510,6 +542,7 @@ impl LongNameBuilder { } } +#[cfg(feature = "alloc")] struct LfnEntriesGenerator<'a> { name_parts_iter: iter::Rev>, checksum: u8, @@ -518,6 +551,7 @@ struct LfnEntriesGenerator<'a> { 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; @@ -532,6 +566,7 @@ impl<'a> LfnEntriesGenerator<'a> { } } +#[cfg(feature = "alloc")] impl<'a> Iterator for LfnEntriesGenerator<'a> { type Item = DirLfnEntryData; @@ -577,4 +612,5 @@ impl<'a> Iterator for LfnEntriesGenerator<'a> { } // name_parts_iter is ExactSizeIterator so size_hint returns one limit +#[cfg(feature = "alloc")] impl<'a> ExactSizeIterator for LfnEntriesGenerator<'a> {} diff --git a/src/dir_entry.rs b/src/dir_entry.rs index 87e502d..6ae0e14 100644 --- a/src/dir_entry.rs +++ b/src/dir_entry.rs @@ -1,4 +1,4 @@ -use core::fmt; +use core::{fmt, str}; use io::prelude::*; use io; use io::Cursor; @@ -10,7 +10,7 @@ use chrono::{TimeZone, Local, Datelike, Timelike}; #[cfg(feature = "chrono")] use chrono; -#[cfg(not(feature = "std"))] +#[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::{Vec, String, string::ToString}; use fs::{FileSystemRef, FatType}; @@ -37,6 +37,45 @@ pub(crate) const DIR_ENTRY_SIZE: u64 = 32; pub(crate) const DIR_ENTRY_FREE_FLAG: u8 = 0xE5; pub(crate) const LFN_ENTRY_LAST_FLAG: u8 = 0x40; +/// Decoded file short name +#[derive(Clone, Debug, Default)] +pub(crate) struct ShortName { + name: [u8; 12], + len: u8, +} + +impl ShortName { + pub(crate) fn new(raw_name: &[u8; 11]) -> Self { + // get name components length by looking for space character + const SPACE: u8 = ' ' as u8; + let name_len = raw_name[0..8].iter().position(|x| *x == SPACE).unwrap_or(8); + let ext_len = raw_name[8..11].iter().position(|x| *x == SPACE).unwrap_or(3); + let mut name = [SPACE; 12]; + name[..name_len].copy_from_slice(&raw_name[..name_len]); + let total_len = if ext_len > 0 { + name[name_len] = '.' as u8; + name[name_len+1..name_len+1+ext_len].copy_from_slice(&raw_name[8..8+ext_len]); + // Return total name length + name_len+1+ext_len + } else { + // No extension - return length of name part + name_len + }; + // Short names in FAT filesystem are encoded in OEM code-page. Rust operates on UTF-8 strings + // and there is no built-in conversion so strip non-ascii characters in the name. + use strip_non_ascii; + strip_non_ascii(&mut name); + ShortName { + name, + len: total_len as u8, + } + } + + fn to_str(&self) -> &str { + str::from_utf8(&self.name[..self.len as usize]).unwrap() // SAFE: all characters outside of ASCII table has been removed + } +} + #[allow(dead_code)] #[derive(Clone, Debug, Default)] pub(crate) struct DirFileEntryData { @@ -251,10 +290,12 @@ impl DirLfnEntryData { Ok(()) } + #[cfg(feature = "alloc")] pub(crate) fn order(&self) -> u8 { self.order } + #[cfg(feature = "alloc")] pub(crate) fn checksum(&self) -> u8 { self.checksum } @@ -539,6 +580,8 @@ impl DirEntryEditor { #[derive(Clone)] pub struct DirEntry<'a, 'b: 'a> { pub(crate) data: DirFileEntryData, + pub(crate) short_name: ShortName, + #[cfg(feature = "alloc")] pub(crate) lfn: Vec, pub(crate) entry_pos: u64, pub(crate) offset_range: (u64, u64), @@ -547,19 +590,17 @@ pub struct DirEntry<'a, 'b: 'a> { impl <'a, 'b> DirEntry<'a, 'b> { /// Returns short file name + #[cfg(feature = "alloc")] pub fn short_file_name(&self) -> String { - let name_str = String::from_utf8_lossy(&self.data.name[0..8]); - let ext_str = String::from_utf8_lossy(&self.data.name[8..11]); - let name_trimmed = name_str.trim_right(); - let ext_trimmed = ext_str.trim_right(); - if ext_trimmed.is_empty() { - name_trimmed.to_string() - } else { - format!("{}.{}", name_trimmed, ext_trimmed) - } + self.short_name.to_str().to_string() + } + #[cfg(not(feature = "alloc"))] + pub fn short_file_name(&self) -> &str { + self.short_name.to_str() } /// Returns long file name or if it doesn't exist fallbacks to short file name. + #[cfg(feature = "alloc")] pub fn file_name(&self) -> String { if self.lfn.len() > 0 { String::from_utf16_lossy(&self.lfn) @@ -567,6 +608,10 @@ impl <'a, 'b> DirEntry<'a, 'b> { self.short_file_name() } } + #[cfg(not(feature = "alloc"))] + pub fn file_name(&self) -> &str { + self.short_file_name() + } /// Returns file attributes pub fn attributes(&self) -> FileAttributes { diff --git a/src/fs.rs b/src/fs.rs index f17dcbc..f717f4d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -11,8 +11,10 @@ use dir::{DirRawStream, Dir}; use dir_entry::DIR_ENTRY_SIZE; use table::{ClusterIterator, alloc_cluster, read_fat_flags}; -#[cfg(not(feature = "std"))] +#[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::{String, string::ToString}; +#[cfg(all(not(feature = "std"), not(feature = "alloc")))] +use core::str; // FAT implementation based on: // http://wiki.osdev.org/FAT @@ -46,6 +48,14 @@ impl ReadSeek for T where T: Read + Seek {} pub trait ReadWriteSeek: Read + Write + Seek {} impl ReadWriteSeek for T where T: Read + Write + Seek {} +pub(crate) fn strip_non_ascii(slice: &mut [u8]) { + for c in slice { + if *c < 0x20 || *c >= 0x80 { + *c = '_' as u8; + } + } +} + #[allow(dead_code)] #[derive(Default, Debug, Clone)] struct BiosParameterBlock { @@ -130,6 +140,8 @@ impl BiosParameterBlock { rdr.read_exact(&mut bpb.volume_label)?; rdr.read_exact(&mut bpb.fs_type_label)?; } + // Strip non-ascii characters from volume label + strip_non_ascii(&mut bpb.volume_label); if bpb.ext_sig != 0x29 { // fields after ext_sig are not used - clean them bpb.volume_id = 0; @@ -284,9 +296,14 @@ impl <'a> FileSystem<'a> { /// /// Note: File with VOLUME_ID attribute in root directory is ignored by this library. /// Only label from BPB is used. + #[cfg(feature = "alloc")] pub fn volume_label(&self) -> String { String::from_utf8_lossy(&self.bpb.volume_label).trim_right().to_string() } + #[cfg(not(feature = "alloc"))] + pub fn volume_label(&self) -> &str { + str::from_utf8(&self.bpb.volume_label).unwrap_or("").trim_right() + } /// Returns root directory object allowing futher penetration of filesystem structure. pub fn root_dir<'b>(&'b self) -> Dir<'b, 'a> { diff --git a/src/lib.rs b/src/lib.rs index 0677265..77702ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,9 @@ #![cfg_attr(not(feature="std"), no_std)] #![cfg_attr(not(feature="std"), feature(alloc))] +// Disable warnings to not clutter code with cfg too much +#![cfg_attr(not(feature="alloc"), allow(dead_code, unused_imports))] + extern crate byteorder; #[macro_use] @@ -18,7 +21,7 @@ extern crate chrono; #[cfg(not(feature = "std"))] extern crate core_io; -#[cfg(not(feature = "std"))] +#[cfg(all(not(feature = "std"), feature = "alloc"))] #[macro_use] extern crate alloc; @@ -27,6 +30,8 @@ mod dir; mod dir_entry; mod file; mod table; + +#[cfg(feature = "alloc")] mod utils; #[cfg(not(feature = "std"))] @@ -48,4 +53,6 @@ pub use fs::*; pub use dir::*; pub use dir_entry::*; pub use file::*; + +#[cfg(feature = "alloc")] pub use utils::*;