Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ New features:
* Undeprecate `set_created`, `set_accessed`, `set_modified` methods on `File` - there are scenarios where those methods are needed
* Relax BPB validation by allowing non-zero values in both `total_sectors_32` and `total_sectors_16`
* Add `strict` field in `FsOptions`, which allows disabling validation of boot signature to improve compatibility with old FAT images
* Improve `open_dir`
* Expose bytes_per_sector and first_cluster

Bug fixes:
* Fix formatting volumes with size in range 4096-4199 KB
* Always respect `fat_type` from `FormatVolumeOptions`
* Fill FAT32 root directory clusters with zeros after allocation to avoid interpreting old data as directory entries
* Put '.' and '..' in the first two directory entries. (fixes "Expected a valid '.' entry in this slot." fsck error)
* Set the cluster number to 0 in the ".." directory entry if it points to the root dir
* Flush on unmount

0.3.4 (2020-07-20)
------------------
Expand Down
6 changes: 5 additions & 1 deletion src/boot_sector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ impl BiosParameterBlock {
}
if self.total_sectors_16 != 0
&& self.total_sectors_32 != 0
&& self.total_sectors_16 as u32 != self.total_sectors_32
&& u32::from(self.total_sectors_16) != self.total_sectors_32
{
error!("Invalid BPB: total_sectors_16 and total_sectors_32 are non-zero and have conflicting values");
return Err(Error::CorruptedFileSystem);
Expand Down Expand Up @@ -395,6 +395,10 @@ impl BiosParameterBlock {
u32::from(self.sectors_per_cluster) * u32::from(self.bytes_per_sector)
}

pub(crate) fn bytes_per_sector(&self) -> u16 {
self.bytes_per_sector
}

pub(crate) fn clusters_from_bytes(&self, bytes: u64) -> u32 {
let cluster_size = u64::from(self.cluster_size());
((bytes + cluster_size - 1) / cluster_size) as u32
Expand Down
27 changes: 20 additions & 7 deletions src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl<'a, IO: ReadWriteSeek, TP: TimeProvider, OCC: OemCpConverter> Dir<'a, IO, T
Err(err) => return Err(err),
// directory already exists - return it
Ok(e) => return Ok(DirEntryOrShortName::DirEntry(e)),
};
}
// try to generate short name
if let Ok(name) = short_name_gen.generate() {
return Ok(DirEntryOrShortName::ShortName(name));
Expand All @@ -200,20 +200,32 @@ impl<'a, IO: ReadWriteSeek, TP: TimeProvider, OCC: OemCpConverter> Dir<'a, IO, T
///
/// `path` is a '/' separated directory path relative to self directory.
///
/// An empty path returns the current directory.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is a good way to handle this situation. Do you know any other API that behaves like this? What problem does this change solve?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a side-effect of how the code is now written.
I can add an extra check to return an error in case of an empty string.

As it turns out, I use it no longer - so is (from my point) not neccessary.

/// A trailing slash is ignored.
/// (But the path '/' will not be found.)
///
/// # Errors
///
/// Errors that can be returned:
///
/// * `Error::NotFound` will be returned if `path` does not point to any existing directory entry.
/// * `Error::InvalidInput` will be returned if `path` points to a file that is not a directory.
/// * `Error::Io` will be returned if the underlying storage object returned an I/O error.
pub fn open_dir(&self, path: &str) -> Result<Self, Error<IO::Error>> {
pub fn open_dir(&self, mut path: &str) -> Result<Self, Error<IO::Error>> {
trace!("Dir::open_dir {}", path);
let (name, rest_opt) = split_path(path);
let e = self.find_entry(name, Some(true), None)?;
match rest_opt {
Some(rest) => e.to_dir().open_dir(rest),
None => Ok(e.to_dir()),
let mut dir = self.clone();
loop {
if path.is_empty() {
return Ok(dir);
}
let (name, rest_opt) = split_path(path);
dir = dir.find_entry(name, Some(true), None)?.to_dir();
match rest_opt {
Some(rest) => {
path = rest;
}
None => return Ok(dir),
}
}
}

Expand Down Expand Up @@ -541,6 +553,7 @@ impl<'a, IO: ReadWriteSeek, TP: TimeProvider, OCC: OemCpConverter> Dir<'a, IO, T
Ok((stream, start_pos))
}

#[allow(clippy::type_complexity)]
fn alloc_sfn_entry(&self) -> Result<(DirRawStream<'a, IO, TP, OCC>, u64), Error<IO::Error>> {
let mut stream = self.find_free_entries(1)?;
let start_pos = stream.seek(io::SeekFrom::Current(0))?;
Expand Down
4 changes: 3 additions & 1 deletion src/dir_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,9 @@ impl<'a, IO: ReadWriteSeek, TP, OCC: OemCpConverter> DirEntry<'a, IO, TP, OCC> {
self.data.is_file()
}

pub(crate) fn first_cluster(&self) -> Option<u32> {
/// Return first cluster of a entry.
#[must_use]
pub fn first_cluster(&self) -> Option<u32> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why make it public? How the user can use this information?

Copy link
Author

@alexkazik alexkazik Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use it as a identifier (while a fs is mounted), since it's much simpler than filenames (due to lfn and only Option<u32> to be stored than a full filename - [u8;255]) in my no_std, no_alloc project.

self.data.first_cluster(self.fs.fat_type())
}

Expand Down
10 changes: 9 additions & 1 deletion src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ impl<TP: TimeProvider, OCC: OemCpConverter> FsOptions<TP, OCC> {
}

/// If enabled more validations are performed to check if file-system is conforming to specification.
#[must_use]
pub fn strict(self, strict: bool) -> Self {
Self {
update_accessed_date: self.update_accessed_date,
Expand Down Expand Up @@ -459,6 +460,12 @@ impl<IO: Read + Write + Seek, TP, OCC> FileSystem<IO, TP, OCC> {
self.bpb.cluster_size()
}

/// Get sector size in bytes
#[must_use]
pub fn bytes_per_sector(&self) -> u16 {
self.bpb.bytes_per_sector()
}

pub(crate) fn offset_from_cluster(&self, cluster: u32) -> u64 {
self.offset_from_sector(self.sector_from_cluster(cluster))
}
Expand Down Expand Up @@ -576,6 +583,7 @@ impl<IO: Read + Write + Seek, TP, OCC> FileSystem<IO, TP, OCC> {
fn unmount_internal(&self) -> Result<(), Error<IO::Error>> {
self.flush_fs_info()?;
self.set_dirty_flag(false)?;
self.disk.borrow_mut().flush()?;
Ok(())
}

Expand Down Expand Up @@ -617,7 +625,7 @@ impl<IO: Read + Write + Seek, TP, OCC> FileSystem<IO, TP, OCC> {
}

/// Returns a root directory object allowing for futher penetration of a filesystem structure.
pub fn root_dir(&self) -> Dir<IO, TP, OCC> {
pub fn root_dir(&self) -> Dir<'_, IO, TP, OCC> {
trace!("root_dir");
let root_rdr = {
match self.fat_type {
Expand Down
4 changes: 2 additions & 2 deletions src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ where
fat.write_u32_le(u32::from(media) | 0xFFF_FF00)?;
fat.write_u32_le(0xFFFF_FFFF)?;
}
};
}
// mark entries at the end of FAT as used (after FAT but before sector end)
let start_cluster = total_clusters + RESERVED_FAT_ENTRIES;
let end_cluster = (bytes_per_fat * BITS_PER_BYTE / u64::from(fat_type.bits_per_fat_entry())) as u32;
Expand Down Expand Up @@ -531,7 +531,7 @@ impl FatTrait for Fat32 {
"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 => 0x0FFF_FFF7,
Expand Down