From 840290f754b33e4daa23d22bb555d461d9d8c334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Harabie=C5=84?= Date: Sun, 24 Sep 2017 22:12:38 +0200 Subject: [PATCH] Add LFN support and rename some functions. --- README.md | 12 ++- examples/cat.rs | 2 +- examples/ls.rs | 2 +- resources/fat12.img | Bin 1024000 -> 1024000 bytes resources/fat16.img | Bin 2560000 -> 2560000 bytes resources/fat32.img | Bin 34816000 -> 34816000 bytes scripts/create-test-img.sh | 7 +- src/dir.rs | 153 ++++++++++++++++++++++++++++--------- src/fs.rs | 6 +- tests/integration-test.rs | 42 ++++++++-- 10 files changed, 172 insertions(+), 52 deletions(-) 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 6ae8a191976bed71b3fd4ceca3d516efb6f1a723..7f051a5da70a9bf866cc6634625d3163fbc97728 100644 GIT binary patch delta 626 zcmZoTVApWKZh|^@MVN_DNNRD3qJqLkBNnF3#!RQUC-IqqX(_0L0uXR8FeI(F@V4-V z(35{jns6u0hYHNMm~1I!io&myGR;Mj_f}v40ViFCJcdMuT!vHzeugRrhW~H?VKbHi zWr`Ro8FWGNISlzg<>^393Q%t*P$JAVC{ov_&JgGz0iZ(=_Is;rJ}7n6VN(pBl{T`l z0?1YdJw1>YzyMX-WXox$yvXVmH#;)5GESPthhmB%Aya0cnxcf&6eU8Y%rfOic8f9t zAJCj;#!XZBnCg)wRPY-Fj3N+lN(BZSB((V%!c!Ua7)lr_fEWZo!HyEv;fSzK1IlMI z#JNRlzPxW^9_%Hixlo4pZA4=C(O3ZF5-L=CHNRVQ-tm(Kd&(Z4Ou4 Q9PYL`JZ*D$+ve~A0Lv()P5=M^ delta 357 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 I7ZV=-FJ=4x3;+NC 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) +}