rust-fatfs/tests/write.rs
Rafał Harabień e4b72836f0 Fix file-system corruption when creating directory entries
Corruption happens if:
* files are created in non-root directory
* directory size becomes greater or equal to cluster size
Creating last directory entry in the directory cluster corrupts the
cluster directly preceding the directory cluster.
For 512 bytes long cluster creating 8th directory entry (counted from 1)
will cause first corruption.
Directory entry that causes the corruption will not be updated correctly.

Fixes #42
2021-01-22 00:22:54 +01:00

385 lines
13 KiB
Rust

extern crate env_logger;
extern crate fatfs;
extern crate fscommon;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::mem;
use std::str;
use fatfs::FsOptions;
use fscommon::BufStream;
const FAT12_IMG: &str = "fat12.img";
const FAT16_IMG: &str = "fat16.img";
const FAT32_IMG: &str = "fat32.img";
const IMG_DIR: &str = "resources";
const TMP_DIR: &str = "tmp";
const TEST_STR: &str = "Hi there Rust programmer!\n";
const TEST_STR2: &str = "Rust is cool!\n";
type FileSystem = fatfs::FileSystem<BufStream<fs::File>>;
fn call_with_tmp_img(f: &Fn(&str) -> (), filename: &str, test_seq: u32) {
let _ = env_logger::try_init();
let img_path = format!("{}/{}", IMG_DIR, filename);
let tmp_path = format!("{}/{}-{}", TMP_DIR, test_seq, filename);
fs::create_dir(TMP_DIR).ok();
fs::copy(&img_path, &tmp_path).unwrap();
f(tmp_path.as_str());
fs::remove_file(tmp_path).unwrap();
}
fn open_filesystem_rw(tmp_path: &str) -> FileSystem {
let file = fs::OpenOptions::new().read(true).write(true).open(&tmp_path).unwrap();
let buf_file = BufStream::new(file);
let options = FsOptions::new().update_accessed_date(true);
FileSystem::new(buf_file, options).unwrap()
}
fn call_with_fs(f: &Fn(FileSystem) -> (), filename: &str, test_seq: u32) {
let callback = |tmp_path: &str| {
let fs = open_filesystem_rw(tmp_path);
f(fs);
};
call_with_tmp_img(&callback, filename, test_seq);
}
fn test_write_short_file(fs: FileSystem) {
let root_dir = fs.root_dir();
let mut file = root_dir.open_file("short.txt").expect("open file");
file.truncate().unwrap();
file.write_all(&TEST_STR.as_bytes()).unwrap();
file.seek(io::SeekFrom::Start(0)).unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(TEST_STR, str::from_utf8(&buf).unwrap());
}
#[test]
fn test_write_file_fat12() {
call_with_fs(&test_write_short_file, FAT12_IMG, 1)
}
#[test]
fn test_write_file_fat16() {
call_with_fs(&test_write_short_file, FAT16_IMG, 1)
}
#[test]
fn test_write_file_fat32() {
call_with_fs(&test_write_short_file, FAT32_IMG, 1)
}
fn test_write_long_file(fs: FileSystem) {
let root_dir = fs.root_dir();
let mut file = root_dir.open_file("long.txt").expect("open file");
file.truncate().unwrap();
let test_str = TEST_STR.repeat(1000);
file.write_all(&test_str.as_bytes()).unwrap();
file.seek(io::SeekFrom::Start(0)).unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(test_str, str::from_utf8(&buf).unwrap());
file.seek(io::SeekFrom::Start(1234)).unwrap();
file.truncate().unwrap();
file.seek(io::SeekFrom::Start(0)).unwrap();
buf.clear();
file.read_to_end(&mut buf).unwrap();
assert_eq!(&test_str[..1234], str::from_utf8(&buf).unwrap());
}
#[test]
fn test_write_long_file_fat12() {
call_with_fs(&test_write_long_file, FAT12_IMG, 2)
}
#[test]
fn test_write_long_file_fat16() {
call_with_fs(&test_write_long_file, FAT16_IMG, 2)
}
#[test]
fn test_write_long_file_fat32() {
call_with_fs(&test_write_long_file, FAT32_IMG, 2)
}
fn test_remove(fs: FileSystem) {
let root_dir = fs.root_dir();
assert!(root_dir.remove("very/long/path").is_err());
let dir = root_dir.open_dir("very/long/path").unwrap();
let mut names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt"]);
root_dir.remove("very/long/path/test.txt").unwrap();
names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", ".."]);
assert!(root_dir.remove("very/long/path").is_ok());
names = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name"]);
root_dir.remove("long.txt").unwrap();
names = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, ["short.txt", "very", "very-long-dir-name"]);
}
#[test]
fn test_remove_fat12() {
call_with_fs(&test_remove, FAT12_IMG, 3)
}
#[test]
fn test_remove_fat16() {
call_with_fs(&test_remove, FAT16_IMG, 3)
}
#[test]
fn test_remove_fat32() {
call_with_fs(&test_remove, FAT32_IMG, 3)
}
fn test_create_file(fs: FileSystem) {
let root_dir = fs.root_dir();
let dir = root_dir.open_dir("very/long/path").unwrap();
let mut names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt"]);
{
// test some invalid names
assert!(root_dir.create_file("very/long/path/:").is_err());
assert!(root_dir.create_file("very/long/path/\0").is_err());
// create file
let mut file = root_dir.create_file("very/long/path/new-file-with-long-name.txt").unwrap();
file.write_all(&TEST_STR.as_bytes()).unwrap();
}
// check for dir entry
names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt", "new-file-with-long-name.txt"]);
names = dir.iter().map(|r| r.unwrap().short_file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "TEST.TXT", "NEW-FI~1.TXT"]);
{
// check contents
let mut file = root_dir.open_file("very/long/path/new-file-with-long-name.txt").unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
assert_eq!(&content, &TEST_STR);
}
// Create enough entries to allocate next cluster
for i in 0..512 / 32 {
let name = format!("test{}", i);
dir.create_file(&name).unwrap();
}
names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names.len(), 4 + 512 / 32);
// check creating existing file opens it
{
let mut file = root_dir.create_file("very/long/path/new-file-with-long-name.txt").unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
assert_eq!(&content, &TEST_STR);
}
// check using create_file with existing directory fails
assert!(root_dir.create_file("very").is_err());
}
#[test]
fn test_create_file_fat12() {
call_with_fs(&test_create_file, FAT12_IMG, 4)
}
#[test]
fn test_create_file_fat16() {
call_with_fs(&test_create_file, FAT16_IMG, 4)
}
#[test]
fn test_create_file_fat32() {
call_with_fs(&test_create_file, FAT32_IMG, 4)
}
fn test_create_dir(fs: FileSystem) {
let root_dir = fs.root_dir();
let parent_dir = root_dir.open_dir("very/long/path").unwrap();
let mut names = parent_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt"]);
{
let subdir = root_dir.create_dir("very/long/path/new-dir-with-long-name").unwrap();
names = subdir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", ".."]);
}
// check if new entry is visible in parent
names = parent_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt", "new-dir-with-long-name"]);
{
// Check if new directory can be opened and read
let subdir = root_dir.open_dir("very/long/path/new-dir-with-long-name").unwrap();
names = subdir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", ".."]);
}
// Check if '.' is alias for new directory
{
let subdir = root_dir.open_dir("very/long/path/new-dir-with-long-name/.").unwrap();
names = subdir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", ".."]);
}
// Check if '..' is alias for parent directory
{
let subdir = root_dir.open_dir("very/long/path/new-dir-with-long-name/..").unwrap();
names = subdir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "test.txt", "new-dir-with-long-name"]);
}
// check if creating existing directory returns it
{
let subdir = root_dir.create_dir("very").unwrap();
names = subdir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", "..", "long"]);
}
// check short names validity after create_dir
{
let subdir = root_dir.create_dir("test").unwrap();
names = subdir.iter().map(|r| r.unwrap().short_file_name()).collect::<Vec<String>>();
assert_eq!(names, [".", ".."]);
}
// check using create_dir with existing file fails
assert!(root_dir.create_dir("very/long/path/test.txt").is_err());
}
#[test]
fn test_create_dir_fat12() {
call_with_fs(&test_create_dir, FAT12_IMG, 5)
}
#[test]
fn test_create_dir_fat16() {
call_with_fs(&test_create_dir, FAT16_IMG, 5)
}
#[test]
fn test_create_dir_fat32() {
call_with_fs(&test_create_dir, FAT32_IMG, 5)
}
fn test_rename_file(fs: FileSystem) {
let root_dir = fs.root_dir();
let parent_dir = root_dir.open_dir("very/long/path").unwrap();
let entries = parent_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
let names = entries.iter().map(|r| r.file_name()).collect::<Vec<_>>();
assert_eq!(names, [".", "..", "test.txt"]);
assert_eq!(entries[2].len(), 14);
let stats = fs.stats().unwrap();
parent_dir.rename("test.txt", &parent_dir, "new-long-name.txt").unwrap();
let entries = parent_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
let names = entries.iter().map(|r| r.file_name()).collect::<Vec<_>>();
assert_eq!(names, [".", "..", "new-long-name.txt"]);
assert_eq!(entries[2].len(), TEST_STR2.len() as u64);
let mut file = parent_dir.open_file("new-long-name.txt").unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(str::from_utf8(&buf).unwrap(), TEST_STR2);
parent_dir.rename("new-long-name.txt", &root_dir, "moved-file.txt").unwrap();
let entries = root_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
let names = entries.iter().map(|r| r.file_name()).collect::<Vec<_>>();
assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name", "moved-file.txt"]);
assert_eq!(entries[4].len(), TEST_STR2.len() as u64);
let mut file = root_dir.open_file("moved-file.txt").unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(str::from_utf8(&buf).unwrap(), TEST_STR2);
assert!(root_dir.rename("moved-file.txt", &root_dir, "short.txt").is_err());
let entries = root_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
let names = entries.iter().map(|r| r.file_name()).collect::<Vec<_>>();
assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name", "moved-file.txt"]);
assert!(root_dir.rename("moved-file.txt", &root_dir, "moved-file.txt").is_ok());
let new_stats = fs.stats().unwrap();
assert_eq!(new_stats.free_clusters(), stats.free_clusters());
}
#[test]
fn test_rename_file_fat12() {
call_with_fs(&test_rename_file, FAT12_IMG, 6)
}
#[test]
fn test_rename_file_fat16() {
call_with_fs(&test_rename_file, FAT16_IMG, 6)
}
#[test]
fn test_rename_file_fat32() {
call_with_fs(&test_rename_file, FAT32_IMG, 6)
}
fn test_dirty_flag(tmp_path: &str) {
// Open filesystem, make change, and forget it - should become dirty
let fs = open_filesystem_rw(tmp_path);
let status_flags = fs.read_status_flags().unwrap();
assert_eq!(status_flags.dirty(), false);
assert_eq!(status_flags.io_error(), false);
fs.root_dir().create_file("abc.txt").unwrap();
mem::forget(fs);
// Check if volume is dirty now
let fs = open_filesystem_rw(tmp_path);
let status_flags = fs.read_status_flags().unwrap();
assert_eq!(status_flags.dirty(), true);
assert_eq!(status_flags.io_error(), false);
fs.unmount().unwrap();
// Make sure remounting does not clear the dirty flag
let fs = open_filesystem_rw(tmp_path);
let status_flags = fs.read_status_flags().unwrap();
assert_eq!(status_flags.dirty(), true);
assert_eq!(status_flags.io_error(), false);
}
#[test]
fn test_dirty_flag_fat12() {
call_with_tmp_img(&test_dirty_flag, FAT12_IMG, 7)
}
#[test]
fn test_dirty_flag_fat16() {
call_with_tmp_img(&test_dirty_flag, FAT16_IMG, 7)
}
#[test]
fn test_dirty_flag_fat32() {
call_with_tmp_img(&test_dirty_flag, FAT32_IMG, 7)
}
fn test_multiple_files_in_directory(fs: FileSystem) {
let dir = fs.root_dir().create_dir("/TMP").unwrap();
for i in 0..8 {
let name = format!("T{}.TXT", i);
let mut file = dir.create_file(&name).unwrap();
file.write_all(TEST_STR.as_bytes()).unwrap();
file.flush().unwrap();
let file = dir.iter()
.map(|r| r.unwrap())
.filter(|e| e.file_name() == name)
.next()
.unwrap();
assert_eq!(TEST_STR.len() as u64, file.len(), "Wrong file len on iteration {}", i);
}
}
#[test]
fn test_multiple_files_in_directory_fat12() {
call_with_fs(&test_multiple_files_in_directory, FAT12_IMG, 8)
}
#[test]
fn test_multiple_files_in_directory_fat16() {
call_with_fs(&test_multiple_files_in_directory, FAT16_IMG, 8)
}
#[test]
fn test_multiple_files_in_directory_fat32() {
call_with_fs(&test_multiple_files_in_directory, FAT32_IMG, 8)
}