diff --git a/README.md b/README.md index d72a624..283004c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,17 @@ Rust FAT Introduction ------------ -FAT filesystem library implemented in Rust. +FAT filesystem read-only library implemented in Rust. -Supports filesystem versions: FAT12, FAT16, FAT32. Library is read-only at this point but write support is planned. LFN (Long File Name) is not supported yet. +Features: +* FAT12, FAT16, FAT32 filesystem versions, +* read directory, +* read file, +* LFN (Long File Names). + +Planned features: +* write support, +* no_std environment support. License ------- diff --git a/examples/cat.rs b/examples/cat.rs index 7e5faa9..38ed191 100644 --- a/examples/cat.rs +++ b/examples/cat.rs @@ -13,7 +13,7 @@ fn main() { let buf_rdr = BufReader::new(file); let mut fs = FatFileSystem::new(Box::new(buf_rdr)).unwrap(); let mut root_dir = fs.root_dir(); - let mut file = root_dir.get_file(&env::args().nth(1).unwrap()).unwrap(); + let mut file = root_dir.open_file(&env::args().nth(1).unwrap()).unwrap(); let mut buf = vec![]; file.read_to_end(&mut buf).unwrap(); print!("{}", str::from_utf8(&buf).unwrap()); diff --git a/examples/ls.rs b/examples/ls.rs index 888eb53..8e32fb7 100644 --- a/examples/ls.rs +++ b/examples/ls.rs @@ -30,7 +30,7 @@ fn main() { let mut dir = match env::args().nth(1) { None => root_dir, Some(ref path) if path == "." => root_dir, - Some(ref path) => root_dir.get_dir(&path).unwrap(), + Some(ref path) => root_dir.open_dir(&path).unwrap(), }; let entries = dir.list().unwrap(); for e in entries { diff --git a/resources/fat12.img b/resources/fat12.img index 6ae8a19..7f051a5 100644 Binary files a/resources/fat12.img and b/resources/fat12.img differ diff --git a/resources/fat16.img b/resources/fat16.img index a66d617..c79f99e 100644 Binary files a/resources/fat16.img and b/resources/fat16.img differ diff --git a/resources/fat32.img b/resources/fat32.img index 00c5e73..1f66512 100644 Binary files a/resources/fat32.img and b/resources/fat32.img differ diff --git a/scripts/create-test-img.sh b/scripts/create-test-img.sh index 2581c41..acd048d 100755 --- a/scripts/create-test-img.sh +++ b/scripts/create-test-img.sh @@ -7,15 +7,18 @@ create_test_img() { local blkcount=$2 local fatSize=$3 dd if=/dev/zero of="$name" bs=1024 count=$blkcount - mkfs.vfat -s 1 -F $fatSize "$name" + mkfs.vfat -s 1 -F $fatSize -n "Test!" -i 12345678 "$name" mkdir -p mnt sudo mount -o loop "$name" mnt -o rw,uid=$USER,gid=$USER for i in {1..1000}; do echo "Rust is cool!" >>"mnt/long.txt" done echo "Rust is cool!" >>"mnt/short.txt" - mkdir -p mnt/very/long/path + mkdir -p "mnt/very/long/path" echo "Rust is cool!" >>"mnt/very/long/path/test.txt" + mkdir -p "mnt/very-long-dir-name" + echo "Rust is cool!" >>"mnt/very-long-dir-name/very-long-file-name.txt" + sudo umount mnt } diff --git a/src/dir.rs b/src/dir.rs index 8aba99d..8f0bb3c 100644 --- a/src/dir.rs +++ b/src/dir.rs @@ -2,7 +2,7 @@ use std::ascii::AsciiExt; use std::fmt; use std::io::prelude::*; use std::io; -use std::io::{ErrorKind, SeekFrom}; +use std::io::{Cursor, ErrorKind, SeekFrom}; use std::str; use byteorder::{LittleEndian, ReadBytesExt}; use chrono::{DateTime, Date, TimeZone, Local}; @@ -11,6 +11,7 @@ use fs::{FatSharedStateRef, ReadSeek}; use file::FatFile; bitflags! { + #[derive(Default)] pub struct FatFileAttributes: u8 { const READ_ONLY = 0x01; const HIDDEN = 0x02; @@ -24,8 +25,8 @@ bitflags! { } #[allow(dead_code)] -#[derive(Clone, Copy, Debug)] -pub struct FatDirEntryData { +#[derive(Clone, Debug, Default)] +struct FatDirFileEntryData { name: [u8; 11], attrs: FatFileAttributes, reserved_0: u8, @@ -40,19 +41,47 @@ pub struct FatDirEntryData { size: u32, } +#[allow(dead_code)] +#[derive(Clone, Debug, Default)] +struct FatDirLfnEntryData { + order: u8, + name_0: [u16; 5], + attrs: FatFileAttributes, + entry_type: u8, + checksum: u8, + name_1: [u16; 6], + reserved_0: u16, + name_2: [u16; 2], +} + +#[derive(Clone, Debug)] +enum FatDirEntryData { + File(FatDirFileEntryData), + Lfn(FatDirLfnEntryData), +} + #[derive(Clone)] pub struct FatDirEntry { - data: FatDirEntryData, + data: FatDirFileEntryData, + lfn: Vec, state: FatSharedStateRef, } impl FatDirEntry { - pub fn file_name(&self) -> String { + pub fn short_file_name(&self) -> String { let name = str::from_utf8(&self.data.name[0..8]).unwrap().trim_right(); let ext = str::from_utf8(&self.data.name[8..11]).unwrap().trim_right(); if ext == "" { name.to_string() } else { format!("{}.{}", name, ext) } } + pub fn file_name(&self) -> String { + if self.lfn.len() > 0 { + String::from_utf16(&self.lfn).unwrap() + } else { + self.short_file_name() + } + } + pub fn attributes(&self) -> FatFileAttributes { self.data.attrs } @@ -141,20 +170,36 @@ impl FatDir { let mut name = [0; 11]; self.rdr.read(&mut name)?; let attrs = FatFileAttributes::from_bits(self.rdr.read_u8()?).expect("invalid attributes"); - Ok(FatDirEntryData { - name, - attrs, - reserved_0: self.rdr.read_u8()?, - create_time_0: self.rdr.read_u8()?, - create_time_1: self.rdr.read_u16::()?, - create_date: self.rdr.read_u16::()?, - access_date: self.rdr.read_u16::()?, - first_cluster_hi: self.rdr.read_u16::()?, - modify_time: self.rdr.read_u16::()?, - modify_date: self.rdr.read_u16::()?, - first_cluster_lo: self.rdr.read_u16::()?, - size: self.rdr.read_u32::()?, - }) + if attrs == FatFileAttributes::LFN { + let mut data = FatDirLfnEntryData { + attrs, ..Default::default() + }; + let mut cur = Cursor::new(&name); + data.order = cur.read_u8()?; + cur.read_u16_into::(&mut data.name_0)?; + data.entry_type = self.rdr.read_u8()?; + data.checksum = self.rdr.read_u8()?; + self.rdr.read_u16_into::(&mut data.name_1)?; + data.reserved_0 = self.rdr.read_u16::()?; + self.rdr.read_u16_into::(&mut data.name_2)?; + Ok(FatDirEntryData::Lfn(data)) + } else { + let data = FatDirFileEntryData { + name, + attrs, + reserved_0: self.rdr.read_u8()?, + create_time_0: self.rdr.read_u8()?, + create_time_1: self.rdr.read_u16::()?, + create_date: self.rdr.read_u16::()?, + access_date: self.rdr.read_u16::()?, + first_cluster_hi: self.rdr.read_u16::()?, + modify_time: self.rdr.read_u16::()?, + modify_date: self.rdr.read_u16::()?, + first_cluster_lo: self.rdr.read_u16::()?, + size: self.rdr.read_u32::()?, + }; + Ok(FatDirEntryData::File(data)) + } } fn split_path<'a>(path: &'a str) -> (&'a str, Option<&'a str>) { @@ -174,20 +219,20 @@ impl FatDir { Err(io::Error::new(ErrorKind::NotFound, "file not found")) } - pub fn get_dir(&mut self, path: &str) -> io::Result { + pub fn open_dir(&mut self, path: &str) -> io::Result { let (name, rest_opt) = Self::split_path(path); let e = self.find_entry(name)?; match rest_opt { - Some(rest) => e.to_dir().get_dir(rest), + Some(rest) => e.to_dir().open_dir(rest), None => Ok(e.to_dir()) } } - pub fn get_file(&mut self, path: &str) -> io::Result { + pub fn open_file(&mut self, path: &str) -> io::Result { let (name, rest_opt) = Self::split_path(path); let e = self.find_entry(name)?; match rest_opt { - Some(rest) => e.to_dir().get_file(rest), + Some(rest) => e.to_dir().open_file(rest), None => Ok(e.to_file()) } } @@ -197,25 +242,61 @@ impl Iterator for FatDir { type Item = io::Result; fn next(&mut self) -> Option> { + let mut lfn_buf = Vec::::new(); loop { let res = self.read_dir_entry_data(); let data = match res { Ok(data) => data, Err(err) => return Some(Err(err)), }; - if data.name[0] == 0 { - return None; // end of dir - } - if data.name[0] == 0xE5 { - continue; // deleted - } - if data.attrs == FatFileAttributes::LFN { - continue; // FIXME: support LFN - } - return Some(Ok(FatDirEntry { - data, - state: self.state.clone(), - })); + match data { + FatDirEntryData::File(data) => { + // Check if this is end of dif + if data.name[0] == 0 { + return None; + } + // Check if this is deleted or volume ID entry + if data.name[0] == 0xE5 || data.attrs.contains(FatFileAttributes::VOLUME_ID) { + lfn_buf.clear(); + continue; + } + // Truncate 0 and 0xFFFF characters from LFN buffer + let mut lfn_len = lfn_buf.len(); + loop { + if lfn_len == 0 { + break; + } + match lfn_buf[lfn_len-1] { + 0xFFFF | 0 => lfn_len -= 1, + _ => break, + } + } + lfn_buf.truncate(lfn_len); + return Some(Ok(FatDirEntry { + data, + lfn: lfn_buf, + state: self.state.clone(), + })); + }, + FatDirEntryData::Lfn(data) => { + // Check if this is deleted entry + if data.order == 0xE5 { + lfn_buf.clear(); + continue; + } + const LFN_PART_LEN: usize = 13; + let index = (data.order & 0x1F) - 1; + let pos = LFN_PART_LEN * index as usize; + // resize LFN buffer to have enough space for entire name + if lfn_buf.len() < pos + LFN_PART_LEN { + lfn_buf.resize(pos + LFN_PART_LEN, 0); + } + // copy name parts into LFN buffer + lfn_buf[pos+0..pos+5].clone_from_slice(&data.name_0); + lfn_buf[pos+5..pos+11].clone_from_slice(&data.name_1); + lfn_buf[pos+11..pos+13].clone_from_slice(&data.name_2); + } + }; } } } diff --git a/src/fs.rs b/src/fs.rs index 1d3c6ee..0f786ad 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -154,15 +154,15 @@ impl FatFileSystem { }) } - pub fn get_type(&self) -> FatType { + pub fn fat_type(&self) -> FatType { self.state.borrow().fat_type } - pub fn get_volume_id(&self) -> u32 { + pub fn volume_id(&self) -> u32 { self.state.borrow().boot.bpb.volume_id } - pub fn get_volume_label(&self) -> String { + pub fn volume_label(&self) -> String { str::from_utf8(&self.state.borrow().boot.bpb.volume_label).unwrap().trim_right().to_string() } diff --git a/tests/integration-test.rs b/tests/integration-test.rs index da963b2..161b569 100644 --- a/tests/integration-test.rs +++ b/tests/integration-test.rs @@ -5,7 +5,7 @@ use std::io::{BufReader, SeekFrom}; use std::io::prelude::*; use std::str; -use rfat::FatFileSystem; +use rfat::{FatFileSystem, FatType}; const TEST_TEXT: &'static str = "Rust is cool!\n"; const FAT12_IMG: &'static str = "resources/fat12.img"; @@ -21,8 +21,10 @@ fn open_fs(filename: &str) -> FatFileSystem { fn test_root_dir(mut fs: FatFileSystem) { let mut root_dir = fs.root_dir(); let entries = root_dir.list().unwrap(); + let short_names = entries.iter().map(|e| e.short_file_name()).collect::>(); + assert_eq!(short_names, ["LONG.TXT", "SHORT.TXT", "VERY", "VERY-L~1"]); let names = entries.iter().map(|e| e.file_name()).collect::>(); - assert_eq!(names, ["LONG.TXT", "SHORT.TXT", "VERY"]); + assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name"]); // Try read again let entries = root_dir.list().unwrap(); let names2 = entries.iter().map(|e| e.file_name()).collect::>(); @@ -46,7 +48,7 @@ fn test_root_dir_fat32() { fn test_read_seek_short_file(mut fs: FatFileSystem) { let mut root_dir = fs.root_dir(); - let mut short_file = root_dir.get_file("short.txt").unwrap(); + let mut short_file = root_dir.open_file("short.txt").unwrap(); let mut buf = Vec::new(); short_file.read_to_end(&mut buf).unwrap(); assert_eq!(str::from_utf8(&buf).unwrap(), TEST_TEXT); @@ -74,7 +76,7 @@ fn test_read_seek_short_file_fat32() { fn test_read_long_file(mut fs: FatFileSystem) { let mut root_dir = fs.root_dir(); - let mut long_file = root_dir.get_file("long.txt").unwrap(); + let mut long_file = root_dir.open_file("long.txt").unwrap(); let mut buf = Vec::new(); long_file.read_to_end(&mut buf).unwrap(); assert_eq!(str::from_utf8(&buf).unwrap(), TEST_TEXT.repeat(1000)); @@ -103,10 +105,10 @@ fn test_read_long_file_fat32() { fn test_get_dir_by_path(mut fs: FatFileSystem) { let mut root_dir = fs.root_dir(); - let mut dir = root_dir.get_dir("very/long/path/").unwrap(); + let mut dir = root_dir.open_dir("very/long/path/").unwrap(); let entries = dir.list().unwrap(); let names = entries.iter().map(|e| e.file_name()).collect::>(); - assert_eq!(names, [".", "..", "TEST.TXT"]); + assert_eq!(names, [".", "..", "test.txt"]); } #[test] @@ -126,7 +128,12 @@ fn test_get_dir_by_path_fat32() { fn test_get_file_by_path(mut fs: FatFileSystem) { let mut root_dir = fs.root_dir(); - let mut file = root_dir.get_file("very/long/path/test.txt").unwrap(); + let mut file = root_dir.open_file("very/long/path/test.txt").unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + assert_eq!(str::from_utf8(&buf).unwrap(), TEST_TEXT); + + let mut file = root_dir.open_file("very-long-dir-name/very-long-file-name.txt").unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); assert_eq!(str::from_utf8(&buf).unwrap(), TEST_TEXT); @@ -146,3 +153,24 @@ fn test_get_file_by_path_fat16() { fn test_get_file_by_path_fat32() { test_get_file_by_path(open_fs(FAT32_IMG)) } + +fn test_volume_metadata(fs: FatFileSystem, fat_type: FatType) { + assert_eq!(fs.volume_id(), 0x12345678); + assert_eq!(fs.volume_label(), "Test!"); + assert_eq!(fs.fat_type(), fat_type); +} + +#[test] +fn test_volume_metadata_fat12() { + test_volume_metadata(open_fs(FAT12_IMG), FatType::Fat12) +} + +#[test] +fn test_volume_metadata_fat16() { + test_volume_metadata(open_fs(FAT16_IMG), FatType::Fat16) +} + +#[test] +fn test_volume_metadata_fat32() { + test_volume_metadata(open_fs(FAT32_IMG), FatType::Fat32) +}