Initial
This commit is contained in:
commit
b2dde724ca
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"sha1": "954c68457fda6f1b9b87393f3130f74da5f548f8"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
max_line_length = 120
|
|
@ -0,0 +1,3 @@
|
||||||
|
target
|
||||||
|
tmp
|
||||||
|
Cargo.lock
|
|
@ -0,0 +1,35 @@
|
||||||
|
language: rust
|
||||||
|
env:
|
||||||
|
- RUST_LOG=warn RUST_BACKTRACE=1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Minimal supported rustc version
|
||||||
|
- rust: 1.24.0
|
||||||
|
script:
|
||||||
|
# Build only the library (examples may fail)
|
||||||
|
- cargo update
|
||||||
|
- cargo update -p byteorder --precise 1.3.4
|
||||||
|
- cargo update -p cfg-if --precise 0.1.9
|
||||||
|
- cargo update -p log --precise 0.4.8
|
||||||
|
- cargo build --verbose
|
||||||
|
- rust: stable
|
||||||
|
- rust: beta
|
||||||
|
- rust: nightly
|
||||||
|
- rust: nightly-2018-03-07
|
||||||
|
script:
|
||||||
|
# nostd build
|
||||||
|
# byteorder crate version is configured here to fix build in old nightly compiler
|
||||||
|
# See: https://github.com/BurntSushi/byteorder/pull/150
|
||||||
|
- cargo update
|
||||||
|
- cargo update -p byteorder --precise 1.2.7
|
||||||
|
- cargo update -p cfg-if --precise 0.1.9
|
||||||
|
- cargo update -p log --precise 0.4.8
|
||||||
|
- cargo build --verbose --no-default-features --features core_io
|
||||||
|
- cargo build --verbose --no-default-features --features core_io,alloc
|
||||||
|
- rust: nightly-2019-07-01
|
||||||
|
script:
|
||||||
|
- cargo build --verbose --no-default-features --features core_io
|
||||||
|
- cargo build --verbose --no-default-features --features core_io,alloc
|
||||||
|
|
||||||
|
allow_failures:
|
||||||
|
- rust: nightly
|
|
@ -0,0 +1,61 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
0.3.5 (2021-01-23)
|
||||||
|
------------------------
|
||||||
|
Bug fixes:
|
||||||
|
* Fix file-system corruption that occurs when creating multiple directory entries in a non-root directory (directory
|
||||||
|
size must be greater or equal to the cluster size for the corruption to happen)
|
||||||
|
|
||||||
|
0.3.4 (2020-07-20)
|
||||||
|
------------------
|
||||||
|
Bug fixes:
|
||||||
|
* Fix time encoding and decoding in directory entries
|
||||||
|
|
||||||
|
0.3.3 (2019-11-10)
|
||||||
|
------------------
|
||||||
|
Bug fixes:
|
||||||
|
* Add missing characters to the whitelist for long file name (`^`, `#`, `&`)
|
||||||
|
* Fix invalid short file names for `.` and `..` entries when creating a new directory
|
||||||
|
* Fix `no_std` build
|
||||||
|
|
||||||
|
Misc changes:
|
||||||
|
* Fix compiler warnings
|
||||||
|
* Improve documentation
|
||||||
|
|
||||||
|
0.3.2 (2018-12-29)
|
||||||
|
------------------
|
||||||
|
New features:
|
||||||
|
* Add `format_volume` function for initializing a FAT filesystem on a partition
|
||||||
|
* Add more checks of filesystem correctness when mounting
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
* Clear directory returned from `create_dir` method - upgrade ASAP if this method is used
|
||||||
|
* Fix handling of FSInfo sector on FAT32 volumes with sector size different than 512 - upgrade ASAP if such sector size is used
|
||||||
|
* Use `write_all` in `serialize` method for FSInfo sector - previously it could have been improperly updated
|
||||||
|
|
||||||
|
0.3.1 (2018-10-20)
|
||||||
|
------------------
|
||||||
|
New features:
|
||||||
|
* Increased file creation time resolution from 2s to 1/100s
|
||||||
|
* Added oem_cp_converter filesystem option allowing to provide custom short name decoder
|
||||||
|
* Added time_provider filesystem option allowing to provide time used when modifying directory entries
|
||||||
|
* Added marking volume as dirty on first write and not-dirty on unmount
|
||||||
|
* Added support for reading volume label from root directory
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
* Fixed handling of short names with spaces in the middle - all characters after first space in 8.3 components were
|
||||||
|
stripped before
|
||||||
|
* Fixed decoding 0xE5 character in first byte of short name - if first character of short name is equal to 0xE5,
|
||||||
|
it was read as 0x05
|
||||||
|
* Preserve 4 most significant bits in FAT32 entries - it is required by FAT specification, but previous behavior
|
||||||
|
should not cause any compatibility problem because all known implementations ignore those bits
|
||||||
|
* Fixed warnings for file entries without LFN entries - they were handled properly, but caused warnings in run-time
|
||||||
|
|
||||||
|
Misc changes:
|
||||||
|
* Deprecated set_created. set_accessed, set_modified methods on File - those fields are updated automatically using
|
||||||
|
information provided by TimeProvider
|
||||||
|
* Fixed size formatting in ls.rs example
|
||||||
|
* Added more filesystem checks causing errors or warnings when incompatibility is detected
|
||||||
|
* Removed unnecessary clone() calls
|
||||||
|
* Code formatting and docs fixes
|
|
@ -0,0 +1,52 @@
|
||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies
|
||||||
|
#
|
||||||
|
# If you believe there's an error in this file please file an
|
||||||
|
# issue against the rust-lang/cargo repository. If you're
|
||||||
|
# editing this file be aware that the upstream Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable)
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "fatfs"
|
||||||
|
version = "0.3.5"
|
||||||
|
authors = ["Rafał Harabień <rafalh92@outlook.com>"]
|
||||||
|
exclude = ["resources/*"]
|
||||||
|
description = "FAT filesystem library.\n"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["fat", "filesystem", "no_std"]
|
||||||
|
categories = ["filesystem"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/rafalh/rust-fatfs"
|
||||||
|
[dependencies.bitflags]
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
[dependencies.byteorder]
|
||||||
|
version = "1"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.core_io]
|
||||||
|
version = "0.1"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4"
|
||||||
|
[dev-dependencies.env_logger]
|
||||||
|
version = "0.5"
|
||||||
|
|
||||||
|
[dev-dependencies.fscommon]
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
alloc = []
|
||||||
|
default = ["chrono", "std", "alloc"]
|
||||||
|
std = ["byteorder/std"]
|
||||||
|
[badges.travis-ci]
|
||||||
|
repository = "rafalh/rust-fatfs"
|
|
@ -0,0 +1,37 @@
|
||||||
|
[package]
|
||||||
|
name = "fatfs"
|
||||||
|
version = "0.3.5"
|
||||||
|
authors = ["Rafał Harabień <rafalh92@outlook.com>"]
|
||||||
|
repository = "https://github.com/rafalh/rust-fatfs"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["fat", "filesystem", "no_std"]
|
||||||
|
categories = ["filesystem"]
|
||||||
|
license = "MIT"
|
||||||
|
description = """
|
||||||
|
FAT filesystem library.
|
||||||
|
"""
|
||||||
|
exclude = [
|
||||||
|
"resources/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
travis-ci = { repository = "rafalh/rust-fatfs" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Use Rust std library
|
||||||
|
std = ["byteorder/std"]
|
||||||
|
# Use dynamic allocation - required for LFN support. When used without std please enable core_io/collections
|
||||||
|
alloc = []
|
||||||
|
# Default features
|
||||||
|
default = ["chrono", "std", "alloc"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = { version = "1", default-features = false }
|
||||||
|
bitflags = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
chrono = { version = "0.4", optional = true }
|
||||||
|
core_io = { version = "0.1", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.5"
|
||||||
|
fscommon = "0.1"
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright 2017 Rafał Harabień
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,73 @@
|
||||||
|
Rust FAT FS
|
||||||
|
===========
|
||||||
|
|
||||||
|
[![Travis Build Status](https://travis-ci.org/rafalh/rust-fatfs.svg?branch=master)](https://travis-ci.org/rafalh/rust-fatfs)
|
||||||
|
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.txt)
|
||||||
|
[![crates.io](http://meritbadge.herokuapp.com/fatfs)](https://crates.io/crates/fatfs)
|
||||||
|
[![Documentation](https://docs.rs/fatfs/badge.svg)](https://docs.rs/fatfs)
|
||||||
|
[![Minimum rustc version](https://img.shields.io/badge/rustc-1.24+-yellow.svg)](https://blog.rust-lang.org/2018/02/15/Rust-1.24.html)
|
||||||
|
|
||||||
|
A FAT filesystem library implemented in Rust.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
* read/write file using standard Read/Write traits
|
||||||
|
* read directory contents
|
||||||
|
* create/remove file or directory
|
||||||
|
* rename/move file or directory
|
||||||
|
* read/write file timestamps (updated automatically if `chrono` feature is enabled)
|
||||||
|
* format volume
|
||||||
|
* FAT12, FAT16, FAT32 compatibility
|
||||||
|
* LFN (Long File Names) extension is supported
|
||||||
|
* Basic no_std environment support
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fatfs = "0.3"
|
||||||
|
|
||||||
|
and this to your crate root:
|
||||||
|
|
||||||
|
extern crate fatfs;
|
||||||
|
|
||||||
|
You can start using the `fatfs` library now:
|
||||||
|
|
||||||
|
let img_file = File::open("fat.img")?;
|
||||||
|
let fs = fatfs::FileSystem::new(img_file, fatfs::FsOptions::new())?;
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let mut file = root_dir.create_file("hello.txt")?;
|
||||||
|
file.write_all(b"Hello World!")?;
|
||||||
|
|
||||||
|
Note: it is recommended to wrap the underlying file struct in a buffering/caching object like `BufStream` from `fscommon` crate. For example:
|
||||||
|
|
||||||
|
extern crate fscommon;
|
||||||
|
let buf_stream = BufStream::new(img_file);
|
||||||
|
let fs = fatfs::FileSystem::new(buf_stream, fatfs::FsOptions::new())?;
|
||||||
|
|
||||||
|
See more examples in the `examples` subdirectory.
|
||||||
|
|
||||||
|
no_std usage
|
||||||
|
------------
|
||||||
|
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fatfs = { version = "0.3", features = ["core_io"], default-features = false }
|
||||||
|
|
||||||
|
For building in `no_std` mode a Rust compiler version compatible with `core_io` crate is required.
|
||||||
|
|
||||||
|
For now `core_io` supports only nightly Rust channel. See a date suffix in latest `core_io` crate version for exact
|
||||||
|
compiler version.
|
||||||
|
|
||||||
|
Additional features:
|
||||||
|
|
||||||
|
* `alloc` - use `alloc` crate for dynamic allocation. Required for LFN (long file name) support and API which uses
|
||||||
|
`String` type. You may have to provide a memory allocator implementation.
|
||||||
|
|
||||||
|
Note: above feature is enabled by default.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
The MIT license. See `LICENSE.txt`.
|
|
@ -0,0 +1,4 @@
|
||||||
|
TODO
|
||||||
|
====
|
||||||
|
* add method for getting `DirEntry` from a path (possible names: metadata, lookup)
|
||||||
|
* do not create LFN entries if the name fits in a SFN entry
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cargo build --no-default-features --features core_io
|
||||||
|
cargo build --no-default-features --features core_io,alloc,core_io/collections
|
|
@ -0,0 +1,21 @@
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
|
use fatfs::{FileSystem, FsOptions};
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let file = File::open("resources/fat32.img")?;
|
||||||
|
let buf_rdr = BufStream::new(file);
|
||||||
|
let fs = FileSystem::new(buf_rdr, FsOptions::new())?;
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let mut file = root_dir.open_file(&env::args().nth(1).expect("filename expected"))?;
|
||||||
|
let mut buf = vec![];
|
||||||
|
file.read_to_end(&mut buf)?;
|
||||||
|
print!("{}", String::from_utf8_lossy(&buf));
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
use fatfs::{FileSystem, FsOptions};
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
fn format_file_size(size: u64) -> String {
|
||||||
|
const KB: u64 = 1024;
|
||||||
|
const MB: u64 = 1024 * KB;
|
||||||
|
const GB: u64 = 1024 * MB;
|
||||||
|
if size < KB {
|
||||||
|
format!("{}B", size)
|
||||||
|
} else if size < MB {
|
||||||
|
format!("{}KB", size / KB)
|
||||||
|
} else if size < GB {
|
||||||
|
format!("{}MB", size / MB)
|
||||||
|
} else {
|
||||||
|
format!("{}GB", size / GB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let file = File::open("resources/fat32.img")?;
|
||||||
|
let buf_rdr = BufStream::new(file);
|
||||||
|
let fs = FileSystem::new(buf_rdr, FsOptions::new())?;
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let dir = match env::args().nth(1) {
|
||||||
|
None => root_dir,
|
||||||
|
Some(ref path) if path == "." => root_dir,
|
||||||
|
Some(ref path) => root_dir.open_dir(&path)?,
|
||||||
|
};
|
||||||
|
for r in dir.iter() {
|
||||||
|
let e = r?;
|
||||||
|
let modified = DateTime::<Local>::from(e.modified()).format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
println!("{:4} {} {}", format_file_size(e.len()), modified, e.file_name());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let filename = env::args().nth(1).expect("image path expected");
|
||||||
|
let file = fs::OpenOptions::new().read(true).write(true).open(&filename)?;
|
||||||
|
let buf_file = BufStream::new(file);
|
||||||
|
fatfs::format_volume(buf_file, fatfs::FormatVolumeOptions::new())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::{fs, io};
|
||||||
|
|
||||||
|
use fatfs::{FileSystem, FsOptions};
|
||||||
|
use fscommon::{BufStream, StreamSlice};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
// Open disk image
|
||||||
|
let file = fs::File::open("resources/fat32.img")?;
|
||||||
|
// Provide sample partition localization. In real application it should be read from MBR/GPT.
|
||||||
|
let first_lba = 0;
|
||||||
|
let last_lba = 10000;
|
||||||
|
// Create partition using provided start address and size in bytes
|
||||||
|
let partition = StreamSlice::new(file, first_lba, last_lba + 1)?;
|
||||||
|
// Create buffered stream to optimize file access
|
||||||
|
let buf_rdr = BufStream::new(partition);
|
||||||
|
// Finally initialize filesystem struct using provided partition
|
||||||
|
let fs = FileSystem::new(buf_rdr, FsOptions::new())?;
|
||||||
|
// Read and display volume label
|
||||||
|
println!("Volume Label: {}", fs.volume_label());
|
||||||
|
// other operations...
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::{self, prelude::*};
|
||||||
|
|
||||||
|
use fatfs::{FileSystem, FsOptions};
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let img_file = match OpenOptions::new().read(true).write(true).open("fat.img") {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(err) => {
|
||||||
|
println!("Failed to open image!");
|
||||||
|
return Err(err);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let buf_stream = BufStream::new(img_file);
|
||||||
|
let options = FsOptions::new().update_accessed_date(true);
|
||||||
|
let fs = FileSystem::new(buf_stream, options)?;
|
||||||
|
let mut file = fs.root_dir().create_file("hello.txt")?;
|
||||||
|
file.write_all(b"Hello World!")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
max_width = 120
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
ignore = [
|
||||||
|
"src/byteorder_core_io.rs",
|
||||||
|
]
|
||||||
|
unstable_features = true
|
||||||
|
use_small_heuristics = "Max"
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
OUT_DIR=../resources
|
||||||
|
set -e
|
||||||
|
|
||||||
|
create_test_img() {
|
||||||
|
local name=$1
|
||||||
|
local blkcount=$2
|
||||||
|
local fatSize=$3
|
||||||
|
dd if=/dev/zero of="$name" bs=1024 count=$blkcount
|
||||||
|
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 $(seq 1 1000); do
|
||||||
|
echo "Rust is cool!" >>"mnt/long.txt"
|
||||||
|
done
|
||||||
|
echo "Rust is cool!" >>"mnt/short.txt"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
create_test_img "$OUT_DIR/fat12.img" 1000 12
|
||||||
|
create_test_img "$OUT_DIR/fat16.img" 2500 16
|
||||||
|
create_test_img "$OUT_DIR/fat32.img" 34000 32
|
|
@ -0,0 +1,884 @@
|
||||||
|
use core::cmp;
|
||||||
|
use core::u16;
|
||||||
|
use core::u8;
|
||||||
|
use io;
|
||||||
|
use io::prelude::*;
|
||||||
|
use io::{Error, ErrorKind};
|
||||||
|
|
||||||
|
use byteorder::LittleEndian;
|
||||||
|
use byteorder_ext::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use dir_entry::DIR_ENTRY_SIZE;
|
||||||
|
use fs::{FatType, FormatVolumeOptions, FsStatusFlags};
|
||||||
|
use table::RESERVED_FAT_ENTRIES;
|
||||||
|
|
||||||
|
const BITS_PER_BYTE: u32 = 8;
|
||||||
|
const KB: u64 = 1024;
|
||||||
|
const MB: u64 = KB * 1024;
|
||||||
|
const GB: u64 = MB * 1024;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub(crate) struct BiosParameterBlock {
|
||||||
|
pub(crate) bytes_per_sector: u16,
|
||||||
|
pub(crate) sectors_per_cluster: u8,
|
||||||
|
pub(crate) reserved_sectors: u16,
|
||||||
|
pub(crate) fats: u8,
|
||||||
|
pub(crate) root_entries: u16,
|
||||||
|
pub(crate) total_sectors_16: u16,
|
||||||
|
pub(crate) media: u8,
|
||||||
|
pub(crate) sectors_per_fat_16: u16,
|
||||||
|
pub(crate) sectors_per_track: u16,
|
||||||
|
pub(crate) heads: u16,
|
||||||
|
pub(crate) hidden_sectors: u32,
|
||||||
|
pub(crate) total_sectors_32: u32,
|
||||||
|
|
||||||
|
// Extended BIOS Parameter Block
|
||||||
|
pub(crate) sectors_per_fat_32: u32,
|
||||||
|
pub(crate) extended_flags: u16,
|
||||||
|
pub(crate) fs_version: u16,
|
||||||
|
pub(crate) root_dir_first_cluster: u32,
|
||||||
|
pub(crate) fs_info_sector: u16,
|
||||||
|
pub(crate) backup_boot_sector: u16,
|
||||||
|
pub(crate) reserved_0: [u8; 12],
|
||||||
|
pub(crate) drive_num: u8,
|
||||||
|
pub(crate) reserved_1: u8,
|
||||||
|
pub(crate) ext_sig: u8,
|
||||||
|
pub(crate) volume_id: u32,
|
||||||
|
pub(crate) volume_label: [u8; 11],
|
||||||
|
pub(crate) fs_type_label: [u8; 8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BiosParameterBlock {
|
||||||
|
fn deserialize<T: Read>(rdr: &mut T) -> io::Result<BiosParameterBlock> {
|
||||||
|
let mut bpb: BiosParameterBlock = Default::default();
|
||||||
|
bpb.bytes_per_sector = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.sectors_per_cluster = rdr.read_u8()?;
|
||||||
|
bpb.reserved_sectors = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.fats = rdr.read_u8()?;
|
||||||
|
bpb.root_entries = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.total_sectors_16 = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.media = rdr.read_u8()?;
|
||||||
|
bpb.sectors_per_fat_16 = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.sectors_per_track = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.heads = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.hidden_sectors = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
bpb.total_sectors_32 = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
if bpb.is_fat32() {
|
||||||
|
bpb.sectors_per_fat_32 = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
bpb.extended_flags = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.fs_version = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.root_dir_first_cluster = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
bpb.fs_info_sector = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
bpb.backup_boot_sector = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
rdr.read_exact(&mut bpb.reserved_0)?;
|
||||||
|
bpb.drive_num = rdr.read_u8()?;
|
||||||
|
bpb.reserved_1 = rdr.read_u8()?;
|
||||||
|
bpb.ext_sig = rdr.read_u8()?; // 0x29
|
||||||
|
bpb.volume_id = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
rdr.read_exact(&mut bpb.volume_label)?;
|
||||||
|
rdr.read_exact(&mut bpb.fs_type_label)?;
|
||||||
|
} else {
|
||||||
|
bpb.drive_num = rdr.read_u8()?;
|
||||||
|
bpb.reserved_1 = rdr.read_u8()?;
|
||||||
|
bpb.ext_sig = rdr.read_u8()?; // 0x29
|
||||||
|
bpb.volume_id = rdr.read_u32::<LittleEndian>()?;
|
||||||
|
rdr.read_exact(&mut bpb.volume_label)?;
|
||||||
|
rdr.read_exact(&mut bpb.fs_type_label)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the extended boot signature is anything other than 0x29, the fields are invalid
|
||||||
|
if bpb.ext_sig != 0x29 {
|
||||||
|
// fields after ext_sig are not used - clean them
|
||||||
|
bpb.volume_id = 0;
|
||||||
|
bpb.volume_label = [0; 11];
|
||||||
|
bpb.fs_type_label = [0; 8];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bpb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize<T: Write>(&self, mut wrt: T) -> io::Result<()> {
|
||||||
|
wrt.write_u16::<LittleEndian>(self.bytes_per_sector)?;
|
||||||
|
wrt.write_u8(self.sectors_per_cluster)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.reserved_sectors)?;
|
||||||
|
wrt.write_u8(self.fats)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.root_entries)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.total_sectors_16)?;
|
||||||
|
wrt.write_u8(self.media)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.sectors_per_fat_16)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.sectors_per_track)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.heads)?;
|
||||||
|
wrt.write_u32::<LittleEndian>(self.hidden_sectors)?;
|
||||||
|
wrt.write_u32::<LittleEndian>(self.total_sectors_32)?;
|
||||||
|
|
||||||
|
if self.is_fat32() {
|
||||||
|
wrt.write_u32::<LittleEndian>(self.sectors_per_fat_32)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.extended_flags)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.fs_version)?;
|
||||||
|
wrt.write_u32::<LittleEndian>(self.root_dir_first_cluster)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.fs_info_sector)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.backup_boot_sector)?;
|
||||||
|
wrt.write_all(&self.reserved_0)?;
|
||||||
|
wrt.write_u8(self.drive_num)?;
|
||||||
|
wrt.write_u8(self.reserved_1)?;
|
||||||
|
wrt.write_u8(self.ext_sig)?; // 0x29
|
||||||
|
wrt.write_u32::<LittleEndian>(self.volume_id)?;
|
||||||
|
wrt.write_all(&self.volume_label)?;
|
||||||
|
wrt.write_all(&self.fs_type_label)?;
|
||||||
|
} else {
|
||||||
|
wrt.write_u8(self.drive_num)?;
|
||||||
|
wrt.write_u8(self.reserved_1)?;
|
||||||
|
wrt.write_u8(self.ext_sig)?; // 0x29
|
||||||
|
wrt.write_u32::<LittleEndian>(self.volume_id)?;
|
||||||
|
wrt.write_all(&self.volume_label)?;
|
||||||
|
wrt.write_all(&self.fs_type_label)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> io::Result<()> {
|
||||||
|
// sanity checks
|
||||||
|
if self.bytes_per_sector.count_ones() != 1 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid bytes_per_sector value in BPB (not power of two)"));
|
||||||
|
} else if self.bytes_per_sector < 512 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid bytes_per_sector value in BPB (value < 512)"));
|
||||||
|
} else if self.bytes_per_sector > 4096 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid bytes_per_sector value in BPB (value > 4096)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sectors_per_cluster.count_ones() != 1 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid sectors_per_cluster value in BPB (not power of two)"));
|
||||||
|
} else if self.sectors_per_cluster < 1 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid sectors_per_cluster value in BPB (value < 1)"));
|
||||||
|
} else if self.sectors_per_cluster > 128 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid sectors_per_cluster value in BPB (value > 128)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes per sector is u16, sectors per cluster is u8, so guaranteed no overflow in multiplication
|
||||||
|
let bytes_per_cluster = self.bytes_per_sector as u32 * self.sectors_per_cluster as u32;
|
||||||
|
let maximum_compatibility_bytes_per_cluster: u32 = 32 * 1024;
|
||||||
|
|
||||||
|
if bytes_per_cluster > maximum_compatibility_bytes_per_cluster {
|
||||||
|
// 32k is the largest value to maintain greatest compatibility
|
||||||
|
// Many implementations appear to support 64k per cluster, and some may support 128k or larger
|
||||||
|
// However, >32k is not as thoroughly tested...
|
||||||
|
warn!("fs compatibility: bytes_per_cluster value '{}' in BPB exceeds '{}', and thus may be incompatible with some implementations",
|
||||||
|
bytes_per_cluster, maximum_compatibility_bytes_per_cluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_fat32 = self.is_fat32();
|
||||||
|
if self.reserved_sectors < 1 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid reserved_sectors value in BPB"));
|
||||||
|
} else if !is_fat32 && self.reserved_sectors != 1 {
|
||||||
|
// Microsoft document indicates fat12 and fat16 code exists that presume this value is 1
|
||||||
|
warn!(
|
||||||
|
"fs compatibility: reserved_sectors value '{}' in BPB is not '1', and thus is incompatible with some implementations",
|
||||||
|
self.reserved_sectors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.fats == 0 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid fats value in BPB"));
|
||||||
|
} else if self.fats > 2 {
|
||||||
|
// Microsoft document indicates that few implementations support any values other than 1 or 2
|
||||||
|
warn!(
|
||||||
|
"fs compatibility: numbers of FATs '{}' in BPB is greater than '2', and thus is incompatible with some implementations",
|
||||||
|
self.fats
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_fat32 && self.root_entries != 0 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid root_entries value in BPB (should be zero for FAT32)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_fat32 && self.root_entries == 0 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Empty root directory region defined in FAT12/FAT16 BPB"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u32::from(self.root_entries) * DIR_ENTRY_SIZE as u32) % u32::from(self.bytes_per_sector) != 0 {
|
||||||
|
warn!("Root entries should fill sectors fully");
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_fat32 && self.total_sectors_16 != 0 {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Invalid total_sectors_16 value in BPB (should be zero for FAT32)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.total_sectors_16 == 0) == (self.total_sectors_32 == 0) {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Invalid BPB (total_sectors_16 or total_sectors_32 should be non-zero)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_fat32 && self.sectors_per_fat_32 == 0 {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Invalid sectors_per_fat_32 value in BPB (should be non-zero for FAT32)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.fs_version != 0 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Unknown FS version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.total_sectors() <= self.first_data_sector() {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid BPB (total_sectors field value is too small)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_fat32 && self.backup_boot_sector() >= self.reserved_sectors() {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid BPB (backup boot-sector not in a reserved region)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_fat32 && self.fs_info_sector() >= self.reserved_sectors() {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid BPB (FSInfo sector not in a reserved region)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_clusters = self.total_clusters();
|
||||||
|
let fat_type = FatType::from_clusters(total_clusters);
|
||||||
|
if is_fat32 != (fat_type == FatType::Fat32) {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Invalid BPB (result of FAT32 determination from total number of clusters and sectors_per_fat_16 field differs)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if fat_type == FatType::Fat32 && total_clusters > 0x0FFF_FFFF {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid BPB (too many clusters)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bits_per_fat_entry = fat_type.bits_per_fat_entry();
|
||||||
|
let total_fat_entries = self.sectors_per_fat() * self.bytes_per_sector as u32 * 8 / bits_per_fat_entry as u32;
|
||||||
|
if total_fat_entries - RESERVED_FAT_ENTRIES < total_clusters {
|
||||||
|
warn!("FAT is too small compared to total number of clusters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mirroring_enabled(&self) -> bool {
|
||||||
|
self.extended_flags & 0x80 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_fat(&self) -> u16 {
|
||||||
|
// The zero-based number of the active FAT is only valid if mirroring is disabled.
|
||||||
|
if self.mirroring_enabled() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.extended_flags & 0x0F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn status_flags(&self) -> FsStatusFlags {
|
||||||
|
FsStatusFlags::decode(self.reserved_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_fat32(&self) -> bool {
|
||||||
|
// because this field must be zero on FAT32, and
|
||||||
|
// because it must be non-zero on FAT12/FAT16,
|
||||||
|
// this provides a simple way to detect FAT32
|
||||||
|
self.sectors_per_fat_16 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sectors_per_fat(&self) -> u32 {
|
||||||
|
if self.is_fat32() {
|
||||||
|
self.sectors_per_fat_32
|
||||||
|
} else {
|
||||||
|
self.sectors_per_fat_16 as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn total_sectors(&self) -> u32 {
|
||||||
|
if self.total_sectors_16 == 0 {
|
||||||
|
self.total_sectors_32
|
||||||
|
} else {
|
||||||
|
self.total_sectors_16 as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reserved_sectors(&self) -> u32 {
|
||||||
|
self.reserved_sectors as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn root_dir_sectors(&self) -> u32 {
|
||||||
|
let root_dir_bytes = self.root_entries as u32 * DIR_ENTRY_SIZE as u32;
|
||||||
|
(root_dir_bytes + self.bytes_per_sector as u32 - 1) / self.bytes_per_sector as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sectors_per_all_fats(&self) -> u32 {
|
||||||
|
self.fats as u32 * self.sectors_per_fat()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_data_sector(&self) -> u32 {
|
||||||
|
let root_dir_sectors = self.root_dir_sectors();
|
||||||
|
let fat_sectors = self.sectors_per_all_fats();
|
||||||
|
self.reserved_sectors() + fat_sectors + root_dir_sectors
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn total_clusters(&self) -> u32 {
|
||||||
|
let total_sectors = self.total_sectors();
|
||||||
|
let first_data_sector = self.first_data_sector();
|
||||||
|
let data_sectors = total_sectors - first_data_sector;
|
||||||
|
data_sectors / self.sectors_per_cluster as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bytes_from_sectors(&self, sectors: u32) -> u64 {
|
||||||
|
// Note: total number of sectors is a 32 bit number so offsets have to be 64 bit
|
||||||
|
(sectors as u64) * self.bytes_per_sector as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sectors_from_clusters(&self, clusters: u32) -> u32 {
|
||||||
|
// Note: total number of sectors is a 32 bit number so it should not overflow
|
||||||
|
clusters * (self.sectors_per_cluster as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cluster_size(&self) -> u32 {
|
||||||
|
self.sectors_per_cluster as u32 * self.bytes_per_sector as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clusters_from_bytes(&self, bytes: u64) -> u32 {
|
||||||
|
let cluster_size = self.cluster_size() as i64;
|
||||||
|
((bytes as i64 + cluster_size - 1) / cluster_size) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fs_info_sector(&self) -> u32 {
|
||||||
|
self.fs_info_sector as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn backup_boot_sector(&self) -> u32 {
|
||||||
|
self.backup_boot_sector as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct BootSector {
|
||||||
|
bootjmp: [u8; 3],
|
||||||
|
oem_name: [u8; 8],
|
||||||
|
pub(crate) bpb: BiosParameterBlock,
|
||||||
|
boot_code: [u8; 448],
|
||||||
|
boot_sig: [u8; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootSector {
|
||||||
|
pub(crate) fn deserialize<T: Read>(rdr: &mut T) -> io::Result<BootSector> {
|
||||||
|
let mut boot: BootSector = Default::default();
|
||||||
|
rdr.read_exact(&mut boot.bootjmp)?;
|
||||||
|
rdr.read_exact(&mut boot.oem_name)?;
|
||||||
|
boot.bpb = BiosParameterBlock::deserialize(rdr)?;
|
||||||
|
|
||||||
|
if boot.bpb.is_fat32() {
|
||||||
|
rdr.read_exact(&mut boot.boot_code[0..420])?;
|
||||||
|
} else {
|
||||||
|
rdr.read_exact(&mut boot.boot_code[0..448])?;
|
||||||
|
}
|
||||||
|
rdr.read_exact(&mut boot.boot_sig)?;
|
||||||
|
Ok(boot)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serialize<T: Write>(&self, mut wrt: T) -> io::Result<()> {
|
||||||
|
wrt.write_all(&self.bootjmp)?;
|
||||||
|
wrt.write_all(&self.oem_name)?;
|
||||||
|
self.bpb.serialize(&mut wrt)?;
|
||||||
|
|
||||||
|
if self.bpb.is_fat32() {
|
||||||
|
wrt.write_all(&self.boot_code[0..420])?;
|
||||||
|
} else {
|
||||||
|
wrt.write_all(&self.boot_code[0..448])?;
|
||||||
|
}
|
||||||
|
wrt.write_all(&self.boot_sig)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate(&self) -> io::Result<()> {
|
||||||
|
if self.boot_sig != [0x55, 0xAA] {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid boot sector signature"));
|
||||||
|
}
|
||||||
|
if self.bootjmp[0] != 0xEB && self.bootjmp[0] != 0xE9 {
|
||||||
|
warn!("Unknown opcode {:x} in bootjmp boot sector field", self.bootjmp[0]);
|
||||||
|
}
|
||||||
|
self.bpb.validate()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BootSector {
|
||||||
|
fn default() -> BootSector {
|
||||||
|
BootSector {
|
||||||
|
bootjmp: Default::default(),
|
||||||
|
oem_name: Default::default(),
|
||||||
|
bpb: Default::default(),
|
||||||
|
boot_code: [0; 448],
|
||||||
|
boot_sig: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn estimate_fat_type(total_bytes: u64) -> FatType {
|
||||||
|
// Used only to select cluster size if FAT type has not been overriden in options
|
||||||
|
if total_bytes < 4 * MB {
|
||||||
|
FatType::Fat12
|
||||||
|
} else if total_bytes < 512 * MB {
|
||||||
|
FatType::Fat16
|
||||||
|
} else {
|
||||||
|
FatType::Fat32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_bytes_per_cluster(total_bytes: u64, bytes_per_sector: u16, fat_type: Option<FatType>) -> u32 {
|
||||||
|
let fat_type = fat_type.unwrap_or_else(|| estimate_fat_type(total_bytes));
|
||||||
|
let bytes_per_cluster = match fat_type {
|
||||||
|
FatType::Fat12 => (total_bytes.next_power_of_two() / MB * 512) as u32,
|
||||||
|
FatType::Fat16 => {
|
||||||
|
if total_bytes <= 16 * MB {
|
||||||
|
1 * KB as u32
|
||||||
|
} else if total_bytes <= 128 * MB {
|
||||||
|
2 * KB as u32
|
||||||
|
} else {
|
||||||
|
(total_bytes.next_power_of_two() / (64 * MB) * KB) as u32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FatType::Fat32 => {
|
||||||
|
if total_bytes <= 260 * MB {
|
||||||
|
512
|
||||||
|
} else if total_bytes <= 8 * GB {
|
||||||
|
4 * KB as u32
|
||||||
|
} else {
|
||||||
|
(total_bytes.next_power_of_two() / (2 * GB) * KB) as u32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const MAX_CLUSTER_SIZE: u32 = 32 * KB as u32;
|
||||||
|
debug_assert!(bytes_per_cluster.is_power_of_two());
|
||||||
|
cmp::min(cmp::max(bytes_per_cluster, bytes_per_sector as u32), MAX_CLUSTER_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_sectors_per_fat(
|
||||||
|
total_sectors: u32,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
sectors_per_cluster: u8,
|
||||||
|
fat_type: FatType,
|
||||||
|
reserved_sectors: u16,
|
||||||
|
root_dir_sectors: u32,
|
||||||
|
fats: u8,
|
||||||
|
) -> u32 {
|
||||||
|
//
|
||||||
|
// FAT size formula transformations:
|
||||||
|
//
|
||||||
|
// Initial basic formula:
|
||||||
|
// size of FAT in bits >= (total number of clusters + 2) * bits per FAT entry
|
||||||
|
//
|
||||||
|
// Note: when computing number of clusters from number of sectors rounding down is used because partial clusters
|
||||||
|
// are not allowed
|
||||||
|
// Note: in those transformations '/' is a floating-point division (not a rounding towards zero division)
|
||||||
|
//
|
||||||
|
// data_sectors = total_sectors - reserved_sectors - fats * sectors_per_fat - root_dir_sectors
|
||||||
|
// total_clusters = floor(data_sectors / sectors_per_cluster)
|
||||||
|
// bits_per_sector = bytes_per_sector * 8
|
||||||
|
// sectors_per_fat * bits_per_sector >= (total_clusters + 2) * bits_per_fat_entry
|
||||||
|
// sectors_per_fat * bits_per_sector >= (floor(data_sectors / sectors_per_cluster) + 2) * bits_per_fat_entry
|
||||||
|
//
|
||||||
|
// Note: omitting the floor function can cause the FAT to be bigger by 1 entry - negligible
|
||||||
|
//
|
||||||
|
// sectors_per_fat * bits_per_sector >= (data_sectors / sectors_per_cluster + 2) * bits_per_fat_entry
|
||||||
|
// t0 = total_sectors - reserved_sectors - root_dir_sectors
|
||||||
|
// sectors_per_fat * bits_per_sector >= ((t0 - fats * sectors_per_fat) / sectors_per_cluster + 2) * bits_per_fat_entry
|
||||||
|
// sectors_per_fat * bits_per_sector / bits_per_fat_entry >= (t0 - fats * sectors_per_fat) / sectors_per_cluster + 2
|
||||||
|
// sectors_per_fat * bits_per_sector / bits_per_fat_entry >= t0 / sectors_per_cluster + 2 - fats * sectors_per_fat / sectors_per_cluster
|
||||||
|
// sectors_per_fat * bits_per_sector / bits_per_fat_entry + fats * sectors_per_fat / sectors_per_cluster >= t0 / sectors_per_cluster + 2
|
||||||
|
// sectors_per_fat * (bits_per_sector / bits_per_fat_entry + fats / sectors_per_cluster) >= t0 / sectors_per_cluster + 2
|
||||||
|
// sectors_per_fat >= (t0 / sectors_per_cluster + 2) / (bits_per_sector / bits_per_fat_entry + fats / sectors_per_cluster)
|
||||||
|
//
|
||||||
|
// Note: MS specification omits the constant 2 in calculations. This library is taking a better approach...
|
||||||
|
//
|
||||||
|
// sectors_per_fat >= ((t0 + 2 * sectors_per_cluster) / sectors_per_cluster) / (bits_per_sector / bits_per_fat_entry + fats / sectors_per_cluster)
|
||||||
|
// sectors_per_fat >= (t0 + 2 * sectors_per_cluster) / (sectors_per_cluster * bits_per_sector / bits_per_fat_entry + fats)
|
||||||
|
//
|
||||||
|
// Note: compared to MS formula this one can suffer from an overflow problem if u32 type is used
|
||||||
|
//
|
||||||
|
// When converting formula to integer types round towards a bigger FAT:
|
||||||
|
// * first division towards infinity
|
||||||
|
// * second division towards zero (it is in a denominator of the first division)
|
||||||
|
|
||||||
|
let t0: u32 = total_sectors - u32::from(reserved_sectors) - root_dir_sectors;
|
||||||
|
let t1: u64 = u64::from(t0) + u64::from(2 * u32::from(sectors_per_cluster));
|
||||||
|
let bits_per_cluster = u32::from(sectors_per_cluster) * u32::from(bytes_per_sector) * BITS_PER_BYTE;
|
||||||
|
let t2 = u64::from(bits_per_cluster / u32::from(fat_type.bits_per_fat_entry()) + u32::from(fats));
|
||||||
|
let sectors_per_fat = (t1 + t2 - 1) / t2;
|
||||||
|
// Note: casting is safe here because number of sectors per FAT cannot be bigger than total sectors number
|
||||||
|
sectors_per_fat as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_fs_geometry(
|
||||||
|
total_sectors: u32,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
sectors_per_cluster: u8,
|
||||||
|
fat_type: FatType,
|
||||||
|
root_dir_sectors: u32,
|
||||||
|
fats: u8,
|
||||||
|
) -> io::Result<(u16, u32)> {
|
||||||
|
// Note: most of implementations use 32 reserved sectors for FAT32 but it's wasting of space
|
||||||
|
// This implementation uses only 8. This is enough to fit in two boot sectors (main and backup) with additional
|
||||||
|
// bootstrap code and one FSInfo sector. It also makes FAT alligned to 4096 which is a nice number.
|
||||||
|
let reserved_sectors: u16 = if fat_type == FatType::Fat32 { 8 } else { 1 };
|
||||||
|
|
||||||
|
// Check if volume has enough space to accomodate reserved sectors, FAT, root directory and some data space
|
||||||
|
// Having less than 8 sectors for FAT and data would make a little sense
|
||||||
|
if total_sectors <= u32::from(reserved_sectors) + u32::from(root_dir_sectors) + 8 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Volume is too small"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate File Allocation Table size
|
||||||
|
let sectors_per_fat = determine_sectors_per_fat(
|
||||||
|
total_sectors,
|
||||||
|
bytes_per_sector,
|
||||||
|
sectors_per_cluster,
|
||||||
|
fat_type,
|
||||||
|
reserved_sectors,
|
||||||
|
root_dir_sectors,
|
||||||
|
fats,
|
||||||
|
);
|
||||||
|
|
||||||
|
let data_sectors =
|
||||||
|
total_sectors - u32::from(reserved_sectors) - u32::from(root_dir_sectors) - sectors_per_fat * u32::from(fats);
|
||||||
|
let total_clusters = data_sectors / u32::from(sectors_per_cluster);
|
||||||
|
if fat_type != FatType::from_clusters(total_clusters) {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Invalid FAT type"));
|
||||||
|
}
|
||||||
|
debug_assert!(total_clusters >= fat_type.min_clusters());
|
||||||
|
if total_clusters > fat_type.max_clusters() {
|
||||||
|
// Note: it can happen for FAT32
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Too many clusters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((reserved_sectors, sectors_per_fat));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_root_dir_sectors(root_dir_entries: u16, bytes_per_sector: u16, fat_type: FatType) -> u32 {
|
||||||
|
if fat_type == FatType::Fat32 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let root_dir_bytes = u32::from(root_dir_entries) * DIR_ENTRY_SIZE as u32;
|
||||||
|
(root_dir_bytes + u32::from(bytes_per_sector) - 1) / u32::from(bytes_per_sector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_fs_geometry(
|
||||||
|
total_sectors: u32,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
sectors_per_cluster: u8,
|
||||||
|
root_dir_entries: u16,
|
||||||
|
fats: u8,
|
||||||
|
) -> io::Result<(FatType, u16, u32)> {
|
||||||
|
for &fat_type in &[FatType::Fat32, FatType::Fat16, FatType::Fat12] {
|
||||||
|
let root_dir_sectors = determine_root_dir_sectors(root_dir_entries, bytes_per_sector, fat_type);
|
||||||
|
let result =
|
||||||
|
try_fs_geometry(total_sectors, bytes_per_sector, sectors_per_cluster, fat_type, root_dir_sectors, fats);
|
||||||
|
if result.is_ok() {
|
||||||
|
let (reserved_sectors, sectors_per_fat) = result.unwrap(); // SAFE: used is_ok() before
|
||||||
|
return Ok((fat_type, reserved_sectors, sectors_per_fat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::new(ErrorKind::Other, "Cannot select FAT type - unfortunate disk size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_bpb(
|
||||||
|
options: &FormatVolumeOptions,
|
||||||
|
total_sectors: u32,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
) -> io::Result<(BiosParameterBlock, FatType)> {
|
||||||
|
let bytes_per_cluster = options.bytes_per_cluster.unwrap_or_else(|| {
|
||||||
|
let total_bytes = u64::from(total_sectors) * u64::from(bytes_per_sector);
|
||||||
|
determine_bytes_per_cluster(total_bytes, bytes_per_sector, options.fat_type)
|
||||||
|
});
|
||||||
|
|
||||||
|
let sectors_per_cluster = bytes_per_cluster / u32::from(bytes_per_sector);
|
||||||
|
assert!(sectors_per_cluster <= u32::from(u8::MAX));
|
||||||
|
let sectors_per_cluster = sectors_per_cluster as u8;
|
||||||
|
|
||||||
|
let fats = options.fats.unwrap_or(2u8);
|
||||||
|
let root_dir_entries = options.max_root_dir_entries.unwrap_or(512);
|
||||||
|
let (fat_type, reserved_sectors, sectors_per_fat) =
|
||||||
|
determine_fs_geometry(total_sectors, bytes_per_sector, sectors_per_cluster, root_dir_entries, fats)?;
|
||||||
|
|
||||||
|
// drive_num should be 0 for floppy disks and 0x80 for hard disks - determine it using FAT type
|
||||||
|
let drive_num = options.drive_num.unwrap_or_else(|| if fat_type == FatType::Fat12 { 0 } else { 0x80 });
|
||||||
|
|
||||||
|
// reserved_0 is always zero
|
||||||
|
let reserved_0 = [0u8; 12];
|
||||||
|
|
||||||
|
// setup volume label
|
||||||
|
let mut volume_label = [0u8; 11];
|
||||||
|
if let Some(volume_label_from_opts) = options.volume_label {
|
||||||
|
volume_label.copy_from_slice(&volume_label_from_opts);
|
||||||
|
} else {
|
||||||
|
volume_label.copy_from_slice(b"NO NAME ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup fs_type_label field
|
||||||
|
let mut fs_type_label = [0u8; 8];
|
||||||
|
let fs_type_label_str = match fat_type {
|
||||||
|
FatType::Fat12 => b"FAT12 ",
|
||||||
|
FatType::Fat16 => b"FAT16 ",
|
||||||
|
FatType::Fat32 => b"FAT32 ",
|
||||||
|
};
|
||||||
|
fs_type_label.copy_from_slice(fs_type_label_str);
|
||||||
|
|
||||||
|
// create Bios Parameter Block struct
|
||||||
|
let is_fat32 = fat_type == FatType::Fat32;
|
||||||
|
let sectors_per_fat_16 = if is_fat32 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
debug_assert!(sectors_per_fat <= u32::from(u16::MAX));
|
||||||
|
sectors_per_fat as u16
|
||||||
|
};
|
||||||
|
let bpb = BiosParameterBlock {
|
||||||
|
bytes_per_sector,
|
||||||
|
sectors_per_cluster,
|
||||||
|
reserved_sectors,
|
||||||
|
fats,
|
||||||
|
root_entries: if is_fat32 { 0 } else { root_dir_entries },
|
||||||
|
total_sectors_16: if total_sectors < 0x10000 { total_sectors as u16 } else { 0 },
|
||||||
|
media: options.media.unwrap_or(0xF8),
|
||||||
|
sectors_per_fat_16,
|
||||||
|
sectors_per_track: options.sectors_per_track.unwrap_or(0x20),
|
||||||
|
heads: options.heads.unwrap_or(0x40),
|
||||||
|
hidden_sectors: 0,
|
||||||
|
total_sectors_32: if total_sectors >= 0x10000 { total_sectors } else { 0 },
|
||||||
|
// FAT32 fields start
|
||||||
|
sectors_per_fat_32: if is_fat32 { sectors_per_fat } else { 0 },
|
||||||
|
extended_flags: 0, // mirroring enabled
|
||||||
|
fs_version: 0,
|
||||||
|
root_dir_first_cluster: if is_fat32 { 2 } else { 0 },
|
||||||
|
fs_info_sector: if is_fat32 { 1 } else { 0 },
|
||||||
|
backup_boot_sector: if is_fat32 { 6 } else { 0 },
|
||||||
|
reserved_0,
|
||||||
|
// FAT32 fields end
|
||||||
|
drive_num,
|
||||||
|
reserved_1: 0,
|
||||||
|
ext_sig: 0x29,
|
||||||
|
volume_id: options.volume_id.unwrap_or(0x12345678),
|
||||||
|
volume_label,
|
||||||
|
fs_type_label,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if number of clusters is proper for used FAT type
|
||||||
|
if FatType::from_clusters(bpb.total_clusters()) != fat_type {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"Total number of clusters and FAT type does not match. Try other volume size",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((bpb, fat_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_boot_sector(
|
||||||
|
options: &FormatVolumeOptions,
|
||||||
|
total_sectors: u32,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
) -> io::Result<(BootSector, FatType)> {
|
||||||
|
let mut boot: BootSector = Default::default();
|
||||||
|
let (bpb, fat_type) = format_bpb(options, total_sectors, bytes_per_sector)?;
|
||||||
|
boot.bpb = bpb;
|
||||||
|
boot.oem_name.copy_from_slice(b"MSWIN4.1");
|
||||||
|
// Boot code copied from FAT32 boot sector initialized by mkfs.fat
|
||||||
|
boot.bootjmp = [0xEB, 0x58, 0x90];
|
||||||
|
let boot_code: [u8; 129] = [
|
||||||
|
0x0E, 0x1F, 0xBE, 0x77, 0x7C, 0xAC, 0x22, 0xC0, 0x74, 0x0B, 0x56, 0xB4, 0x0E, 0xBB, 0x07, 0x00, 0xCD, 0x10,
|
||||||
|
0x5E, 0xEB, 0xF0, 0x32, 0xE4, 0xCD, 0x16, 0xCD, 0x19, 0xEB, 0xFE, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73,
|
||||||
|
0x20, 0x6E, 0x6F, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6F, 0x6F, 0x74, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x64, 0x69,
|
||||||
|
0x73, 0x6B, 0x2E, 0x20, 0x20, 0x50, 0x6C, 0x65, 0x61, 0x73, 0x65, 0x20, 0x69, 0x6E, 0x73, 0x65, 0x72, 0x74,
|
||||||
|
0x20, 0x61, 0x20, 0x62, 0x6F, 0x6F, 0x74, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x66, 0x6C, 0x6F, 0x70, 0x70, 0x79,
|
||||||
|
0x20, 0x61, 0x6E, 0x64, 0x0D, 0x0A, 0x70, 0x72, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6E, 0x79, 0x20, 0x6B, 0x65,
|
||||||
|
0x79, 0x20, 0x74, 0x6F, 0x20, 0x74, 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6E, 0x20, 0x2E, 0x2E, 0x2E,
|
||||||
|
0x20, 0x0D, 0x0A,
|
||||||
|
];
|
||||||
|
boot.boot_code[..boot_code.len()].copy_from_slice(&boot_code);
|
||||||
|
boot.boot_sig = [0x55, 0xAA];
|
||||||
|
|
||||||
|
// fix offsets in bootjmp and boot code for non-FAT32 filesystems (bootcode is on a different offset)
|
||||||
|
if fat_type != FatType::Fat32 {
|
||||||
|
// offset of boot code
|
||||||
|
let boot_code_offset: u8 = 0x36 + 8;
|
||||||
|
boot.bootjmp[1] = boot_code_offset - 2;
|
||||||
|
// offset of message
|
||||||
|
const MESSAGE_OFFSET: u16 = 29;
|
||||||
|
let message_offset_in_sector = u16::from(boot_code_offset) + MESSAGE_OFFSET + 0x7c00;
|
||||||
|
boot.boot_code[3] = (message_offset_in_sector & 0xff) as u8;
|
||||||
|
boot.boot_code[4] = (message_offset_in_sector >> 8) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((boot, fat_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_estimate_fat_type() {
|
||||||
|
assert_eq!(estimate_fat_type(3 * MB), FatType::Fat12);
|
||||||
|
assert_eq!(estimate_fat_type(4 * MB), FatType::Fat16);
|
||||||
|
assert_eq!(estimate_fat_type(511 * MB), FatType::Fat16);
|
||||||
|
assert_eq!(estimate_fat_type(512 * MB), FatType::Fat32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_determine_bytes_per_cluster_fat12() {
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1 * MB + 0, 512, Some(FatType::Fat12)), 512);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1 * MB + 1, 512, Some(FatType::Fat12)), 1024);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1 * MB, 4096, Some(FatType::Fat12)), 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_determine_bytes_per_cluster_fat16() {
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1 * MB, 512, Some(FatType::Fat16)), 1 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1 * MB, 4 * KB as u16, Some(FatType::Fat16)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(16 * MB + 0, 512, Some(FatType::Fat16)), 1 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(16 * MB + 1, 512, Some(FatType::Fat16)), 2 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(128 * MB + 0, 512, Some(FatType::Fat16)), 2 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(128 * MB + 1, 512, Some(FatType::Fat16)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(256 * MB + 0, 512, Some(FatType::Fat16)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(256 * MB + 1, 512, Some(FatType::Fat16)), 8 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(512 * MB + 0, 512, Some(FatType::Fat16)), 8 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(512 * MB + 1, 512, Some(FatType::Fat16)), 16 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1024 * MB + 0, 512, Some(FatType::Fat16)), 16 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(1024 * MB + 1, 512, Some(FatType::Fat16)), 32 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(99999 * MB, 512, Some(FatType::Fat16)), 32 * KB as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_determine_bytes_per_cluster_fat32() {
|
||||||
|
assert_eq!(determine_bytes_per_cluster(260 * MB as u64, 512, Some(FatType::Fat32)), 512);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(260 * MB as u64, 4 * KB as u16, Some(FatType::Fat32)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(260 * MB as u64 + 1, 512, Some(FatType::Fat32)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(8 * GB as u64, 512, Some(FatType::Fat32)), 4 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(8 * GB as u64 + 1, 512, Some(FatType::Fat32)), 8 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(16 * GB as u64 + 0, 512, Some(FatType::Fat32)), 8 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(16 * GB as u64 + 1, 512, Some(FatType::Fat32)), 16 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(32 * GB as u64, 512, Some(FatType::Fat32)), 16 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(32 * GB as u64 + 1, 512, Some(FatType::Fat32)), 32 * KB as u32);
|
||||||
|
assert_eq!(determine_bytes_per_cluster(999 * GB as u64, 512, Some(FatType::Fat32)), 32 * KB as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_determine_sectors_per_fat_single(
|
||||||
|
total_bytes: u64,
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
bytes_per_cluster: u32,
|
||||||
|
fat_type: FatType,
|
||||||
|
reserved_sectors: u16,
|
||||||
|
fats: u8,
|
||||||
|
root_dir_entries: u32,
|
||||||
|
) {
|
||||||
|
let total_sectors = total_bytes / u64::from(bytes_per_sector);
|
||||||
|
debug_assert!(total_sectors <= u64::from(core::u32::MAX), "{:x}", total_sectors);
|
||||||
|
let total_sectors = total_sectors as u32;
|
||||||
|
|
||||||
|
let sectors_per_cluster = (bytes_per_cluster / u32::from(bytes_per_sector)) as u8;
|
||||||
|
let root_dir_size = root_dir_entries * DIR_ENTRY_SIZE as u32;
|
||||||
|
let root_dir_sectors = (root_dir_size + u32::from(bytes_per_sector) - 1) / u32::from(bytes_per_sector);
|
||||||
|
let sectors_per_fat = determine_sectors_per_fat(
|
||||||
|
total_sectors,
|
||||||
|
bytes_per_sector,
|
||||||
|
sectors_per_cluster,
|
||||||
|
fat_type,
|
||||||
|
reserved_sectors,
|
||||||
|
root_dir_sectors,
|
||||||
|
fats,
|
||||||
|
);
|
||||||
|
|
||||||
|
let sectors_per_all_fats = u32::from(fats) * sectors_per_fat;
|
||||||
|
let total_data_sectors = total_sectors - u32::from(reserved_sectors) - sectors_per_all_fats - root_dir_sectors;
|
||||||
|
let total_clusters = total_data_sectors / u32::from(sectors_per_cluster);
|
||||||
|
if FatType::from_clusters(total_clusters) != fat_type {
|
||||||
|
// Skip impossible FAT configurations
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bits_per_sector = u32::from(bytes_per_sector) * BITS_PER_BYTE;
|
||||||
|
let bits_per_fat = u64::from(sectors_per_fat) * u64::from(bits_per_sector);
|
||||||
|
let total_fat_entries = (bits_per_fat / u64::from(fat_type.bits_per_fat_entry())) as u32;
|
||||||
|
let fat_clusters = total_fat_entries - RESERVED_FAT_ENTRIES;
|
||||||
|
// Note: fat_entries_per_sector is rounded down for FAT12
|
||||||
|
let fat_entries_per_sector = u32::from(bits_per_sector) / fat_type.bits_per_fat_entry();
|
||||||
|
let desc = format!("total_clusters {}, fat_clusters {}, total_sectors {}, bytes/sector {}, sectors/cluster {}, fat_type {:?}, reserved_sectors {}, root_dir_sectors {}, sectors_per_fat {}",
|
||||||
|
total_clusters, fat_clusters, total_sectors, bytes_per_sector, sectors_per_cluster, fat_type, reserved_sectors, root_dir_sectors, sectors_per_fat);
|
||||||
|
assert!(fat_clusters >= total_clusters, "Too small FAT: {}", desc);
|
||||||
|
assert!(fat_clusters <= total_clusters + 2 * fat_entries_per_sector, "Too big FAT: {}", desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_determine_sectors_per_fat_for_multiple_sizes(
|
||||||
|
bytes_per_sector: u16,
|
||||||
|
fat_type: FatType,
|
||||||
|
reserved_sectors: u16,
|
||||||
|
fats: u8,
|
||||||
|
root_dir_entries: u32,
|
||||||
|
) {
|
||||||
|
let mut bytes_per_cluster = u32::from(bytes_per_sector);
|
||||||
|
while bytes_per_cluster <= 64 * KB as u32 {
|
||||||
|
let mut size = 1 * MB;
|
||||||
|
while size < 2048 * GB {
|
||||||
|
test_determine_sectors_per_fat_single(
|
||||||
|
size,
|
||||||
|
bytes_per_sector,
|
||||||
|
bytes_per_cluster,
|
||||||
|
fat_type,
|
||||||
|
reserved_sectors,
|
||||||
|
fats,
|
||||||
|
root_dir_entries,
|
||||||
|
);
|
||||||
|
size = size + size / 7;
|
||||||
|
}
|
||||||
|
size = 2048 * GB - 1;
|
||||||
|
test_determine_sectors_per_fat_single(
|
||||||
|
size,
|
||||||
|
bytes_per_sector,
|
||||||
|
bytes_per_cluster,
|
||||||
|
fat_type,
|
||||||
|
reserved_sectors,
|
||||||
|
fats,
|
||||||
|
root_dir_entries,
|
||||||
|
);
|
||||||
|
bytes_per_cluster *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_determine_sectors_per_fat() {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat12, 1, 2, 512);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat12, 1, 1, 512);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat12, 1, 2, 8192);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(4096, FatType::Fat12, 1, 2, 512);
|
||||||
|
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat16, 1, 2, 512);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat16, 1, 1, 512);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat16, 1, 2, 8192);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(4096, FatType::Fat16, 1, 2, 512);
|
||||||
|
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat32, 32, 2, 0);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(512, FatType::Fat32, 32, 1, 0);
|
||||||
|
test_determine_sectors_per_fat_for_multiple_sizes(4096, FatType::Fat32, 32, 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_boot_sector() {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
let bytes_per_sector = 512u16;
|
||||||
|
// test all partition sizes from 1MB to 2TB (u32::MAX sectors is 2TB - 1 for 512 byte sectors)
|
||||||
|
let mut total_sectors_vec = Vec::new();
|
||||||
|
let mut size = 1 * MB;
|
||||||
|
while size < 2048 * GB {
|
||||||
|
total_sectors_vec.push((size / u64::from(bytes_per_sector)) as u32);
|
||||||
|
size = size + size / 7;
|
||||||
|
}
|
||||||
|
total_sectors_vec.push(core::u32::MAX);
|
||||||
|
for total_sectors in total_sectors_vec {
|
||||||
|
let (boot, _) = format_boot_sector(&FormatVolumeOptions::new(), total_sectors, bytes_per_sector)
|
||||||
|
.expect("format_boot_sector");
|
||||||
|
boot.validate().expect("validate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,687 @@
|
||||||
|
#[cfg(all(not(feature = "std"), feature = "alloc"))]
|
||||||
|
use alloc::{string::String, vec::Vec};
|
||||||
|
use core::char;
|
||||||
|
use core::iter::FromIterator;
|
||||||
|
use core::{fmt, str};
|
||||||
|
use io;
|
||||||
|
use io::prelude::*;
|
||||||
|
use io::Cursor;
|
||||||
|
|
||||||
|
use byteorder::LittleEndian;
|
||||||
|
use byteorder_ext::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use dir::{Dir, DirRawStream};
|
||||||
|
use file::File;
|
||||||
|
use fs::{FatType, FileSystem, OemCpConverter, ReadWriteSeek};
|
||||||
|
use time::{Date, DateTime};
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// A FAT file attributes.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FileAttributes: u8 {
|
||||||
|
const READ_ONLY = 0x01;
|
||||||
|
const HIDDEN = 0x02;
|
||||||
|
const SYSTEM = 0x04;
|
||||||
|
const VOLUME_ID = 0x08;
|
||||||
|
const DIRECTORY = 0x10;
|
||||||
|
const ARCHIVE = 0x20;
|
||||||
|
const LFN = Self::READ_ONLY.bits | Self::HIDDEN.bits
|
||||||
|
| Self::SYSTEM.bits | Self::VOLUME_ID.bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size of single directory entry in bytes
|
||||||
|
pub(crate) const DIR_ENTRY_SIZE: u64 = 32;
|
||||||
|
|
||||||
|
// Directory entry flags available in first byte of the short name
|
||||||
|
pub(crate) const DIR_ENTRY_DELETED_FLAG: u8 = 0xE5;
|
||||||
|
pub(crate) const DIR_ENTRY_REALLY_E5_FLAG: u8 = 0x05;
|
||||||
|
|
||||||
|
// Length in characters of a LFN fragment packed in one directory entry
|
||||||
|
pub(crate) const LFN_PART_LEN: usize = 13;
|
||||||
|
|
||||||
|
// Bit used in order field to mark last LFN entry
|
||||||
|
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 {
|
||||||
|
const PADDING: u8 = b' ';
|
||||||
|
|
||||||
|
pub(crate) fn new(raw_name: &[u8; 11]) -> Self {
|
||||||
|
// get name components length by looking for space character
|
||||||
|
let name_len = raw_name[0..8].iter().rposition(|x| *x != Self::PADDING).map(|p| p + 1).unwrap_or(0);
|
||||||
|
let ext_len = raw_name[8..11].iter().rposition(|x| *x != Self::PADDING).map(|p| p + 1).unwrap_or(0);
|
||||||
|
let mut name = [Self::PADDING; 12];
|
||||||
|
name[..name_len].copy_from_slice(&raw_name[..name_len]);
|
||||||
|
let total_len = if ext_len > 0 {
|
||||||
|
name[name_len] = b'.';
|
||||||
|
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
|
||||||
|
};
|
||||||
|
// FAT encodes character 0xE5 as 0x05 because 0xE5 marks deleted files
|
||||||
|
if name[0] == DIR_ENTRY_REALLY_E5_FLAG {
|
||||||
|
name[0] = 0xE5;
|
||||||
|
}
|
||||||
|
// Short names in FAT filesystem are encoded in OEM code-page
|
||||||
|
ShortName { name, len: total_len as u8 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.name[..self.len as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
fn to_string(&self, oem_cp_converter: &OemCpConverter) -> String {
|
||||||
|
// Strip non-ascii characters from short name
|
||||||
|
let char_iter = self.as_bytes().iter().cloned().map(|c| oem_cp_converter.decode(c));
|
||||||
|
// Build string from character iterator
|
||||||
|
String::from_iter(char_iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eq_ignore_case(&self, name: &str, oem_cp_converter: &OemCpConverter) -> bool {
|
||||||
|
// Strip non-ascii characters from short name
|
||||||
|
let byte_iter = self.as_bytes().iter().cloned();
|
||||||
|
let char_iter = byte_iter.map(|c| oem_cp_converter.decode(c));
|
||||||
|
let uppercase_char_iter = char_iter.flat_map(|c| c.to_uppercase());
|
||||||
|
// Build string from character iterator
|
||||||
|
uppercase_char_iter.eq(name.chars().flat_map(|c| c.to_uppercase()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(crate) struct DirFileEntryData {
|
||||||
|
name: [u8; 11],
|
||||||
|
attrs: FileAttributes,
|
||||||
|
reserved_0: u8,
|
||||||
|
create_time_0: u8,
|
||||||
|
create_time_1: u16,
|
||||||
|
create_date: u16,
|
||||||
|
access_date: u16,
|
||||||
|
first_cluster_hi: u16,
|
||||||
|
modify_time: u16,
|
||||||
|
modify_date: u16,
|
||||||
|
first_cluster_lo: u16,
|
||||||
|
size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirFileEntryData {
|
||||||
|
pub(crate) fn new(name: [u8; 11], attrs: FileAttributes) -> Self {
|
||||||
|
DirFileEntryData { name, attrs, ..Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn renamed(&self, new_name: [u8; 11]) -> Self {
|
||||||
|
let mut sfn_entry = self.clone();
|
||||||
|
sfn_entry.name = new_name;
|
||||||
|
sfn_entry
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name(&self) -> &[u8; 11] {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
fn lowercase_name(&self) -> ShortName {
|
||||||
|
let mut name_copy: [u8; 11] = self.name;
|
||||||
|
if self.lowercase_basename() {
|
||||||
|
for c in &mut name_copy[..8] {
|
||||||
|
*c = (*c as char).to_ascii_lowercase() as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.lowercase_ext() {
|
||||||
|
for c in &mut name_copy[8..] {
|
||||||
|
*c = (*c as char).to_ascii_lowercase() as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShortName::new(&name_copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_cluster(&self, fat_type: FatType) -> Option<u32> {
|
||||||
|
let first_cluster_hi = if fat_type == FatType::Fat32 { self.first_cluster_hi } else { 0 };
|
||||||
|
let n = ((first_cluster_hi as u32) << 16) | self.first_cluster_lo as u32;
|
||||||
|
if n == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_first_cluster(&mut self, cluster: Option<u32>, fat_type: FatType) {
|
||||||
|
let n = cluster.unwrap_or(0);
|
||||||
|
if fat_type == FatType::Fat32 {
|
||||||
|
self.first_cluster_hi = (n >> 16) as u16;
|
||||||
|
}
|
||||||
|
self.first_cluster_lo = (n & 0xFFFF) as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size(&self) -> Option<u32> {
|
||||||
|
if self.is_file() {
|
||||||
|
Some(self.size)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_size(&mut self, size: u32) {
|
||||||
|
self.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_dir(&self) -> bool {
|
||||||
|
self.attrs.contains(FileAttributes::DIRECTORY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_file(&self) -> bool {
|
||||||
|
!self.is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lowercase_basename(&self) -> bool {
|
||||||
|
self.reserved_0 & (1 << 3) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lowercase_ext(&self) -> bool {
|
||||||
|
self.reserved_0 & (1 << 4) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn created(&self) -> DateTime {
|
||||||
|
DateTime::decode(self.create_date, self.create_time_1, self.create_time_0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accessed(&self) -> Date {
|
||||||
|
Date::decode(self.access_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modified(&self) -> DateTime {
|
||||||
|
DateTime::decode(self.modify_date, self.modify_time, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_created(&mut self, date_time: DateTime) {
|
||||||
|
self.create_date = date_time.date.encode();
|
||||||
|
let encoded_time = date_time.time.encode();
|
||||||
|
self.create_time_1 = encoded_time.0;
|
||||||
|
self.create_time_0 = encoded_time.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_accessed(&mut self, date: Date) {
|
||||||
|
self.access_date = date.encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_modified(&mut self, date_time: DateTime) {
|
||||||
|
self.modify_date = date_time.date.encode();
|
||||||
|
self.modify_time = date_time.time.encode().0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
|
||||||
|
wrt.write_all(&self.name)?;
|
||||||
|
wrt.write_u8(self.attrs.bits())?;
|
||||||
|
wrt.write_u8(self.reserved_0)?;
|
||||||
|
wrt.write_u8(self.create_time_0)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.create_time_1)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.create_date)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.access_date)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.first_cluster_hi)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.modify_time)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.modify_date)?;
|
||||||
|
wrt.write_u16::<LittleEndian>(self.first_cluster_lo)?;
|
||||||
|
wrt.write_u32::<LittleEndian>(self.size)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_deleted(&self) -> bool {
|
||||||
|
self.name[0] == DIR_ENTRY_DELETED_FLAG
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_deleted(&mut self) {
|
||||||
|
self.name[0] = DIR_ENTRY_DELETED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_end(&self) -> bool {
|
||||||
|
self.name[0] == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_volume(&self) -> bool {
|
||||||
|
self.attrs.contains(FileAttributes::VOLUME_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub(crate) struct DirLfnEntryData {
|
||||||
|
order: u8,
|
||||||
|
name_0: [u16; 5],
|
||||||
|
attrs: FileAttributes,
|
||||||
|
entry_type: u8,
|
||||||
|
checksum: u8,
|
||||||
|
name_1: [u16; 6],
|
||||||
|
reserved_0: u16,
|
||||||
|
name_2: [u16; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirLfnEntryData {
|
||||||
|
pub(crate) fn new(order: u8, checksum: u8) -> Self {
|
||||||
|
DirLfnEntryData { order, checksum, attrs: FileAttributes::LFN, ..Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn copy_name_from_slice(&mut self, lfn_part: &[u16; LFN_PART_LEN]) {
|
||||||
|
self.name_0.copy_from_slice(&lfn_part[0..5]);
|
||||||
|
self.name_1.copy_from_slice(&lfn_part[5..5 + 6]);
|
||||||
|
self.name_2.copy_from_slice(&lfn_part[11..11 + 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn copy_name_to_slice(&self, lfn_part: &mut [u16]) {
|
||||||
|
debug_assert!(lfn_part.len() == LFN_PART_LEN);
|
||||||
|
lfn_part[0..5].copy_from_slice(&self.name_0);
|
||||||
|
lfn_part[5..11].copy_from_slice(&self.name_1);
|
||||||
|
lfn_part[11..13].copy_from_slice(&self.name_2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
|
||||||
|
wrt.write_u8(self.order)?;
|
||||||
|
for ch in self.name_0.iter() {
|
||||||
|
wrt.write_u16::<LittleEndian>(*ch)?;
|
||||||
|
}
|
||||||
|
wrt.write_u8(self.attrs.bits())?;
|
||||||
|
wrt.write_u8(self.entry_type)?;
|
||||||
|
wrt.write_u8(self.checksum)?;
|
||||||
|
for ch in self.name_1.iter() {
|
||||||
|
wrt.write_u16::<LittleEndian>(*ch)?;
|
||||||
|
}
|
||||||
|
wrt.write_u16::<LittleEndian>(self.reserved_0)?;
|
||||||
|
for ch in self.name_2.iter() {
|
||||||
|
wrt.write_u16::<LittleEndian>(*ch)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub(crate) fn order(&self) -> u8 {
|
||||||
|
self.order
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub(crate) fn checksum(&self) -> u8 {
|
||||||
|
self.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_deleted(&self) -> bool {
|
||||||
|
self.order == DIR_ENTRY_DELETED_FLAG
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_deleted(&mut self) {
|
||||||
|
self.order = DIR_ENTRY_DELETED_FLAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_end(&self) -> bool {
|
||||||
|
self.order == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum DirEntryData {
|
||||||
|
File(DirFileEntryData),
|
||||||
|
Lfn(DirLfnEntryData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirEntryData {
|
||||||
|
pub(crate) fn serialize(&self, wrt: &mut Write) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
&DirEntryData::File(ref file) => file.serialize(wrt),
|
||||||
|
&DirEntryData::Lfn(ref lfn) => lfn.serialize(wrt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn deserialize(rdr: &mut Read) -> io::Result<Self> {
|
||||||
|
let mut name = [0; 11];
|
||||||
|
match rdr.read_exact(&mut name) {
|
||||||
|
Err(ref err) if err.kind() == io::ErrorKind::UnexpectedEof => {
|
||||||
|
// entries can occupy all clusters of directory so there is no zero entry at the end
|
||||||
|
// handle it here by returning non-existing empty entry
|
||||||
|
return Ok(DirEntryData::File(DirFileEntryData { ..Default::default() }));
|
||||||
|
},
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
let attrs = FileAttributes::from_bits_truncate(rdr.read_u8()?);
|
||||||
|
if attrs & FileAttributes::LFN == FileAttributes::LFN {
|
||||||
|
// read long name entry
|
||||||
|
let mut data = DirLfnEntryData { attrs, ..Default::default() };
|
||||||
|
// use cursor to divide name into order and LFN name_0
|
||||||
|
let mut cur = Cursor::new(&name);
|
||||||
|
data.order = cur.read_u8()?;
|
||||||
|
cur.read_u16_into::<LittleEndian>(&mut data.name_0)?;
|
||||||
|
data.entry_type = rdr.read_u8()?;
|
||||||
|
data.checksum = rdr.read_u8()?;
|
||||||
|
rdr.read_u16_into::<LittleEndian>(&mut data.name_1)?;
|
||||||
|
data.reserved_0 = rdr.read_u16::<LittleEndian>()?;
|
||||||
|
rdr.read_u16_into::<LittleEndian>(&mut data.name_2)?;
|
||||||
|
Ok(DirEntryData::Lfn(data))
|
||||||
|
} else {
|
||||||
|
// read short name entry
|
||||||
|
let data = DirFileEntryData {
|
||||||
|
name,
|
||||||
|
attrs,
|
||||||
|
reserved_0: rdr.read_u8()?,
|
||||||
|
create_time_0: rdr.read_u8()?,
|
||||||
|
create_time_1: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
create_date: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
access_date: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
first_cluster_hi: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
modify_time: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
modify_date: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
first_cluster_lo: rdr.read_u16::<LittleEndian>()?,
|
||||||
|
size: rdr.read_u32::<LittleEndian>()?,
|
||||||
|
};
|
||||||
|
Ok(DirEntryData::File(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_deleted(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
&DirEntryData::File(ref file) => file.is_deleted(),
|
||||||
|
&DirEntryData::Lfn(ref lfn) => lfn.is_deleted(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_deleted(&mut self) {
|
||||||
|
match self {
|
||||||
|
&mut DirEntryData::File(ref mut file) => file.set_deleted(),
|
||||||
|
&mut DirEntryData::Lfn(ref mut lfn) => lfn.set_deleted(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_end(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
&DirEntryData::File(ref file) => file.is_end(),
|
||||||
|
&DirEntryData::Lfn(ref lfn) => lfn.is_end(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct DirEntryEditor {
|
||||||
|
data: DirFileEntryData,
|
||||||
|
pos: u64,
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirEntryEditor {
|
||||||
|
fn new(data: DirFileEntryData, pos: u64) -> Self {
|
||||||
|
DirEntryEditor { data, pos, dirty: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner(&self) -> &DirFileEntryData {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_first_cluster(&mut self, first_cluster: Option<u32>, fat_type: FatType) {
|
||||||
|
if first_cluster != self.data.first_cluster(fat_type) {
|
||||||
|
self.data.set_first_cluster(first_cluster, fat_type);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_size(&mut self, size: u32) {
|
||||||
|
match self.data.size() {
|
||||||
|
Some(n) if size != n => {
|
||||||
|
self.data.set_size(size);
|
||||||
|
self.dirty = true;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_created(&mut self, date_time: DateTime) {
|
||||||
|
if date_time != self.data.created() {
|
||||||
|
self.data.set_created(date_time);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_accessed(&mut self, date: Date) {
|
||||||
|
if date != self.data.accessed() {
|
||||||
|
self.data.set_accessed(date);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_modified(&mut self, date_time: DateTime) {
|
||||||
|
if date_time != self.data.modified() {
|
||||||
|
self.data.set_modified(date_time);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn flush<T: ReadWriteSeek>(&mut self, fs: &FileSystem<T>) -> io::Result<()> {
|
||||||
|
if self.dirty {
|
||||||
|
self.write(fs)?;
|
||||||
|
self.dirty = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<T: ReadWriteSeek>(&self, fs: &FileSystem<T>) -> io::Result<()> {
|
||||||
|
let mut disk = fs.disk.borrow_mut();
|
||||||
|
disk.seek(io::SeekFrom::Start(self.pos))?;
|
||||||
|
self.data.serialize(&mut *disk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A FAT directory entry.
|
||||||
|
///
|
||||||
|
/// `DirEntry` is returned by `DirIter` when reading a directory.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DirEntry<'a, T: ReadWriteSeek + 'a> {
|
||||||
|
pub(crate) data: DirFileEntryData,
|
||||||
|
pub(crate) short_name: ShortName,
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub(crate) lfn_utf16: Vec<u16>,
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
pub(crate) lfn_utf16: (),
|
||||||
|
pub(crate) entry_pos: u64,
|
||||||
|
pub(crate) offset_range: (u64, u64),
|
||||||
|
pub(crate) fs: &'a FileSystem<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> DirEntry<'a, T> {
|
||||||
|
/// Returns short file name.
|
||||||
|
///
|
||||||
|
/// Non-ASCII characters are replaced by the replacement character (U+FFFD).
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub fn short_file_name(&self) -> String {
|
||||||
|
self.short_name.to_string(self.fs.options.oem_cp_converter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns short file name as byte array slice.
|
||||||
|
///
|
||||||
|
/// Characters are encoded in the OEM codepage.
|
||||||
|
pub fn short_file_name_as_bytes(&self) -> &[u8] {
|
||||||
|
self.short_name.as_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_utf16.is_empty() {
|
||||||
|
self.data.lowercase_name().to_string(self.fs.options.oem_cp_converter)
|
||||||
|
} else {
|
||||||
|
String::from_utf16_lossy(&self.lfn_utf16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file attributes.
|
||||||
|
pub fn attributes(&self) -> FileAttributes {
|
||||||
|
self.data.attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if entry belongs to directory.
|
||||||
|
pub fn is_dir(&self) -> bool {
|
||||||
|
self.data.is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if entry belongs to regular file.
|
||||||
|
pub fn is_file(&self) -> bool {
|
||||||
|
self.data.is_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_cluster(&self) -> Option<u32> {
|
||||||
|
self.data.first_cluster(self.fs.fat_type())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor(&self) -> DirEntryEditor {
|
||||||
|
DirEntryEditor::new(self.data.clone(), self.entry_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_same_entry(&self, other: &DirEntry<T>) -> bool {
|
||||||
|
self.entry_pos == other.entry_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `File` struct for this entry.
|
||||||
|
///
|
||||||
|
/// Panics if this is not a file.
|
||||||
|
pub fn to_file(&self) -> File<'a, T> {
|
||||||
|
assert!(!self.is_dir(), "Not a file entry");
|
||||||
|
File::new(self.first_cluster(), Some(self.editor()), self.fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Dir` struct for this entry.
|
||||||
|
///
|
||||||
|
/// Panics if this is not a directory.
|
||||||
|
pub fn to_dir(&self) -> Dir<'a, T> {
|
||||||
|
assert!(self.is_dir(), "Not a directory entry");
|
||||||
|
match self.first_cluster() {
|
||||||
|
Some(n) => {
|
||||||
|
let file = File::new(Some(n), Some(self.editor()), self.fs);
|
||||||
|
Dir::new(DirRawStream::File(file), self.fs)
|
||||||
|
},
|
||||||
|
None => self.fs.root_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file size or 0 for directory.
|
||||||
|
pub fn len(&self) -> u64 {
|
||||||
|
self.data.size as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file creation date and time.
|
||||||
|
///
|
||||||
|
/// Resolution of the time field is 1/100s.
|
||||||
|
pub fn created(&self) -> DateTime {
|
||||||
|
self.data.created()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file last access date.
|
||||||
|
pub fn accessed(&self) -> Date {
|
||||||
|
self.data.accessed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns file last modification date and time.
|
||||||
|
///
|
||||||
|
/// Resolution of the time field is 2s.
|
||||||
|
pub fn modified(&self) -> DateTime {
|
||||||
|
self.data.modified()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raw_short_name(&self) -> &[u8; 11] {
|
||||||
|
&self.data.name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub(crate) fn eq_name(&self, name: &str) -> bool {
|
||||||
|
let self_name = self.file_name();
|
||||||
|
let self_name_lowercase_iter = self_name.chars().flat_map(|c| c.to_uppercase());
|
||||||
|
let other_name_lowercase_iter = name.chars().flat_map(|c| c.to_uppercase());
|
||||||
|
let long_name_matches = self_name_lowercase_iter.eq(other_name_lowercase_iter);
|
||||||
|
let short_name_matches = self.short_name.eq_ignore_case(name, self.fs.options.oem_cp_converter);
|
||||||
|
long_name_matches || short_name_matches
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alloc"))]
|
||||||
|
pub(crate) fn eq_name(&self, name: &str) -> bool {
|
||||||
|
self.short_name.eq_ignore_case(name, self.fs.options.oem_cp_converter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> fmt::Debug for DirEntry<'a, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
self.data.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use fs::LOSSY_OEM_CP_CONVERTER;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_name_with_ext() {
|
||||||
|
let mut raw_short_name = [0u8; 11];
|
||||||
|
raw_short_name.copy_from_slice(b"FOO BAR");
|
||||||
|
assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.BAR");
|
||||||
|
raw_short_name.copy_from_slice(b"LOOK AT M E");
|
||||||
|
assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "LOOK AT.M E");
|
||||||
|
raw_short_name[0] = 0x99;
|
||||||
|
raw_short_name[10] = 0x99;
|
||||||
|
assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "\u{FFFD}OOK AT.M \u{FFFD}");
|
||||||
|
assert_eq!(
|
||||||
|
ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_name_without_ext() {
|
||||||
|
let mut raw_short_name = [0u8; 11];
|
||||||
|
raw_short_name.copy_from_slice(b"FOO ");
|
||||||
|
assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "FOO");
|
||||||
|
raw_short_name.copy_from_slice(b"LOOK AT ");
|
||||||
|
assert_eq!(ShortName::new(&raw_short_name).to_string(&LOSSY_OEM_CP_CONVERTER), "LOOK AT");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_name_eq_ignore_case() {
|
||||||
|
let mut raw_short_name = [0u8; 11];
|
||||||
|
raw_short_name.copy_from_slice(b"LOOK AT M E");
|
||||||
|
raw_short_name[0] = 0x99;
|
||||||
|
raw_short_name[10] = 0x99;
|
||||||
|
assert_eq!(
|
||||||
|
ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}OOK AT.M \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShortName::new(&raw_short_name).eq_ignore_case("\u{FFFD}ook AT.m \u{FFFD}", &LOSSY_OEM_CP_CONVERTER),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_name_05_changed_to_e5() {
|
||||||
|
let raw_short_name = [0x05; 11];
|
||||||
|
assert_eq!(
|
||||||
|
ShortName::new(&raw_short_name).as_bytes(),
|
||||||
|
[0xE5, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, b'.', 0x05, 0x05, 0x05]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lowercase_short_name() {
|
||||||
|
let mut raw_short_name = [0u8; 11];
|
||||||
|
raw_short_name.copy_from_slice(b"FOO RS ");
|
||||||
|
let mut raw_entry =
|
||||||
|
DirFileEntryData { name: raw_short_name, reserved_0: (1 << 3) | (1 << 4), ..Default::default() };
|
||||||
|
assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "foo.rs");
|
||||||
|
raw_entry.reserved_0 = 1 << 3;
|
||||||
|
assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "foo.RS");
|
||||||
|
raw_entry.reserved_0 = 1 << 4;
|
||||||
|
assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.rs");
|
||||||
|
raw_entry.reserved_0 = 0;
|
||||||
|
assert_eq!(raw_entry.lowercase_name().to_string(&LOSSY_OEM_CP_CONVERTER), "FOO.RS");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,370 @@
|
||||||
|
use core;
|
||||||
|
use core::cmp;
|
||||||
|
use io;
|
||||||
|
use io::prelude::*;
|
||||||
|
use io::{ErrorKind, SeekFrom};
|
||||||
|
|
||||||
|
use dir_entry::DirEntryEditor;
|
||||||
|
use fs::{FileSystem, ReadWriteSeek};
|
||||||
|
use time::{Date, DateTime};
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE: u32 = core::u32::MAX;
|
||||||
|
|
||||||
|
/// A FAT filesystem file object used for reading and writing data.
|
||||||
|
///
|
||||||
|
/// This struct is created by the `open_file` or `create_file` methods on `Dir`.
|
||||||
|
pub struct File<'a, T: ReadWriteSeek + 'a> {
|
||||||
|
// Note first_cluster is None if file is empty
|
||||||
|
first_cluster: Option<u32>,
|
||||||
|
// Note: if offset points between clusters current_cluster is the previous cluster
|
||||||
|
current_cluster: Option<u32>,
|
||||||
|
// current position in this file
|
||||||
|
offset: u32,
|
||||||
|
// file dir entry editor - None for root dir
|
||||||
|
entry: Option<DirEntryEditor>,
|
||||||
|
// file-system reference
|
||||||
|
fs: &'a FileSystem<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> File<'a, T> {
|
||||||
|
pub(crate) fn new(first_cluster: Option<u32>, entry: Option<DirEntryEditor>, fs: &'a FileSystem<T>) -> Self {
|
||||||
|
File {
|
||||||
|
first_cluster,
|
||||||
|
entry,
|
||||||
|
fs,
|
||||||
|
current_cluster: None, // cluster before first one
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_dir_entry_after_write(&mut self) {
|
||||||
|
let offset = self.offset;
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
let now = self.fs.options.time_provider.get_current_date_time();
|
||||||
|
e.set_modified(now);
|
||||||
|
if e.inner().size().map_or(false, |s| offset > s) {
|
||||||
|
e.set_size(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate file in current position.
|
||||||
|
pub fn truncate(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.set_size(self.offset);
|
||||||
|
if self.offset == 0 {
|
||||||
|
e.set_first_cluster(None, self.fs.fat_type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.offset > 0 {
|
||||||
|
debug_assert!(self.current_cluster.is_some());
|
||||||
|
// if offset is not 0 current cluster cannot be empty
|
||||||
|
self.fs.truncate_cluster_chain(self.current_cluster.unwrap()) // SAFE
|
||||||
|
} else {
|
||||||
|
debug_assert!(self.current_cluster.is_none());
|
||||||
|
if let Some(n) = self.first_cluster {
|
||||||
|
self.fs.free_cluster_chain(n)?;
|
||||||
|
self.first_cluster = None;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn abs_pos(&self) -> Option<u64> {
|
||||||
|
// Returns current position relative to filesystem start
|
||||||
|
// Note: when between clusters it returns position after previous cluster
|
||||||
|
match self.current_cluster {
|
||||||
|
Some(n) => {
|
||||||
|
let cluster_size = self.fs.cluster_size();
|
||||||
|
let offset_mod_cluster_size = self.offset % cluster_size;
|
||||||
|
let offset_in_cluster = if offset_mod_cluster_size == 0 {
|
||||||
|
// position points between clusters - we are returning previous cluster so
|
||||||
|
// offset must be set to the cluster size
|
||||||
|
cluster_size
|
||||||
|
} else {
|
||||||
|
offset_mod_cluster_size
|
||||||
|
};
|
||||||
|
let offset_in_fs = self.fs.offset_from_cluster(n) + u64::from(offset_in_cluster);
|
||||||
|
Some(offset_in_fs)
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_dir_entry(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.flush(self.fs)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets date and time of creation for this file.
|
||||||
|
///
|
||||||
|
/// Note: it is set to a value from the `TimeProvider` when creating a file.
|
||||||
|
/// Deprecated: if needed implement a custom `TimeProvider`.
|
||||||
|
#[deprecated]
|
||||||
|
pub fn set_created(&mut self, date_time: DateTime) {
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.set_created(date_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets date of last access for this file.
|
||||||
|
///
|
||||||
|
/// Note: it is overwritten by a value from the `TimeProvider` on every file read operation.
|
||||||
|
/// Deprecated: if needed implement a custom `TimeProvider`.
|
||||||
|
#[deprecated]
|
||||||
|
pub fn set_accessed(&mut self, date: Date) {
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.set_accessed(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets date and time of last modification for this file.
|
||||||
|
///
|
||||||
|
/// Note: it is overwritten by a value from the `TimeProvider` on every file write operation.
|
||||||
|
/// Deprecated: if needed implement a custom `TimeProvider`.
|
||||||
|
#[deprecated]
|
||||||
|
pub fn set_modified(&mut self, date_time: DateTime) {
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.set_modified(date_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Option<u32> {
|
||||||
|
match self.entry {
|
||||||
|
Some(ref e) => e.inner().size(),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_dir(&self) -> bool {
|
||||||
|
match self.entry {
|
||||||
|
Some(ref e) => e.inner().is_dir(),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_left_in_file(&self) -> Option<usize> {
|
||||||
|
// Note: seeking beyond end of file is not allowed so overflow is impossible
|
||||||
|
self.size().map(|s| (s - self.offset) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_first_cluster(&mut self, cluster: u32) {
|
||||||
|
self.first_cluster = Some(cluster);
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
e.set_first_cluster(self.first_cluster, self.fs.fat_type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_cluster(&self) -> Option<u32> {
|
||||||
|
self.first_cluster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> Drop for File<'a, T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = self.flush() {
|
||||||
|
error!("flush failed {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925
|
||||||
|
impl<'a, T: ReadWriteSeek> Clone for File<'a, T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
File {
|
||||||
|
first_cluster: self.first_cluster,
|
||||||
|
current_cluster: self.current_cluster,
|
||||||
|
offset: self.offset,
|
||||||
|
entry: self.entry.clone(),
|
||||||
|
fs: self.fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> Read for File<'a, T> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let cluster_size = self.fs.cluster_size();
|
||||||
|
let current_cluster_opt = if self.offset % cluster_size == 0 {
|
||||||
|
// next cluster
|
||||||
|
match self.current_cluster {
|
||||||
|
None => self.first_cluster,
|
||||||
|
Some(n) => {
|
||||||
|
let r = self.fs.cluster_iter(n).next();
|
||||||
|
match r {
|
||||||
|
Some(Err(err)) => return Err(err),
|
||||||
|
Some(Ok(n)) => Some(n),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.current_cluster
|
||||||
|
};
|
||||||
|
let current_cluster = match current_cluster_opt {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return Ok(0),
|
||||||
|
};
|
||||||
|
let offset_in_cluster = self.offset % cluster_size;
|
||||||
|
let bytes_left_in_cluster = (cluster_size - offset_in_cluster) as usize;
|
||||||
|
let bytes_left_in_file = self.bytes_left_in_file().unwrap_or(bytes_left_in_cluster);
|
||||||
|
let read_size = cmp::min(cmp::min(buf.len(), bytes_left_in_cluster), bytes_left_in_file);
|
||||||
|
if read_size == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
trace!("read {} bytes in cluster {}", read_size, current_cluster);
|
||||||
|
let offset_in_fs = self.fs.offset_from_cluster(current_cluster) + (offset_in_cluster as u64);
|
||||||
|
let read_bytes = {
|
||||||
|
let mut disk = self.fs.disk.borrow_mut();
|
||||||
|
disk.seek(SeekFrom::Start(offset_in_fs))?;
|
||||||
|
disk.read(&mut buf[..read_size])?
|
||||||
|
};
|
||||||
|
if read_bytes == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
self.offset += read_bytes as u32;
|
||||||
|
self.current_cluster = Some(current_cluster);
|
||||||
|
|
||||||
|
if let Some(ref mut e) = self.entry {
|
||||||
|
if self.fs.options.update_accessed_date {
|
||||||
|
let now = self.fs.options.time_provider.get_current_date();
|
||||||
|
e.set_accessed(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(read_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> Write for File<'a, T> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let cluster_size = self.fs.cluster_size();
|
||||||
|
let offset_in_cluster = self.offset % cluster_size;
|
||||||
|
let bytes_left_in_cluster = (cluster_size - offset_in_cluster) as usize;
|
||||||
|
let bytes_left_until_max_file_size = (MAX_FILE_SIZE - self.offset) as usize;
|
||||||
|
let write_size = cmp::min(buf.len(), bytes_left_in_cluster);
|
||||||
|
let write_size = cmp::min(write_size, bytes_left_until_max_file_size);
|
||||||
|
// Exit early if we are going to write no data
|
||||||
|
if write_size == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
// Mark the volume 'dirty'
|
||||||
|
self.fs.set_dirty_flag(true)?;
|
||||||
|
// Get cluster for write possibly allocating new one
|
||||||
|
let current_cluster = if self.offset % cluster_size == 0 {
|
||||||
|
// next cluster
|
||||||
|
let next_cluster = match self.current_cluster {
|
||||||
|
None => self.first_cluster,
|
||||||
|
Some(n) => {
|
||||||
|
let r = self.fs.cluster_iter(n).next();
|
||||||
|
match r {
|
||||||
|
Some(Err(err)) => return Err(err),
|
||||||
|
Some(Ok(n)) => Some(n),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match next_cluster {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
// end of chain reached - allocate new cluster
|
||||||
|
let new_cluster = self.fs.alloc_cluster(self.current_cluster, self.is_dir())?;
|
||||||
|
trace!("allocated cluser {}", new_cluster);
|
||||||
|
if self.first_cluster.is_none() {
|
||||||
|
self.set_first_cluster(new_cluster);
|
||||||
|
}
|
||||||
|
new_cluster
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// self.current_cluster should be a valid cluster
|
||||||
|
match self.current_cluster {
|
||||||
|
Some(n) => n,
|
||||||
|
None => panic!("Offset inside cluster but no cluster allocated"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
trace!("write {} bytes in cluster {}", write_size, current_cluster);
|
||||||
|
let offset_in_fs = self.fs.offset_from_cluster(current_cluster) + (offset_in_cluster as u64);
|
||||||
|
let written_bytes = {
|
||||||
|
let mut disk = self.fs.disk.borrow_mut();
|
||||||
|
disk.seek(SeekFrom::Start(offset_in_fs))?;
|
||||||
|
disk.write(&buf[..write_size])?
|
||||||
|
};
|
||||||
|
if written_bytes == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
// some bytes were writter - update position and optionally size
|
||||||
|
self.offset += written_bytes as u32;
|
||||||
|
self.current_cluster = Some(current_cluster);
|
||||||
|
self.update_dir_entry_after_write();
|
||||||
|
Ok(written_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.flush_dir_entry()?;
|
||||||
|
let mut disk = self.fs.disk.borrow_mut();
|
||||||
|
disk.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ReadWriteSeek> Seek for File<'a, T> {
|
||||||
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
|
let mut new_pos = match pos {
|
||||||
|
SeekFrom::Current(x) => self.offset as i64 + x,
|
||||||
|
SeekFrom::Start(x) => x as i64,
|
||||||
|
SeekFrom::End(x) => {
|
||||||
|
let size = self.size().expect("cannot seek from end if size is unknown") as i64;
|
||||||
|
size + x
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if new_pos < 0 {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidInput, "Seek to a negative offset"));
|
||||||
|
}
|
||||||
|
if let Some(s) = self.size() {
|
||||||
|
if new_pos > s as i64 {
|
||||||
|
info!("seek beyond end of file");
|
||||||
|
new_pos = s as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut new_pos = new_pos as u32;
|
||||||
|
trace!("file seek {} -> {} - entry {:?}", self.offset, new_pos, self.entry);
|
||||||
|
if new_pos == self.offset {
|
||||||
|
// position is the same - nothing to do
|
||||||
|
return Ok(self.offset as u64);
|
||||||
|
}
|
||||||
|
// get number of clusters to seek (favoring previous cluster in corner case)
|
||||||
|
let cluster_count = (self.fs.clusters_from_bytes(new_pos as u64) as i32 - 1) as isize;
|
||||||
|
let old_cluster_count = (self.fs.clusters_from_bytes(self.offset as u64) as i32 - 1) as isize;
|
||||||
|
let new_cluster = if new_pos == 0 {
|
||||||
|
None
|
||||||
|
} else if cluster_count == old_cluster_count {
|
||||||
|
self.current_cluster
|
||||||
|
} else {
|
||||||
|
match self.first_cluster {
|
||||||
|
Some(n) => {
|
||||||
|
let mut cluster = n;
|
||||||
|
let mut iter = self.fs.cluster_iter(n);
|
||||||
|
for i in 0..cluster_count {
|
||||||
|
cluster = match iter.next() {
|
||||||
|
Some(r) => r?,
|
||||||
|
None => {
|
||||||
|
// chain ends before new position - seek to end of last cluster
|
||||||
|
new_pos = self.fs.bytes_from_clusters((i + 1) as u32) as u32;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(cluster)
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// empty file - always seek to 0
|
||||||
|
new_pos = 0;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.offset = new_pos as u32;
|
||||||
|
self.current_cluster = new_cluster;
|
||||||
|
Ok(self.offset as u64)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
//! A FAT filesystem library implemented in Rust.
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! This crate is [on crates.io](https://crates.io/crates/fatfs) and can be
|
||||||
|
//! used by adding `fatfs` to the dependencies in your project's `Cargo.toml`.
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! [dependencies]
|
||||||
|
//! fatfs = "0.3"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! And this in your crate root:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! extern crate fatfs;
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! // Declare external crates
|
||||||
|
//! // Note: `fscommon` crate is used to speedup IO operations
|
||||||
|
//! extern crate fatfs;
|
||||||
|
//! extern crate fscommon;
|
||||||
|
//!
|
||||||
|
//! use std::io::prelude::*;
|
||||||
|
//!
|
||||||
|
//! fn main() -> std::io::Result<()> {
|
||||||
|
//! # std::fs::copy("resources/fat16.img", "tmp/fat.img")?;
|
||||||
|
//! // Initialize a filesystem object
|
||||||
|
//! let img_file = std::fs::OpenOptions::new().read(true).write(true)
|
||||||
|
//! .open("tmp/fat.img")?;
|
||||||
|
//! let buf_stream = fscommon::BufStream::new(img_file);
|
||||||
|
//! let fs = fatfs::FileSystem::new(buf_stream, fatfs::FsOptions::new())?;
|
||||||
|
//! let root_dir = fs.root_dir();
|
||||||
|
//!
|
||||||
|
//! // Write a file
|
||||||
|
//! root_dir.create_dir("foo")?;
|
||||||
|
//! let mut file = root_dir.create_file("foo/hello.txt")?;
|
||||||
|
//! file.truncate()?;
|
||||||
|
//! file.write_all(b"Hello World!")?;
|
||||||
|
//!
|
||||||
|
//! // Read a directory
|
||||||
|
//! let dir = root_dir.open_dir("foo")?;
|
||||||
|
//! for r in dir.iter() {
|
||||||
|
//! let entry = r?;
|
||||||
|
//! println!("{}", entry.file_name());
|
||||||
|
//! }
|
||||||
|
//! # std::fs::remove_file("tmp/fat.img")?;
|
||||||
|
//! # Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![crate_type = "lib"]
|
||||||
|
#![crate_name = "fatfs"]
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
#![cfg_attr(all(not(feature = "std"), feature = "alloc"), feature(alloc))]
|
||||||
|
// Disable warnings to not clutter code with cfg too much
|
||||||
|
#![cfg_attr(not(feature = "alloc"), allow(dead_code, unused_imports))]
|
||||||
|
// Inclusive ranges requires Rust 1.26.0
|
||||||
|
#![allow(ellipsis_inclusive_range_patterns)]
|
||||||
|
// `dyn` syntax requires Rust 1.27.0
|
||||||
|
#![allow(bare_trait_objects)]
|
||||||
|
// `alloc` compiler feature is needed in Rust before 1.36
|
||||||
|
#![cfg_attr(all(not(feature = "std"), feature = "alloc"), allow(stable_features))]
|
||||||
|
|
||||||
|
extern crate byteorder;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate bitflags;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
extern crate chrono;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
extern crate core_io;
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "std"), feature = "alloc"))]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
mod boot_sector;
|
||||||
|
mod dir;
|
||||||
|
mod dir_entry;
|
||||||
|
mod file;
|
||||||
|
mod fs;
|
||||||
|
mod table;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
mod byteorder_core_io;
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use byteorder as byteorder_ext;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use byteorder_core_io as byteorder_ext;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use core_io as io;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std as core;
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
pub use dir::*;
|
||||||
|
pub use dir_entry::*;
|
||||||
|
pub use file::*;
|
||||||
|
pub use fs::*;
|
||||||
|
pub use time::*;
|
|
@ -0,0 +1,564 @@
|
||||||
|
use byteorder::LittleEndian;
|
||||||
|
use byteorder_ext::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use core::cmp;
|
||||||
|
use io;
|
||||||
|
|
||||||
|
use fs::{FatType, FsStatusFlags, ReadSeek, ReadWriteSeek};
|
||||||
|
|
||||||
|
struct Fat<T> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
dummy: [T; 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fat12 = Fat<u8>;
|
||||||
|
type Fat16 = Fat<u16>;
|
||||||
|
type Fat32 = Fat<u32>;
|
||||||
|
|
||||||
|
pub const RESERVED_FAT_ENTRIES: u32 = 2;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
enum FatValue {
|
||||||
|
Free,
|
||||||
|
Data(u32),
|
||||||
|
Bad,
|
||||||
|
EndOfChain,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait FatTrait {
|
||||||
|
fn get_raw<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<u32>;
|
||||||
|
fn get<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<FatValue>;
|
||||||
|
fn set_raw<T: ReadWriteSeek>(fat: &mut T, cluster: u32, raw_value: u32) -> io::Result<()>;
|
||||||
|
fn set<T: ReadWriteSeek>(fat: &mut T, cluster: u32, value: FatValue) -> io::Result<()>;
|
||||||
|
fn find_free<T: ReadSeek>(fat: &mut T, start_cluster: u32, end_cluster: u32) -> io::Result<u32>;
|
||||||
|
fn count_free<T: ReadSeek>(fat: &mut T, end_cluster: u32) -> io::Result<u32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_fat<T: ReadSeek>(fat: &mut T, fat_type: FatType, cluster: u32) -> io::Result<FatValue> {
|
||||||
|
match fat_type {
|
||||||
|
FatType::Fat12 => Fat12::get(fat, cluster),
|
||||||
|
FatType::Fat16 => Fat16::get(fat, cluster),
|
||||||
|
FatType::Fat32 => Fat32::get(fat, cluster),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_fat<T: ReadWriteSeek>(fat: &mut T, fat_type: FatType, cluster: u32, value: FatValue) -> io::Result<()> {
|
||||||
|
trace!("write FAT - cluster {} value {:?}", cluster, value);
|
||||||
|
match fat_type {
|
||||||
|
FatType::Fat12 => Fat12::set(fat, cluster, value),
|
||||||
|
FatType::Fat16 => Fat16::set(fat, cluster, value),
|
||||||
|
FatType::Fat32 => Fat32::set(fat, cluster, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next_cluster<T: ReadSeek>(fat: &mut T, fat_type: FatType, cluster: u32) -> io::Result<Option<u32>> {
|
||||||
|
let val = read_fat(fat, fat_type, cluster)?;
|
||||||
|
match val {
|
||||||
|
FatValue::Data(n) => Ok(Some(n)),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_free_cluster<T: ReadSeek>(
|
||||||
|
fat: &mut T,
|
||||||
|
fat_type: FatType,
|
||||||
|
start_cluster: u32,
|
||||||
|
end_cluster: u32,
|
||||||
|
) -> io::Result<u32> {
|
||||||
|
match fat_type {
|
||||||
|
FatType::Fat12 => Fat12::find_free(fat, start_cluster, end_cluster),
|
||||||
|
FatType::Fat16 => Fat16::find_free(fat, start_cluster, end_cluster),
|
||||||
|
FatType::Fat32 => Fat32::find_free(fat, start_cluster, end_cluster),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn alloc_cluster<T: ReadWriteSeek>(
|
||||||
|
fat: &mut T,
|
||||||
|
fat_type: FatType,
|
||||||
|
prev_cluster: Option<u32>,
|
||||||
|
hint: Option<u32>,
|
||||||
|
total_clusters: u32,
|
||||||
|
) -> io::Result<u32> {
|
||||||
|
let end_cluster = total_clusters + RESERVED_FAT_ENTRIES;
|
||||||
|
let start_cluster = match hint {
|
||||||
|
Some(n) if n < end_cluster => n,
|
||||||
|
_ => RESERVED_FAT_ENTRIES,
|
||||||
|
};
|
||||||
|
let new_cluster = match find_free_cluster(fat, fat_type, start_cluster, end_cluster) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) if start_cluster > RESERVED_FAT_ENTRIES => {
|
||||||
|
find_free_cluster(fat, fat_type, RESERVED_FAT_ENTRIES, start_cluster)?
|
||||||
|
},
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
write_fat(fat, fat_type, new_cluster, FatValue::EndOfChain)?;
|
||||||
|
if let Some(n) = prev_cluster {
|
||||||
|
write_fat(fat, fat_type, n, FatValue::Data(new_cluster))?;
|
||||||
|
}
|
||||||
|
trace!("allocated cluster {}", new_cluster);
|
||||||
|
Ok(new_cluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_fat_flags<T: ReadSeek>(fat: &mut T, fat_type: FatType) -> io::Result<FsStatusFlags> {
|
||||||
|
// check MSB (except in FAT12)
|
||||||
|
let val = match fat_type {
|
||||||
|
FatType::Fat12 => 0xFFF,
|
||||||
|
FatType::Fat16 => Fat16::get_raw(fat, 1)?,
|
||||||
|
FatType::Fat32 => Fat32::get_raw(fat, 1)?,
|
||||||
|
};
|
||||||
|
let dirty = match fat_type {
|
||||||
|
FatType::Fat12 => false,
|
||||||
|
FatType::Fat16 => val & (1 << 15) == 0,
|
||||||
|
FatType::Fat32 => val & (1 << 27) == 0,
|
||||||
|
};
|
||||||
|
let io_error = match fat_type {
|
||||||
|
FatType::Fat12 => false,
|
||||||
|
FatType::Fat16 => val & (1 << 14) == 0,
|
||||||
|
FatType::Fat32 => val & (1 << 26) == 0,
|
||||||
|
};
|
||||||
|
Ok(FsStatusFlags { dirty, io_error })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn count_free_clusters<T: ReadSeek>(fat: &mut T, fat_type: FatType, total_clusters: u32) -> io::Result<u32> {
|
||||||
|
let end_cluster = total_clusters + RESERVED_FAT_ENTRIES;
|
||||||
|
match fat_type {
|
||||||
|
FatType::Fat12 => Fat12::count_free(fat, end_cluster),
|
||||||
|
FatType::Fat16 => Fat16::count_free(fat, end_cluster),
|
||||||
|
FatType::Fat32 => Fat32::count_free(fat, end_cluster),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_fat<T: ReadWriteSeek>(
|
||||||
|
fat: &mut T,
|
||||||
|
fat_type: FatType,
|
||||||
|
media: u8,
|
||||||
|
bytes_per_fat: u64,
|
||||||
|
total_clusters: u32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
// init first two reserved entries to FAT ID
|
||||||
|
match fat_type {
|
||||||
|
FatType::Fat12 => {
|
||||||
|
fat.write_u8(media)?;
|
||||||
|
fat.write_u16::<LittleEndian>(0xFFFF)?;
|
||||||
|
},
|
||||||
|
FatType::Fat16 => {
|
||||||
|
fat.write_u16::<LittleEndian>(media as u16 | 0xFF00)?;
|
||||||
|
fat.write_u16::<LittleEndian>(0xFFFF)?;
|
||||||
|
},
|
||||||
|
FatType::Fat32 => {
|
||||||
|
fat.write_u32::<LittleEndian>(media as u32 | 0xFFFFF00)?;
|
||||||
|
fat.write_u32::<LittleEndian>(0xFFFFFFFF)?;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// mark entries at the end of FAT as used (after FAT but before sector end)
|
||||||
|
const BITS_PER_BYTE: u64 = 8;
|
||||||
|
let start_cluster = total_clusters + RESERVED_FAT_ENTRIES;
|
||||||
|
let end_cluster = (bytes_per_fat * BITS_PER_BYTE / fat_type.bits_per_fat_entry() as u64) as u32;
|
||||||
|
for cluster in start_cluster..end_cluster {
|
||||||
|
write_fat(fat, fat_type, cluster, FatValue::EndOfChain)?;
|
||||||
|
}
|
||||||
|
// mark special entries 0x0FFFFFF0 - 0x0FFFFFFF as BAD if they exists on FAT32 volume
|
||||||
|
if end_cluster > 0x0FFFFFF0 {
|
||||||
|
let end_bad_cluster = cmp::min(0x0FFFFFFF + 1, end_cluster);
|
||||||
|
for cluster in 0x0FFFFFF0..end_bad_cluster {
|
||||||
|
write_fat(fat, fat_type, cluster, FatValue::Bad)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FatTrait for Fat12 {
|
||||||
|
fn get_raw<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<u32> {
|
||||||
|
let fat_offset = cluster + (cluster / 2);
|
||||||
|
fat.seek(io::SeekFrom::Start(fat_offset as u64))?;
|
||||||
|
let packed_val = fat.read_u16::<LittleEndian>()?;
|
||||||
|
Ok(match cluster & 1 {
|
||||||
|
0 => packed_val & 0x0FFF,
|
||||||
|
_ => packed_val >> 4,
|
||||||
|
} as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<FatValue> {
|
||||||
|
let val = Self::get_raw(fat, cluster)?;
|
||||||
|
Ok(match val {
|
||||||
|
0 => FatValue::Free,
|
||||||
|
0xFF7 => FatValue::Bad,
|
||||||
|
0xFF8...0xFFF => FatValue::EndOfChain,
|
||||||
|
n => FatValue::Data(n as u32),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set<T: ReadWriteSeek>(fat: &mut T, cluster: u32, value: FatValue) -> io::Result<()> {
|
||||||
|
let raw_val = match value {
|
||||||
|
FatValue::Free => 0,
|
||||||
|
FatValue::Bad => 0xFF7,
|
||||||
|
FatValue::EndOfChain => 0xFFF,
|
||||||
|
FatValue::Data(n) => n as u16,
|
||||||
|
};
|
||||||
|
Self::set_raw(fat, cluster, raw_val as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_raw<T: ReadWriteSeek>(fat: &mut T, cluster: u32, raw_val: u32) -> io::Result<()> {
|
||||||
|
let fat_offset = cluster + (cluster / 2);
|
||||||
|
fat.seek(io::SeekFrom::Start(fat_offset as u64))?;
|
||||||
|
let old_packed = fat.read_u16::<LittleEndian>()?;
|
||||||
|
fat.seek(io::SeekFrom::Start(fat_offset as u64))?;
|
||||||
|
let new_packed = match cluster & 1 {
|
||||||
|
0 => (old_packed & 0xF000) | raw_val as u16,
|
||||||
|
_ => (old_packed & 0x000F) | ((raw_val as u16) << 4),
|
||||||
|
};
|
||||||
|
fat.write_u16::<LittleEndian>(new_packed)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_free<T: ReadSeek>(fat: &mut T, start_cluster: u32, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut cluster = start_cluster;
|
||||||
|
let fat_offset = cluster + (cluster / 2);
|
||||||
|
fat.seek(io::SeekFrom::Start(fat_offset as u64))?;
|
||||||
|
let mut packed_val = fat.read_u16::<LittleEndian>()?;
|
||||||
|
loop {
|
||||||
|
let val = match cluster & 1 {
|
||||||
|
0 => packed_val & 0x0FFF,
|
||||||
|
_ => packed_val >> 4,
|
||||||
|
};
|
||||||
|
if val == 0 {
|
||||||
|
return Ok(cluster);
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
if cluster == end_cluster {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::Other, "No space left on device"));
|
||||||
|
}
|
||||||
|
packed_val = match cluster & 1 {
|
||||||
|
0 => fat.read_u16::<LittleEndian>()?,
|
||||||
|
_ => {
|
||||||
|
let next_byte = fat.read_u8()? as u16;
|
||||||
|
(packed_val >> 8) | (next_byte << 8)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_free<T: ReadSeek>(fat: &mut T, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut cluster = RESERVED_FAT_ENTRIES;
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 3 / 2) as u64))?;
|
||||||
|
let mut prev_packed_val = 0u16;
|
||||||
|
while cluster < end_cluster {
|
||||||
|
let res = match cluster & 1 {
|
||||||
|
0 => fat.read_u16::<LittleEndian>(),
|
||||||
|
_ => fat.read_u8().map(|n| n as u16),
|
||||||
|
};
|
||||||
|
let packed_val = match res {
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
Ok(n) => n,
|
||||||
|
};
|
||||||
|
let val = match cluster & 1 {
|
||||||
|
0 => packed_val & 0x0FFF,
|
||||||
|
_ => (packed_val << 8) | (prev_packed_val >> 12),
|
||||||
|
};
|
||||||
|
prev_packed_val = packed_val;
|
||||||
|
if val == 0 {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FatTrait for Fat16 {
|
||||||
|
fn get_raw<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<u32> {
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 2) as u64))?;
|
||||||
|
Ok(fat.read_u16::<LittleEndian>()? as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<FatValue> {
|
||||||
|
let val = Self::get_raw(fat, cluster)?;
|
||||||
|
Ok(match val {
|
||||||
|
0 => FatValue::Free,
|
||||||
|
0xFFF7 => FatValue::Bad,
|
||||||
|
0xFFF8...0xFFFF => FatValue::EndOfChain,
|
||||||
|
n => FatValue::Data(n as u32),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_raw<T: ReadWriteSeek>(fat: &mut T, cluster: u32, raw_value: u32) -> io::Result<()> {
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 2) as u64))?;
|
||||||
|
fat.write_u16::<LittleEndian>(raw_value as u16)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set<T: ReadWriteSeek>(fat: &mut T, cluster: u32, value: FatValue) -> io::Result<()> {
|
||||||
|
let raw_value = match value {
|
||||||
|
FatValue::Free => 0,
|
||||||
|
FatValue::Bad => 0xFFF7,
|
||||||
|
FatValue::EndOfChain => 0xFFFF,
|
||||||
|
FatValue::Data(n) => n as u16,
|
||||||
|
};
|
||||||
|
Self::set_raw(fat, cluster, raw_value as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_free<T: ReadSeek>(fat: &mut T, start_cluster: u32, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut cluster = start_cluster;
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 2) as u64))?;
|
||||||
|
while cluster < end_cluster {
|
||||||
|
let val = fat.read_u16::<LittleEndian>()?;
|
||||||
|
if val == 0 {
|
||||||
|
return Ok(cluster);
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
}
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "No space left on device"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_free<T: ReadSeek>(fat: &mut T, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut cluster = RESERVED_FAT_ENTRIES;
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 2) as u64))?;
|
||||||
|
while cluster < end_cluster {
|
||||||
|
let val = fat.read_u16::<LittleEndian>()?;
|
||||||
|
if val == 0 {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FatTrait for Fat32 {
|
||||||
|
fn get_raw<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<u32> {
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 4) as u64))?;
|
||||||
|
Ok(fat.read_u32::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get<T: ReadSeek>(fat: &mut T, cluster: u32) -> io::Result<FatValue> {
|
||||||
|
let val = Self::get_raw(fat, cluster)? & 0x0FFFFFFF;
|
||||||
|
Ok(match val {
|
||||||
|
0 if cluster >= 0x0FFFFFF7 && cluster <= 0x0FFFFFFF => {
|
||||||
|
let tmp = if cluster == 0x0FFFFFF7 { "BAD_CLUSTER" } else { "end-of-chain" };
|
||||||
|
warn!(
|
||||||
|
"cluster number {} is a special value in FAT to indicate {}; it should never be seen as free",
|
||||||
|
cluster, tmp
|
||||||
|
);
|
||||||
|
FatValue::Bad // avoid accidental use or allocation into a FAT chain
|
||||||
|
},
|
||||||
|
0 => FatValue::Free,
|
||||||
|
0x0FFFFFF7 => FatValue::Bad,
|
||||||
|
0x0FFFFFF8...0x0FFFFFFF => FatValue::EndOfChain,
|
||||||
|
n if cluster >= 0x0FFFFFF7 && cluster <= 0x0FFFFFFF => {
|
||||||
|
let tmp = if cluster == 0x0FFFFFF7 { "BAD_CLUSTER" } else { "end-of-chain" };
|
||||||
|
warn!("cluster number {} is a special value in FAT to indicate {}; hiding potential FAT chain value {} and instead reporting as a bad sector", cluster, tmp, n);
|
||||||
|
FatValue::Bad // avoid accidental use or allocation into a FAT chain
|
||||||
|
},
|
||||||
|
n => FatValue::Data(n as u32),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_raw<T: ReadWriteSeek>(fat: &mut T, cluster: u32, raw_value: u32) -> io::Result<()> {
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 4) as u64))?;
|
||||||
|
fat.write_u32::<LittleEndian>(raw_value)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set<T: ReadWriteSeek>(fat: &mut T, cluster: u32, value: FatValue) -> io::Result<()> {
|
||||||
|
let old_reserved_bits = Self::get_raw(fat, cluster)? & 0xF0000000;
|
||||||
|
|
||||||
|
if value == FatValue::Free && cluster >= 0x0FFFFFF7 && cluster <= 0x0FFFFFFF {
|
||||||
|
// NOTE: it is technically allowed for them to store FAT chain loops,
|
||||||
|
// or even have them all store value '4' as their next cluster.
|
||||||
|
// Some believe only FatValue::Bad should be allowed for this edge case.
|
||||||
|
let tmp = if cluster == 0x0FFFFFF7 { "BAD_CLUSTER" } else { "end-of-chain" };
|
||||||
|
panic!(
|
||||||
|
"cluster number {} is a special value in FAT to indicate {}; it should never be set as free",
|
||||||
|
cluster, tmp
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let raw_val = match value {
|
||||||
|
FatValue::Free => 0,
|
||||||
|
FatValue::Bad => 0x0FFFFFF7,
|
||||||
|
FatValue::EndOfChain => 0x0FFFFFFF,
|
||||||
|
FatValue::Data(n) => n,
|
||||||
|
};
|
||||||
|
let raw_val = raw_val | old_reserved_bits; // must preserve original reserved values
|
||||||
|
Self::set_raw(fat, cluster, raw_val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_free<T: ReadSeek>(fat: &mut T, start_cluster: u32, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut cluster = start_cluster;
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 4) as u64))?;
|
||||||
|
while cluster < end_cluster {
|
||||||
|
let val = fat.read_u32::<LittleEndian>()? & 0x0FFFFFFF;
|
||||||
|
if val == 0 {
|
||||||
|
return Ok(cluster);
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
}
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "No space left on device"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_free<T: ReadSeek>(fat: &mut T, end_cluster: u32) -> io::Result<u32> {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut cluster = RESERVED_FAT_ENTRIES;
|
||||||
|
fat.seek(io::SeekFrom::Start((cluster * 4) as u64))?;
|
||||||
|
while cluster < end_cluster {
|
||||||
|
let val = fat.read_u32::<LittleEndian>()? & 0x0FFFFFFF;
|
||||||
|
if val == 0 {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
cluster += 1;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ClusterIterator<T: ReadWriteSeek> {
|
||||||
|
fat: T,
|
||||||
|
fat_type: FatType,
|
||||||
|
cluster: Option<u32>,
|
||||||
|
err: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ReadWriteSeek> ClusterIterator<T> {
|
||||||
|
pub(crate) fn new(fat: T, fat_type: FatType, cluster: u32) -> Self {
|
||||||
|
ClusterIterator { fat, fat_type, cluster: Some(cluster), err: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn truncate(&mut self) -> io::Result<u32> {
|
||||||
|
match self.cluster {
|
||||||
|
Some(n) => {
|
||||||
|
// Move to the next cluster
|
||||||
|
self.next();
|
||||||
|
// Mark previous cluster as end of chain
|
||||||
|
write_fat(&mut self.fat, self.fat_type, n, FatValue::EndOfChain)?;
|
||||||
|
// Free rest of chain
|
||||||
|
self.free()
|
||||||
|
},
|
||||||
|
None => Ok(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn free(&mut self) -> io::Result<u32> {
|
||||||
|
let mut num_free = 0;
|
||||||
|
while let Some(n) = self.cluster {
|
||||||
|
self.next();
|
||||||
|
write_fat(&mut self.fat, self.fat_type, n, FatValue::Free)?;
|
||||||
|
num_free += 1;
|
||||||
|
}
|
||||||
|
Ok(num_free)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ReadWriteSeek> Iterator for ClusterIterator<T> {
|
||||||
|
type Item = io::Result<u32>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.err {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Some(current_cluster) = self.cluster {
|
||||||
|
self.cluster = match get_next_cluster(&mut self.fat, self.fat_type, current_cluster) {
|
||||||
|
Ok(next_cluster) => next_cluster,
|
||||||
|
Err(err) => {
|
||||||
|
self.err = true;
|
||||||
|
return Some(Err(err));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.cluster.map(Ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_fat<T: ReadWriteSeek>(fat_type: FatType, mut cur: T) {
|
||||||
|
// based on cluster maps from Wikipedia:
|
||||||
|
// https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system#Cluster_map
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 1).unwrap(), FatValue::EndOfChain);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 4).unwrap(), FatValue::Data(5));
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 5).unwrap(), FatValue::Data(6));
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 8).unwrap(), FatValue::EndOfChain);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 9).unwrap(), FatValue::Data(0xA));
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0xA).unwrap(), FatValue::Data(0x14));
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x12).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x17).unwrap(), FatValue::Bad);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x18).unwrap(), FatValue::Bad);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x1B).unwrap(), FatValue::Free);
|
||||||
|
|
||||||
|
assert_eq!(find_free_cluster(&mut cur, fat_type, 2, 0x20).unwrap(), 0x12);
|
||||||
|
assert_eq!(find_free_cluster(&mut cur, fat_type, 0x12, 0x20).unwrap(), 0x12);
|
||||||
|
assert_eq!(find_free_cluster(&mut cur, fat_type, 0x13, 0x20).unwrap(), 0x1B);
|
||||||
|
assert!(find_free_cluster(&mut cur, fat_type, 0x13, 0x14).is_err());
|
||||||
|
|
||||||
|
assert_eq!(count_free_clusters(&mut cur, fat_type, 0x1E).unwrap(), 5);
|
||||||
|
|
||||||
|
// test allocation
|
||||||
|
assert_eq!(alloc_cluster(&mut cur, fat_type, None, Some(0x13), 0x1E).unwrap(), 0x1B);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x1B).unwrap(), FatValue::EndOfChain);
|
||||||
|
assert_eq!(alloc_cluster(&mut cur, fat_type, Some(0x1B), None, 0x1E).unwrap(), 0x12);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x1B).unwrap(), FatValue::Data(0x12));
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x12).unwrap(), FatValue::EndOfChain);
|
||||||
|
assert_eq!(count_free_clusters(&mut cur, fat_type, 0x1E).unwrap(), 3);
|
||||||
|
// test reading from iterator
|
||||||
|
{
|
||||||
|
let iter = ClusterIterator::new(&mut cur, fat_type, 0x9);
|
||||||
|
assert_eq!(iter.map(|r| r.unwrap()).collect::<Vec<_>>(), vec![0xA, 0x14, 0x15, 0x16, 0x19, 0x1A]);
|
||||||
|
}
|
||||||
|
// test truncating a chain
|
||||||
|
{
|
||||||
|
let mut iter = ClusterIterator::new(&mut cur, fat_type, 0x9);
|
||||||
|
assert_eq!(iter.nth(3).unwrap().unwrap(), 0x16);
|
||||||
|
iter.truncate().unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x16).unwrap(), FatValue::EndOfChain);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x19).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x1A).unwrap(), FatValue::Free);
|
||||||
|
// test freeing a chain
|
||||||
|
{
|
||||||
|
let mut iter = ClusterIterator::new(&mut cur, fat_type, 0x9);
|
||||||
|
iter.free().unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x9).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0xA).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x14).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x15).unwrap(), FatValue::Free);
|
||||||
|
assert_eq!(read_fat(&mut cur, fat_type, 0x16).unwrap(), FatValue::Free);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fat12() {
|
||||||
|
let fat: Vec<u8> = vec![
|
||||||
|
0xF0, 0xFF, 0xFF, 0x03, 0x40, 0x00, 0x05, 0x60, 0x00, 0x07, 0x80, 0x00, 0xFF, 0xAF, 0x00, 0x14, 0xC0, 0x00,
|
||||||
|
0x0D, 0xE0, 0x00, 0x0F, 0x00, 0x01, 0x11, 0xF0, 0xFF, 0x00, 0xF0, 0xFF, 0x15, 0x60, 0x01, 0x19, 0x70, 0xFF,
|
||||||
|
0xF7, 0xAF, 0x01, 0xFF, 0x0F, 0x00, 0x00, 0x70, 0xFF, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
test_fat(FatType::Fat12, io::Cursor::new(fat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fat16() {
|
||||||
|
let fat: Vec<u8> = vec![
|
||||||
|
0xF0, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, 0x00, 0x08, 0x00, 0xFF, 0xFF,
|
||||||
|
0x0A, 0x00, 0x14, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x10, 0x00, 0x11, 0x00, 0xFF, 0xFF,
|
||||||
|
0x00, 0x00, 0xFF, 0xFF, 0x15, 0x00, 0x16, 0x00, 0x19, 0x00, 0xF7, 0xFF, 0xF7, 0xFF, 0x1A, 0x00, 0xFF, 0xFF,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0xF7, 0xFF, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
test_fat(FatType::Fat16, io::Cursor::new(fat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fat32() {
|
||||||
|
let fat: Vec<u8> = vec![
|
||||||
|
0xF0, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0x0F, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00,
|
||||||
|
0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x0F,
|
||||||
|
0x0A, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00,
|
||||||
|
0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x0F,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, 0x15, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x19, 0x00,
|
||||||
|
0x00, 0x00, 0xF7, 0xFF, 0xFF, 0x0F, 0xF7, 0xFF, 0xFF, 0x0F, 0x1A, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x0F,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00,
|
||||||
|
];
|
||||||
|
test_fat(FatType::Fat32, io::Cursor::new(fat));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
use chrono;
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
use chrono::{Datelike, Local, TimeZone, Timelike};
|
||||||
|
|
||||||
|
/// A DOS compatible date.
|
||||||
|
///
|
||||||
|
/// Used by `DirEntry` time-related methods.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub struct Date {
|
||||||
|
/// Full year - [1980, 2107]
|
||||||
|
pub year: u16,
|
||||||
|
/// Month of the year - [1, 12]
|
||||||
|
pub month: u16,
|
||||||
|
/// Day of the month - [1, 31]
|
||||||
|
pub day: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Date {
|
||||||
|
pub(crate) fn decode(dos_date: u16) -> Self {
|
||||||
|
let (year, month, day) = ((dos_date >> 9) + 1980, (dos_date >> 5) & 0xF, dos_date & 0x1F);
|
||||||
|
Date { year, month, day }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode(&self) -> u16 {
|
||||||
|
((self.year - 1980) << 9) | (self.month << 5) | self.day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A DOS compatible time.
|
||||||
|
///
|
||||||
|
/// Used by `DirEntry` time-related methods.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub struct Time {
|
||||||
|
/// Hours after midnight - [0, 23]
|
||||||
|
pub hour: u16,
|
||||||
|
/// Minutes after the hour - [0, 59]
|
||||||
|
pub min: u16,
|
||||||
|
/// Seconds after the minute - [0, 59]
|
||||||
|
pub sec: u16,
|
||||||
|
/// Milliseconds after the second - [0, 999]
|
||||||
|
pub millis: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub(crate) fn decode(dos_time: u16, dos_time_hi_res: u8) -> Self {
|
||||||
|
let hour = dos_time >> 11;
|
||||||
|
let min = (dos_time >> 5) & 0x3F;
|
||||||
|
let sec = (dos_time & 0x1F) * 2 + (dos_time_hi_res as u16) / 100;
|
||||||
|
let millis = (dos_time_hi_res as u16 % 100) * 10;
|
||||||
|
Time { hour, min, sec, millis }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode(&self) -> (u16, u8) {
|
||||||
|
let dos_time = (self.hour << 11) | (self.min << 5) | (self.sec / 2);
|
||||||
|
let dos_time_hi_res = ((self.millis / 10) + (self.sec % 2) * 100) as u8;
|
||||||
|
(dos_time, dos_time_hi_res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A DOS compatible date and time.
|
||||||
|
///
|
||||||
|
/// Used by `DirEntry` time-related methods.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub struct DateTime {
|
||||||
|
/// A date part
|
||||||
|
pub date: Date,
|
||||||
|
// A time part
|
||||||
|
pub time: Time,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateTime {
|
||||||
|
pub(crate) fn decode(dos_date: u16, dos_time: u16, dos_time_hi_res: u8) -> Self {
|
||||||
|
DateTime { date: Date::decode(dos_date), time: Time::decode(dos_time, dos_time_hi_res) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
impl From<Date> for chrono::Date<Local> {
|
||||||
|
fn from(date: Date) -> Self {
|
||||||
|
Local.ymd(date.year as i32, date.month as u32, date.day as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
impl From<DateTime> for chrono::DateTime<Local> {
|
||||||
|
fn from(date_time: DateTime) -> Self {
|
||||||
|
chrono::Date::<Local>::from(date_time.date).and_hms_milli(
|
||||||
|
date_time.time.hour as u32,
|
||||||
|
date_time.time.min as u32,
|
||||||
|
date_time.time.sec as u32,
|
||||||
|
date_time.time.millis as u32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
impl From<chrono::Date<Local>> for Date {
|
||||||
|
fn from(date: chrono::Date<Local>) -> Self {
|
||||||
|
Date { year: date.year() as u16, month: date.month() as u16, day: date.day() as u16 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
impl From<chrono::DateTime<Local>> for DateTime {
|
||||||
|
fn from(date_time: chrono::DateTime<Local>) -> Self {
|
||||||
|
DateTime {
|
||||||
|
date: Date::from(date_time.date()),
|
||||||
|
time: Time {
|
||||||
|
hour: date_time.hour() as u16,
|
||||||
|
min: date_time.minute() as u16,
|
||||||
|
sec: date_time.second() as u16,
|
||||||
|
millis: (date_time.nanosecond() / 1_000_000) as u16,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A current time and date provider.
|
||||||
|
///
|
||||||
|
/// Provides a custom implementation for a time resolution used when updating directory entry time fields.
|
||||||
|
/// Default implementation gets time from `chrono` crate if `chrono` feature is enabled.
|
||||||
|
/// Otherwise default implementation returns DOS minimal date-time (1980/1/1 0:00:00).
|
||||||
|
/// `TimeProvider` is specified by the `time_provider` property in `FsOptions` struct.
|
||||||
|
pub trait TimeProvider: Debug {
|
||||||
|
fn get_current_date(&self) -> Date;
|
||||||
|
fn get_current_date_time(&self) -> DateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct DefaultTimeProvider {
|
||||||
|
_dummy: (),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeProvider for DefaultTimeProvider {
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
fn get_current_date(&self) -> Date {
|
||||||
|
Date::from(chrono::Local::now().date())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
fn get_current_date(&self) -> Date {
|
||||||
|
Date::decode(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
fn get_current_date_time(&self) -> DateTime {
|
||||||
|
DateTime::from(chrono::Local::now())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
fn get_current_date_time(&self) -> DateTime {
|
||||||
|
DateTime::decode(0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static DEFAULT_TIME_PROVIDER: DefaultTimeProvider = DefaultTimeProvider { _dummy: () };
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Date, Time};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn date_encode_decode() {
|
||||||
|
let d = Date {
|
||||||
|
year: 2055,
|
||||||
|
month: 7,
|
||||||
|
day: 23
|
||||||
|
};
|
||||||
|
let x = d.encode();
|
||||||
|
assert_eq!(x, 38647);
|
||||||
|
assert_eq!(d, Date::decode(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn time_encode_decode() {
|
||||||
|
let t1 = Time {
|
||||||
|
hour: 15,
|
||||||
|
min: 3,
|
||||||
|
sec: 29,
|
||||||
|
millis: 990,
|
||||||
|
};
|
||||||
|
let t2 = Time {
|
||||||
|
sec: 18,
|
||||||
|
.. t1
|
||||||
|
};
|
||||||
|
let t3 = Time {
|
||||||
|
millis: 40,
|
||||||
|
.. t1
|
||||||
|
};
|
||||||
|
let (x1, y1) = t1.encode();
|
||||||
|
let (x2, y2) = t2.encode();
|
||||||
|
let (x3, y3) = t3.encode();
|
||||||
|
assert_eq!((x1, y1), (30830, 199));
|
||||||
|
assert_eq!((x2, y2), (30825, 99));
|
||||||
|
assert_eq!((x3, y3), (30830, 104));
|
||||||
|
assert_eq!(t1, Time::decode(x1, y1));
|
||||||
|
assert_eq!(t2, Time::decode(x2, y2));
|
||||||
|
assert_eq!(t3, Time::decode(x3, y3));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
extern crate env_logger;
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
const KB: u64 = 1024;
|
||||||
|
const MB: u64 = KB * 1024;
|
||||||
|
const TEST_STR: &str = "Hi there Rust programmer!\n";
|
||||||
|
|
||||||
|
type FileSystem = fatfs::FileSystem<BufStream<io::Cursor<Vec<u8>>>>;
|
||||||
|
|
||||||
|
fn basic_fs_test(fs: &FileSystem) {
|
||||||
|
let stats = fs.stats().expect("stats");
|
||||||
|
if fs.fat_type() == fatfs::FatType::Fat32 {
|
||||||
|
// On FAT32 one cluster is allocated for root directory
|
||||||
|
assert_eq!(stats.total_clusters(), stats.free_clusters() + 1);
|
||||||
|
} else {
|
||||||
|
assert_eq!(stats.total_clusters(), stats.free_clusters());
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let entries = root_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
|
||||||
|
assert_eq!(entries.len(), 0);
|
||||||
|
|
||||||
|
let subdir1 = root_dir.create_dir("subdir1").expect("create_dir subdir1");
|
||||||
|
let subdir2 = root_dir.create_dir("subdir1/subdir2 with long name").expect("create_dir subdir2");
|
||||||
|
|
||||||
|
let test_str = TEST_STR.repeat(1000);
|
||||||
|
{
|
||||||
|
let mut file = subdir2.create_file("test file name.txt").expect("create file");
|
||||||
|
file.truncate().expect("truncate file");
|
||||||
|
file.write_all(test_str.as_bytes()).expect("write file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = root_dir.open_file("subdir1/subdir2 with long name/test file name.txt").unwrap();
|
||||||
|
let mut content = String::new();
|
||||||
|
file.read_to_string(&mut content).expect("read_to_string");
|
||||||
|
assert_eq!(content, test_str);
|
||||||
|
|
||||||
|
let filenames = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(filenames, ["subdir1"]);
|
||||||
|
|
||||||
|
let filenames = subdir2.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(filenames, [".", "..", "test file name.txt"]);
|
||||||
|
|
||||||
|
subdir1.rename("subdir2 with long name/test file name.txt", &root_dir, "new-name.txt").expect("rename");
|
||||||
|
|
||||||
|
let filenames = subdir2.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(filenames, [".", ".."]);
|
||||||
|
|
||||||
|
let filenames = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(filenames, ["subdir1", "new-name.txt"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_format_fs(opts: fatfs::FormatVolumeOptions, total_bytes: u64) -> FileSystem {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
// Init storage to 0xD1 bytes (value has been choosen to be parsed as normal file)
|
||||||
|
let storage_vec: Vec<u8> = vec![0xD1u8; total_bytes as usize];
|
||||||
|
let storage_cur = io::Cursor::new(storage_vec);
|
||||||
|
let mut buffered_stream = BufStream::new(storage_cur);
|
||||||
|
fatfs::format_volume(&mut buffered_stream, opts).expect("format volume");
|
||||||
|
|
||||||
|
let fs = fatfs::FileSystem::new(buffered_stream, fatfs::FsOptions::new()).expect("open fs");
|
||||||
|
basic_fs_test(&fs);
|
||||||
|
fs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_1mb() {
|
||||||
|
let total_bytes = 1 * MB;
|
||||||
|
let opts = fatfs::FormatVolumeOptions::new();
|
||||||
|
let fs = test_format_fs(opts, total_bytes);
|
||||||
|
assert_eq!(fs.fat_type(), fatfs::FatType::Fat12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_8mb_1fat() {
|
||||||
|
let total_bytes = 8 * MB;
|
||||||
|
let opts = fatfs::FormatVolumeOptions::new().fats(1);
|
||||||
|
let fs = test_format_fs(opts, total_bytes);
|
||||||
|
assert_eq!(fs.fat_type(), fatfs::FatType::Fat16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_50mb() {
|
||||||
|
let total_bytes = 50 * MB;
|
||||||
|
let opts = fatfs::FormatVolumeOptions::new();
|
||||||
|
let fs = test_format_fs(opts, total_bytes);
|
||||||
|
assert_eq!(fs.fat_type(), fatfs::FatType::Fat16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_512mb_512sec() {
|
||||||
|
let total_bytes = 2 * 1024 * MB;
|
||||||
|
let opts = fatfs::FormatVolumeOptions::new();
|
||||||
|
let fs = test_format_fs(opts, total_bytes);
|
||||||
|
assert_eq!(fs.fat_type(), fatfs::FatType::Fat32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_512mb_4096sec() {
|
||||||
|
let total_bytes = 1 * 1024 * MB;
|
||||||
|
let opts = fatfs::FormatVolumeOptions::new().bytes_per_sector(4096);
|
||||||
|
let fs = test_format_fs(opts, total_bytes);
|
||||||
|
assert_eq!(fs.fat_type(), fatfs::FatType::Fat32);
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
extern crate env_logger;
|
||||||
|
extern crate fatfs;
|
||||||
|
extern crate fscommon;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::io::SeekFrom;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
use fatfs::{FatType, FsOptions};
|
||||||
|
use fscommon::BufStream;
|
||||||
|
|
||||||
|
const TEST_TEXT: &str = "Rust is cool!\n";
|
||||||
|
const FAT12_IMG: &str = "resources/fat12.img";
|
||||||
|
const FAT16_IMG: &str = "resources/fat16.img";
|
||||||
|
const FAT32_IMG: &str = "resources/fat32.img";
|
||||||
|
|
||||||
|
type FileSystem = fatfs::FileSystem<BufStream<fs::File>>;
|
||||||
|
|
||||||
|
fn call_with_fs(f: &Fn(FileSystem) -> (), filename: &str) {
|
||||||
|
let _ = env_logger::try_init();
|
||||||
|
let file = fs::File::open(filename).unwrap();
|
||||||
|
let buf_file = BufStream::new(file);
|
||||||
|
let fs = FileSystem::new(buf_file, FsOptions::new()).unwrap();
|
||||||
|
f(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_root_dir(fs: FileSystem) {
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let entries = root_dir.iter().map(|r| r.unwrap()).collect::<Vec<_>>();
|
||||||
|
let short_names = entries.iter().map(|e| e.short_file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(short_names, ["LONG.TXT", "SHORT.TXT", "VERY", "VERY-L~1"]);
|
||||||
|
let names = entries.iter().map(|e| e.file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(names, ["long.txt", "short.txt", "very", "very-long-dir-name"]);
|
||||||
|
// Try read again
|
||||||
|
let names2 = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(names2, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_root_dir_fat12() {
|
||||||
|
call_with_fs(&test_root_dir, FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_root_dir_fat16() {
|
||||||
|
call_with_fs(&test_root_dir, FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_root_dir_fat32() {
|
||||||
|
call_with_fs(&test_root_dir, FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_read_seek_short_file(fs: FileSystem) {
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
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);
|
||||||
|
|
||||||
|
assert_eq!(short_file.seek(SeekFrom::Start(5)).unwrap(), 5);
|
||||||
|
let mut buf2 = [0; 5];
|
||||||
|
short_file.read_exact(&mut buf2).unwrap();
|
||||||
|
assert_eq!(str::from_utf8(&buf2).unwrap(), &TEST_TEXT[5..10]);
|
||||||
|
|
||||||
|
assert_eq!(short_file.seek(SeekFrom::Start(1000)).unwrap(), TEST_TEXT.len() as u64);
|
||||||
|
let mut buf2 = [0; 5];
|
||||||
|
assert_eq!(short_file.read(&mut buf2).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_seek_short_file_fat12() {
|
||||||
|
call_with_fs(&test_read_seek_short_file, FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_seek_short_file_fat16() {
|
||||||
|
call_with_fs(&test_read_seek_short_file, FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_seek_short_file_fat32() {
|
||||||
|
call_with_fs(&test_read_seek_short_file, FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_read_long_file(fs: FileSystem) {
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
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));
|
||||||
|
|
||||||
|
assert_eq!(long_file.seek(SeekFrom::Start(2017)).unwrap(), 2017);
|
||||||
|
buf.clear();
|
||||||
|
let mut buf2 = [0; 10];
|
||||||
|
long_file.read_exact(&mut buf2).unwrap();
|
||||||
|
assert_eq!(str::from_utf8(&buf2).unwrap(), &TEST_TEXT.repeat(1000)[2017..2027]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_long_file_fat12() {
|
||||||
|
call_with_fs(&test_read_long_file, FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_long_file_fat16() {
|
||||||
|
call_with_fs(&test_read_long_file, FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_long_file_fat32() {
|
||||||
|
call_with_fs(&test_read_long_file, FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_get_dir_by_path(fs: FileSystem) {
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
let dir = root_dir.open_dir("very/long/path/").unwrap();
|
||||||
|
let names = dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(names, [".", "..", "test.txt"]);
|
||||||
|
|
||||||
|
let dir2 = root_dir.open_dir("very/long/path/././.").unwrap();
|
||||||
|
let names2 = dir2.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(names2, [".", "..", "test.txt"]);
|
||||||
|
|
||||||
|
let root_dir2 = root_dir.open_dir("very/long/path/../../..").unwrap();
|
||||||
|
let root_names = root_dir2.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
let root_names2 = root_dir.iter().map(|r| r.unwrap().file_name()).collect::<Vec<String>>();
|
||||||
|
assert_eq!(root_names, root_names2);
|
||||||
|
|
||||||
|
root_dir.open_dir("VERY-L~1").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_dir_by_path_fat12() {
|
||||||
|
call_with_fs(&test_get_dir_by_path, FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_dir_by_path_fat16() {
|
||||||
|
call_with_fs(&test_get_dir_by_path, FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_dir_by_path_fat32() {
|
||||||
|
call_with_fs(&test_get_dir_by_path, FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_get_file_by_path(fs: FileSystem) {
|
||||||
|
let root_dir = fs.root_dir();
|
||||||
|
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);
|
||||||
|
|
||||||
|
root_dir.open_file("VERY-L~1/VERY-L~1.TXT").unwrap();
|
||||||
|
|
||||||
|
// try opening dir as file
|
||||||
|
assert!(root_dir.open_file("very/long/path").is_err());
|
||||||
|
// try opening file as dir
|
||||||
|
assert!(root_dir.open_dir("very/long/path/test.txt").is_err());
|
||||||
|
// try using invalid path containing file as non-last component
|
||||||
|
assert!(root_dir.open_file("very/long/path/test.txt/abc").is_err());
|
||||||
|
assert!(root_dir.open_dir("very/long/path/test.txt/abc").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_file_by_path_fat12() {
|
||||||
|
call_with_fs(&test_get_file_by_path, FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_file_by_path_fat16() {
|
||||||
|
call_with_fs(&test_get_file_by_path, FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_file_by_path_fat32() {
|
||||||
|
call_with_fs(&test_get_file_by_path, FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_volume_metadata(fs: FileSystem, fat_type: FatType) {
|
||||||
|
assert_eq!(fs.volume_id(), 0x12345678);
|
||||||
|
assert_eq!(fs.volume_label(), "Test!");
|
||||||
|
assert_eq!(&fs.read_volume_label_from_root_dir().unwrap().unwrap(), "Test!");
|
||||||
|
assert_eq!(fs.fat_type(), fat_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_volume_metadata_fat12() {
|
||||||
|
call_with_fs(&|fs| test_volume_metadata(fs, FatType::Fat12), FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_volume_metadata_fat16() {
|
||||||
|
call_with_fs(&|fs| test_volume_metadata(fs, FatType::Fat16), FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_volume_metadata_fat32() {
|
||||||
|
call_with_fs(&|fs| test_volume_metadata(fs, FatType::Fat32), FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_status_flags(fs: FileSystem) {
|
||||||
|
let status_flags = fs.read_status_flags().unwrap();
|
||||||
|
assert_eq!(status_flags.dirty(), false);
|
||||||
|
assert_eq!(status_flags.io_error(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_flags_fat12() {
|
||||||
|
call_with_fs(&|fs| test_status_flags(fs), FAT12_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_flags_fat16() {
|
||||||
|
call_with_fs(&|fs| test_status_flags(fs), FAT16_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_flags_fat32() {
|
||||||
|
call_with_fs(&|fs| test_status_flags(fs), FAT32_IMG)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stats_fat12() {
|
||||||
|
call_with_fs(
|
||||||
|
&|fs| {
|
||||||
|
let stats = fs.stats().unwrap();
|
||||||
|
assert_eq!(stats.cluster_size(), 512);
|
||||||
|
assert_eq!(stats.total_clusters(), 1955); // 1000 * 1024 / 512 = 2000
|
||||||
|
assert_eq!(stats.free_clusters(), 1920);
|
||||||
|
},
|
||||||
|
FAT12_IMG,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stats_fat16() {
|
||||||
|
call_with_fs(
|
||||||
|
&|fs| {
|
||||||
|
let stats = fs.stats().unwrap();
|
||||||
|
assert_eq!(stats.cluster_size(), 512);
|
||||||
|
assert_eq!(stats.total_clusters(), 4927); // 2500 * 1024 / 512 = 5000
|
||||||
|
assert_eq!(stats.free_clusters(), 4892);
|
||||||
|
},
|
||||||
|
FAT16_IMG,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stats_fat32() {
|
||||||
|
call_with_fs(
|
||||||
|
&|fs| {
|
||||||
|
let stats = fs.stats().unwrap();
|
||||||
|
assert_eq!(stats.cluster_size(), 512);
|
||||||
|
assert_eq!(stats.total_clusters(), 66922); // 34000 * 1024 / 512 = 68000
|
||||||
|
assert_eq!(stats.free_clusters(), 66886);
|
||||||
|
},
|
||||||
|
FAT32_IMG,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,384 @@
|
||||||
|
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)
|
||||||
|
}
|
Loading…
Reference in New Issue