├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── COPYING ├── Cargo.toml ├── README.md ├── src ├── cluster.rs ├── directory.rs ├── disk.rs ├── entries.rs ├── fat.rs ├── file.rs ├── lib.rs ├── param.rs └── timestamp.rs └── tests ├── exfat.img └── integration_test.rs /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | publish: 8 | name: Publish 9 | runs-on: ubuntu-22.04 10 | env: 11 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 12 | steps: 13 | - name: Checkout source 14 | uses: actions/checkout@v3 15 | - name: Publish crate 16 | run: cargo publish 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | - name: Check code styles 17 | run: cargo fmt --check 18 | - name: Run Clippy 19 | run: cargo clippy -- -D warnings 20 | - name: Run tests 21 | run: cargo test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /target/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.insertFinalNewline": true, 3 | "files.trimFinalNewlines": true, 4 | "files.trimTrailingWhitespace": true 5 | } 6 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Obliteration Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exfat" 3 | version = "0.1.0" 4 | description = "Pure Rust implementation of exFAT file system" 5 | repository = "https://github.com/obhq/exfat" 6 | license = "MIT" 7 | edition = "2021" 8 | rust-version = "1.81" 9 | 10 | [features] 11 | default = ["std"] 12 | std = [] 13 | 14 | [dependencies] 15 | byteorder = { version = "1.4", default-features = false } 16 | thiserror = "1.0" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exFAT in pure Rust 2 | [![Crates.io](https://img.shields.io/crates/v/exfat)](https://crates.io/crates/exfat) 3 | 4 | This is an implementation of exFAT in pure Rust. Currently it is supports only reading, not writing; and not all features are implemented but if all you need is listing the directories and read the files then you are good to go. 5 | 6 | This implementation require a global allocator. 7 | 8 | ## Usage 9 | 10 | ```rust 11 | use exfat::image::Image; 12 | use std::fs::File; 13 | 14 | let image = File::open("exfat.img").expect("cannot open exfat.img"); 15 | let image = Image::open(image).expect("cannot open exFAT image from exfat.img"); 16 | let root = Root::open(image).expect("cannot open the root directory"); 17 | 18 | for item in root { 19 | // item will be either file or directory. 20 | } 21 | ``` 22 | 23 | ## Breaking changes 24 | 25 | ### 0.1 to 0.2 26 | 27 | My Rust skill has improved a lot since version 0.1 so I take this semver breaking change to make a lot of things better. That mean version 0.2 is not compatible with 0.1 in any ways. 28 | 29 | ## License 30 | 31 | MIT 32 | -------------------------------------------------------------------------------- /src/cluster.rs: -------------------------------------------------------------------------------- 1 | use crate::disk::DiskPartition; 2 | use crate::fat::Fat; 3 | use crate::param::Params; 4 | use std::cmp::min; 5 | use thiserror::Error; 6 | 7 | /// Struct to read all data in a cluster chain. 8 | pub struct ClustersReader { 9 | disk: D, 10 | params: P, 11 | chain: Vec, 12 | data_length: u64, 13 | offset: u64, 14 | } 15 | 16 | impl> ClustersReader { 17 | pub fn new( 18 | disk: D, 19 | params: P, 20 | fat: &Fat, 21 | first_cluster: usize, 22 | data_length: Option, 23 | no_fat_chain: Option, 24 | ) -> Result { 25 | if first_cluster < 2 { 26 | return Err(NewError::InvalidFirstCluster); 27 | } 28 | 29 | // Get cluster chain. 30 | let cluster_size = params.as_ref().cluster_size(); 31 | let (chain, data_length) = if no_fat_chain.unwrap_or(false) { 32 | // If the NoFatChain bit is 1 then DataLength must not be zero. 33 | let data_length = match data_length { 34 | Some(v) if v > 0 => v, 35 | _ => return Err(NewError::InvalidDataLength), 36 | }; 37 | 38 | // FIXME: Use div_ceil once https://github.com/rust-lang/rust/issues/88581 stabilized. 39 | let count = (data_length + cluster_size - 1) / cluster_size; 40 | let chain: Vec = (first_cluster..(first_cluster + count as usize)).collect(); 41 | 42 | (chain, data_length) 43 | } else { 44 | let chain: Vec = fat.get_cluster_chain(first_cluster).collect(); 45 | 46 | if chain.is_empty() { 47 | return Err(NewError::InvalidFirstCluster); 48 | } 49 | 50 | let data_length = match data_length { 51 | Some(v) => { 52 | if v > cluster_size * chain.len() as u64 { 53 | return Err(NewError::InvalidDataLength); 54 | } else { 55 | v 56 | } 57 | } 58 | None => { 59 | params.as_ref().bytes_per_sector 60 | * (params.as_ref().sectors_per_cluster * chain.len() as u64) 61 | } 62 | }; 63 | 64 | (chain, data_length) 65 | }; 66 | 67 | Ok(Self { 68 | disk, 69 | params, 70 | chain, 71 | data_length, 72 | offset: 0, 73 | }) 74 | } 75 | 76 | pub fn cluster(&self) -> usize { 77 | self.chain[(self.offset / self.params.as_ref().cluster_size()) as usize] 78 | } 79 | } 80 | 81 | impl ClustersReader { 82 | pub fn data_length(&self) -> u64 { 83 | self.data_length 84 | } 85 | 86 | pub fn seek(&mut self, off: u64) -> bool { 87 | if off > self.data_length { 88 | return false; 89 | } 90 | 91 | self.offset = off; 92 | true 93 | } 94 | 95 | pub fn rewind(&mut self) { 96 | self.offset = 0; 97 | } 98 | 99 | pub fn stream_position(&self) -> u64 { 100 | self.offset 101 | } 102 | } 103 | 104 | impl> ClustersReader { 105 | pub fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 106 | use std::io::{Error, ErrorKind}; 107 | 108 | // Check if the actual read is required. 109 | if buf.is_empty() || self.offset == self.data_length { 110 | return Ok(0); 111 | } 112 | 113 | // Get remaining data in the current cluster. 114 | let params = self.params.as_ref(); 115 | let cluster_size = params.cluster_size(); 116 | let cluster_remaining = cluster_size - self.offset % cluster_size; 117 | let remaining = min(cluster_remaining, self.data_length - self.offset); 118 | 119 | // Get the offset in the partition. 120 | let cluster = self.chain[(self.offset / cluster_size) as usize]; 121 | let offset = match params.cluster_offset(cluster) { 122 | Some(v) => v + self.offset % cluster_size, 123 | None => { 124 | return Err(Error::new( 125 | ErrorKind::Other, 126 | format!("cluster #{cluster} is not available"), 127 | )); 128 | } 129 | }; 130 | 131 | // Read image. 132 | let amount = min(buf.len(), remaining as usize); 133 | 134 | if let Err(e) = self.disk.read_exact(offset, &mut buf[..amount]) { 135 | return Err(Error::new(ErrorKind::Other, Box::new(e))); 136 | } 137 | 138 | self.offset += amount as u64; 139 | 140 | Ok(amount) 141 | } 142 | 143 | pub fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<(), std::io::Error> { 144 | while !buf.is_empty() { 145 | let n = self.read(buf)?; 146 | 147 | if n == 0 { 148 | return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); 149 | } 150 | 151 | buf = &mut buf[n..]; 152 | } 153 | 154 | Ok(()) 155 | } 156 | } 157 | 158 | /// Represents an error for [`new()`][ClustersReader::new()]. 159 | #[derive(Debug, Error)] 160 | pub enum NewError { 161 | #[error("first cluster is not valid")] 162 | InvalidFirstCluster, 163 | 164 | #[error("data length is not valid")] 165 | InvalidDataLength, 166 | } 167 | -------------------------------------------------------------------------------- /src/directory.rs: -------------------------------------------------------------------------------- 1 | use crate::cluster::ClustersReader; 2 | use crate::disk::DiskPartition; 3 | use crate::entries::{ClusterAllocation, EntriesReader, EntryType, FileEntry, StreamEntry}; 4 | use crate::fat::Fat; 5 | use crate::file::File; 6 | use crate::param::Params; 7 | use crate::timestamp::Timestamps; 8 | use alloc::sync::Arc; 9 | use thiserror::Error; 10 | 11 | /// Represents a directory in an exFAT filesystem. 12 | pub struct Directory { 13 | disk: Arc, 14 | params: Arc, 15 | fat: Arc, 16 | name: String, 17 | stream: StreamEntry, 18 | timestamps: Timestamps, 19 | } 20 | 21 | impl Directory { 22 | pub(crate) fn new( 23 | disk: Arc, 24 | params: Arc, 25 | fat: Arc, 26 | name: String, 27 | stream: StreamEntry, 28 | timestamps: Timestamps, 29 | ) -> Self { 30 | Self { 31 | disk, 32 | params, 33 | fat, 34 | name, 35 | stream, 36 | timestamps, 37 | } 38 | } 39 | 40 | pub fn name(&self) -> &str { 41 | self.name.as_ref() 42 | } 43 | 44 | pub fn timestamps(&self) -> &Timestamps { 45 | &self.timestamps 46 | } 47 | } 48 | 49 | impl Directory { 50 | pub fn open(&self) -> Result>, DirectoryError> { 51 | // Create an entries reader. 52 | let alloc = self.stream.allocation(); 53 | let mut reader = match ClustersReader::new( 54 | &self.disk, 55 | &self.params, 56 | &self.fat, 57 | alloc.first_cluster(), 58 | Some(alloc.data_length()), 59 | Some(self.stream.no_fat_chain()), 60 | ) { 61 | Ok(v) => EntriesReader::new(v), 62 | Err(e) => return Err(DirectoryError::CreateClustersReaderFailed(alloc.clone(), e)), 63 | }; 64 | 65 | // Read file entries. 66 | let mut items: Vec> = Vec::new(); 67 | 68 | loop { 69 | // Read primary entry. 70 | let entry = match reader.read() { 71 | Ok(v) => v, 72 | Err(e) => return Err(DirectoryError::ReadEntryFailed(e)), 73 | }; 74 | 75 | // Check entry type. 76 | let ty = entry.ty(); 77 | 78 | if !ty.is_regular() { 79 | break; 80 | } else if ty.type_category() != EntryType::PRIMARY { 81 | return Err(DirectoryError::NotPrimaryEntry( 82 | entry.index(), 83 | entry.cluster(), 84 | )); 85 | } else if ty.type_importance() != EntryType::CRITICAL || ty.type_code() != 5 { 86 | return Err(DirectoryError::NotFileEntry(entry.index(), entry.cluster())); 87 | } 88 | 89 | // Parse file entry. 90 | let file = match FileEntry::load(&entry, &mut reader) { 91 | Ok(v) => v, 92 | Err(e) => return Err(DirectoryError::LoadFileEntryFailed(e)), 93 | }; 94 | 95 | // Construct item. 96 | let name = file.name; 97 | let attrs = file.attributes; 98 | let stream = file.stream; 99 | let timestamps = file.timestamps; 100 | 101 | items.push(if attrs.is_directory() { 102 | Item::Directory(Self { 103 | disk: self.disk.clone(), 104 | params: self.params.clone(), 105 | fat: self.fat.clone(), 106 | name, 107 | stream, 108 | timestamps, 109 | }) 110 | } else { 111 | match File::new( 112 | &self.disk, 113 | &self.params, 114 | &self.fat, 115 | name, 116 | stream, 117 | timestamps, 118 | ) { 119 | Ok(v) => Item::File(v), 120 | Err(e) => { 121 | return Err(DirectoryError::CreateFileObjectFailed( 122 | entry.index(), 123 | entry.cluster(), 124 | e, 125 | )); 126 | } 127 | } 128 | }); 129 | } 130 | 131 | Ok(items) 132 | } 133 | } 134 | 135 | /// Represents an item in the directory. 136 | pub enum Item { 137 | Directory(Directory), 138 | File(File), 139 | } 140 | 141 | /// Represents an error when [`Directory::open()`] fails. 142 | #[derive(Debug, Error)] 143 | pub enum DirectoryError { 144 | #[error("cannot create a clusters reader for allocation {0}")] 145 | CreateClustersReaderFailed(ClusterAllocation, #[source] crate::cluster::NewError), 146 | 147 | #[error("cannot read an entry")] 148 | ReadEntryFailed(#[source] crate::entries::ReaderError), 149 | 150 | #[error("entry #{0} on cluster #{1} is not a primary entry")] 151 | NotPrimaryEntry(usize, usize), 152 | 153 | #[error("entry #{0} on cluster #{1} is not a file entry")] 154 | NotFileEntry(usize, usize), 155 | 156 | #[error("cannot load file entry")] 157 | LoadFileEntryFailed(#[source] crate::entries::FileEntryError), 158 | 159 | #[error("cannot create a file object for directory entry #{0} on cluster #{1}")] 160 | CreateFileObjectFailed(usize, usize, #[source] crate::file::NewError), 161 | } 162 | -------------------------------------------------------------------------------- /src/disk.rs: -------------------------------------------------------------------------------- 1 | use alloc::sync::Arc; 2 | use core::error::Error; 3 | use core::ops::Deref; 4 | 5 | /// Encapsulate a disk partition. 6 | pub trait DiskPartition { 7 | type Err: PartitionError + 'static; 8 | 9 | fn read(&self, offset: u64, buf: &mut [u8]) -> Result; 10 | 11 | fn read_exact(&self, mut offset: u64, mut buf: &mut [u8]) -> Result<(), Self::Err> { 12 | while !buf.is_empty() { 13 | let n = self.read(offset, buf)?; 14 | 15 | if n == 0 { 16 | return Err(PartitionError::unexpected_eop()); 17 | } 18 | 19 | offset = n 20 | .try_into() 21 | .ok() 22 | .and_then(|n| offset.checked_add(n)) 23 | .unwrap(); 24 | 25 | buf = &mut buf[n..]; 26 | } 27 | 28 | Ok(()) 29 | } 30 | } 31 | 32 | /// Represents an error when an operation on [`DiskPartition`] fails. 33 | pub trait PartitionError: Error + Send + Sync { 34 | fn unexpected_eop() -> Self; 35 | } 36 | 37 | impl DiskPartition for &T { 38 | type Err = T::Err; 39 | 40 | fn read(&self, offset: u64, buf: &mut [u8]) -> Result { 41 | (*self).read(offset, buf) 42 | } 43 | } 44 | 45 | impl DiskPartition for Arc { 46 | type Err = T::Err; 47 | 48 | fn read(&self, offset: u64, buf: &mut [u8]) -> Result { 49 | self.deref().read(offset, buf) 50 | } 51 | } 52 | 53 | #[cfg(feature = "std")] 54 | impl DiskPartition for std::fs::File { 55 | type Err = std::io::Error; 56 | 57 | #[cfg(unix)] 58 | fn read(&self, offset: u64, buf: &mut [u8]) -> Result { 59 | std::os::unix::fs::FileExt::read_at(self, buf, offset) 60 | } 61 | 62 | #[cfg(windows)] 63 | fn read(&self, offset: u64, buf: &mut [u8]) -> Result { 64 | std::os::windows::fs::FileExt::seek_read(self, buf, offset) 65 | } 66 | } 67 | 68 | #[cfg(feature = "std")] 69 | impl PartitionError for std::io::Error { 70 | fn unexpected_eop() -> Self { 71 | std::io::Error::from(std::io::ErrorKind::UnexpectedEof) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/entries.rs: -------------------------------------------------------------------------------- 1 | use crate::cluster::ClustersReader; 2 | use crate::disk::DiskPartition; 3 | use crate::param::Params; 4 | use crate::timestamp::{Timestamp, Timestamps}; 5 | use crate::FileAttributes; 6 | use byteorder::{ByteOrder, LE}; 7 | use std::cmp::min; 8 | use std::fmt::{Display, Formatter}; 9 | use thiserror::Error; 10 | 11 | /// Struct to read directory entries. 12 | pub struct EntriesReader { 13 | cluster_reader: ClustersReader, 14 | entry_index: usize, 15 | } 16 | 17 | impl EntriesReader { 18 | pub fn new(cluster_reader: ClustersReader) -> Self { 19 | Self { 20 | cluster_reader, 21 | entry_index: 0, 22 | } 23 | } 24 | } 25 | 26 | impl> EntriesReader { 27 | pub fn read(&mut self) -> Result { 28 | // Get current cluster and entry index. 29 | let cluster = self.cluster_reader.cluster(); 30 | let index = self.entry_index; 31 | 32 | // Read directory entry. 33 | let mut entry = [0u8; 32]; 34 | 35 | if let Err(e) = self.cluster_reader.read_exact(&mut entry) { 36 | return Err(ReaderError::ReadFailed(index, cluster, e)); 37 | } 38 | 39 | // Update entry index. 40 | if self.cluster_reader.cluster() != cluster { 41 | self.entry_index = 0; 42 | } else { 43 | self.entry_index += 1; 44 | } 45 | 46 | Ok(RawEntry { 47 | index, 48 | cluster, 49 | data: entry, 50 | }) 51 | } 52 | } 53 | 54 | /// Represents a raw directory entry. 55 | pub(crate) struct RawEntry { 56 | index: usize, 57 | cluster: usize, 58 | data: [u8; 32], 59 | } 60 | 61 | impl RawEntry { 62 | pub fn ty(&self) -> EntryType { 63 | EntryType(self.data[0]) 64 | } 65 | 66 | pub fn index(&self) -> usize { 67 | self.index 68 | } 69 | 70 | pub fn cluster(&self) -> usize { 71 | self.cluster 72 | } 73 | 74 | pub fn data(&self) -> &[u8; 32] { 75 | &self.data 76 | } 77 | } 78 | 79 | /// Represents a File Directory Entry. 80 | pub(crate) struct FileEntry { 81 | pub name: String, 82 | pub attributes: FileAttributes, 83 | pub stream: StreamEntry, 84 | pub timestamps: Timestamps, 85 | } 86 | 87 | impl FileEntry { 88 | pub fn load>( 89 | raw: &RawEntry, 90 | reader: &mut EntriesReader, 91 | ) -> Result { 92 | // Load fields. 93 | let data = &raw.data; 94 | let secondary_count = data[1] as usize; 95 | let attributes = FileAttributes(LE::read_u16(&data[4..])); 96 | 97 | if secondary_count < 1 { 98 | return Err(FileEntryError::NoStreamExtension(raw.index, raw.cluster)); 99 | } else if secondary_count < 2 { 100 | return Err(FileEntryError::NoFileName(raw.index, raw.cluster)); 101 | } 102 | 103 | // Read stream extension. 104 | let stream = match reader.read() { 105 | Ok(v) => v, 106 | Err(e) => return Err(FileEntryError::ReadStreamFailed(e)), 107 | }; 108 | 109 | // Check if the entry is a stream extension. 110 | let ty = stream.ty(); 111 | 112 | if !ty.is_critical_secondary(0) { 113 | return Err(FileEntryError::NotStreamExtension( 114 | stream.index, 115 | stream.cluster, 116 | )); 117 | } 118 | 119 | // Load stream extension. 120 | let stream = StreamEntry::load(stream, attributes)?; 121 | 122 | // Read file names. 123 | let name_count = secondary_count - 1; 124 | let mut names: Vec = Vec::with_capacity(name_count); 125 | 126 | for i in 0..name_count { 127 | // Read file name. 128 | let entry = match reader.read() { 129 | Ok(v) => v, 130 | Err(e) => return Err(FileEntryError::ReadFileNameFailed(i, e)), 131 | }; 132 | 133 | // Check if the entry is a file name. 134 | let ty = entry.ty(); 135 | 136 | if !ty.is_critical_secondary(1) { 137 | return Err(FileEntryError::NotFileName(entry.index, entry.cluster)); 138 | } 139 | 140 | names.push(entry); 141 | } 142 | 143 | // TODO: Use div_ceil when https://github.com/rust-lang/rust/issues/88581 stabilized. 144 | if names.len() != (stream.name_length + 15 - 1) / 15 { 145 | return Err(FileEntryError::WrongFileNames(raw.index, raw.cluster)); 146 | } 147 | 148 | // Construct a complete file name. 149 | let mut need = stream.name_length * 2; 150 | let mut name = String::with_capacity(15 * names.len()); 151 | 152 | // Read timestamps (see https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#74-file-directory-entry) 153 | let create_ts = LE::read_u32(&data[8..12]); 154 | let last_modified_ts = LE::read_u32(&data[12..16]); 155 | let last_accessed_ts = LE::read_u32(&data[16..20]); 156 | let create_10_ms_increment = data[20]; 157 | let last_modified_10_ms_increment = data[21]; 158 | let create_utc_offset = if ((data[22] >> 7) & 1) == 1 { 159 | (data[22] & 0x7F) as i8 160 | } else { 161 | 0 162 | }; 163 | let last_modified_utc_offset = if ((data[23] >> 7) & 1) == 1 { 164 | (data[23] & 0x7F) as i8 165 | } else { 166 | 0 167 | }; 168 | let last_accessed_utc_offset = if ((data[24] >> 7) & 1) == 1 { 169 | (data[24] & 0x7F) as i8 170 | } else { 171 | 0 172 | }; 173 | 174 | for entry in names { 175 | let data = entry.data; 176 | 177 | // Load GeneralSecondaryFlags. 178 | let general_secondary_flags = SecondaryFlags(data[1]); 179 | 180 | if general_secondary_flags.allocation_possible() { 181 | return Err(FileEntryError::InvalidFileName(entry.index, entry.cluster)); 182 | } 183 | 184 | // Load FileName. 185 | let raw_name = &data[2..(2 + min(30, need))]; 186 | 187 | need -= raw_name.len(); 188 | 189 | // Convert FileName from little-endian to native endian. 190 | let mut file_name = [0u16; 15]; 191 | let file_name = &mut file_name[..(raw_name.len() / 2)]; 192 | 193 | LE::read_u16_into(raw_name, file_name); 194 | 195 | match String::from_utf16(file_name) { 196 | Ok(v) => name.push_str(&v), 197 | Err(_) => return Err(FileEntryError::InvalidFileName(entry.index, entry.cluster)), 198 | } 199 | } 200 | 201 | Ok(Self { 202 | name, 203 | attributes, 204 | stream, 205 | timestamps: Timestamps::new( 206 | Timestamp::new(create_ts, create_10_ms_increment, create_utc_offset), 207 | Timestamp::new( 208 | last_modified_ts, 209 | last_modified_10_ms_increment, 210 | last_modified_utc_offset, 211 | ), 212 | Timestamp::new(last_accessed_ts, 0, last_accessed_utc_offset), 213 | ), 214 | }) 215 | } 216 | } 217 | 218 | /// Represents a Stream Extension Directory Entry. 219 | pub(crate) struct StreamEntry { 220 | no_fat_chain: bool, 221 | name_length: usize, 222 | valid_data_length: u64, 223 | alloc: ClusterAllocation, 224 | } 225 | 226 | impl StreamEntry { 227 | fn load(raw: RawEntry, attrs: FileAttributes) -> Result { 228 | // Load GeneralSecondaryFlags. 229 | let data = &raw.data; 230 | let general_secondary_flags = SecondaryFlags(data[1]); 231 | 232 | if !general_secondary_flags.allocation_possible() { 233 | return Err(FileEntryError::InvalidStreamExtension( 234 | raw.index, 235 | raw.cluster, 236 | )); 237 | } 238 | 239 | // Load NameLength. 240 | let name_length = data[3] as usize; 241 | 242 | if name_length < 1 { 243 | return Err(FileEntryError::InvalidStreamExtension( 244 | raw.index, 245 | raw.cluster, 246 | )); 247 | } 248 | 249 | // Load ValidDataLength and cluster allocation. 250 | let valid_data_length = LE::read_u64(&data[8..]); 251 | let alloc = match ClusterAllocation::load(&raw) { 252 | Ok(v) => v, 253 | Err(_) => { 254 | return Err(FileEntryError::InvalidStreamExtension( 255 | raw.index, 256 | raw.cluster, 257 | )); 258 | } 259 | }; 260 | 261 | if attrs.is_directory() { 262 | if valid_data_length != alloc.data_length { 263 | return Err(FileEntryError::InvalidStreamExtension( 264 | raw.index, 265 | raw.cluster, 266 | )); 267 | } 268 | } else if valid_data_length > alloc.data_length { 269 | return Err(FileEntryError::InvalidStreamExtension( 270 | raw.index, 271 | raw.cluster, 272 | )); 273 | } 274 | 275 | Ok(StreamEntry { 276 | no_fat_chain: general_secondary_flags.no_fat_chain(), 277 | name_length, 278 | valid_data_length, 279 | alloc, 280 | }) 281 | } 282 | 283 | pub fn no_fat_chain(&self) -> bool { 284 | self.no_fat_chain 285 | } 286 | 287 | pub fn valid_data_length(&self) -> u64 { 288 | self.valid_data_length 289 | } 290 | 291 | pub fn allocation(&self) -> &ClusterAllocation { 292 | &self.alloc 293 | } 294 | } 295 | 296 | /// Encapsulate EntryType field of the directory entry. 297 | #[derive(Debug, Clone, Copy)] 298 | #[repr(transparent)] 299 | pub(crate) struct EntryType(u8); 300 | 301 | impl EntryType { 302 | pub const PRIMARY: u8 = 0; 303 | pub const SECONDARY: u8 = 1; 304 | pub const CRITICAL: u8 = 0; 305 | 306 | pub fn is_regular(self) -> bool { 307 | self.0 >= 0x81 308 | } 309 | 310 | pub fn type_code(self) -> u8 { 311 | self.0 & 0x1f 312 | } 313 | 314 | pub fn type_importance(self) -> u8 { 315 | (self.0 & 0x20) >> 5 316 | } 317 | 318 | pub fn type_category(self) -> u8 { 319 | (self.0 & 0x40) >> 6 320 | } 321 | 322 | pub fn is_critical_secondary(self, code: u8) -> bool { 323 | self.is_regular() 324 | && self.type_importance() == Self::CRITICAL 325 | && self.type_category() == Self::SECONDARY 326 | && self.type_code() == code 327 | } 328 | } 329 | 330 | impl Display for EntryType { 331 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 332 | if self.is_regular() { 333 | if self.type_importance() == Self::CRITICAL { 334 | f.write_str("critical ")?; 335 | } else { 336 | f.write_str("benign ")?; 337 | } 338 | 339 | if self.type_category() == Self::PRIMARY { 340 | f.write_str("primary ")?; 341 | } else { 342 | f.write_str("secondary ")?; 343 | } 344 | 345 | write!(f, "{}", self.type_code()) 346 | } else { 347 | write!(f, "{:#04x}", self.0) 348 | } 349 | } 350 | } 351 | 352 | /// Represents GeneralSecondaryFlags in the Generic Secondary DirectoryEntry Template. 353 | #[derive(Clone, Copy)] 354 | #[repr(transparent)] 355 | pub(crate) struct SecondaryFlags(u8); 356 | 357 | impl SecondaryFlags { 358 | pub fn allocation_possible(self) -> bool { 359 | (self.0 & 1) != 0 360 | } 361 | 362 | pub fn no_fat_chain(self) -> bool { 363 | (self.0 & 2) != 0 364 | } 365 | } 366 | 367 | /// Represents FirstCluster and DataLength fields in the Directory Entry. 368 | #[derive(Debug, Clone)] 369 | pub struct ClusterAllocation { 370 | first_cluster: usize, 371 | data_length: u64, 372 | } 373 | 374 | impl ClusterAllocation { 375 | pub(crate) fn load(entry: &RawEntry) -> Result { 376 | // Load fields. 377 | let data = &entry.data; 378 | let first_cluster = LE::read_u32(&data[20..]) as usize; 379 | let data_length = LE::read_u64(&data[24..]); 380 | 381 | // Check values. 382 | if first_cluster == 0 { 383 | if data_length != 0 { 384 | return Err(ClusterAllocationError::InvalidDataLength); 385 | } 386 | } else if first_cluster < 2 { 387 | return Err(ClusterAllocationError::InvalidFirstCluster); 388 | } 389 | 390 | Ok(Self { 391 | first_cluster, 392 | data_length, 393 | }) 394 | } 395 | 396 | pub(crate) fn first_cluster(&self) -> usize { 397 | self.first_cluster 398 | } 399 | 400 | pub(crate) fn data_length(&self) -> u64 { 401 | self.data_length 402 | } 403 | } 404 | 405 | impl Display for ClusterAllocation { 406 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 407 | write!(f, "{}:{}", self.first_cluster, self.data_length) 408 | } 409 | } 410 | 411 | /// Represents an error for [`read()`][EntriesReader::read()]. 412 | #[derive(Debug, Error)] 413 | pub enum ReaderError { 414 | #[error("cannot read entry #{0} on cluster #{1}")] 415 | ReadFailed(usize, usize, #[source] std::io::Error), 416 | } 417 | 418 | /// Represents an error for [`load()`][FileEntry::load()]. 419 | #[derive(Debug, Error)] 420 | pub enum FileEntryError { 421 | #[error("no stream extension is followed the entry #{0} on cluster #{1}")] 422 | NoStreamExtension(usize, usize), 423 | 424 | #[error("no file name is followed the entry #{0} on cluster #{1}")] 425 | NoFileName(usize, usize), 426 | 427 | #[error("cannot read stream extension")] 428 | ReadStreamFailed(#[source] ReaderError), 429 | 430 | #[error("entry #{0} on cluster #{1} is not a stream extension")] 431 | NotStreamExtension(usize, usize), 432 | 433 | #[error("entry #{0} on cluster #{1} is not a valid stream extension")] 434 | InvalidStreamExtension(usize, usize), 435 | 436 | #[error("cannot read file name #{0}")] 437 | ReadFileNameFailed(usize, #[source] ReaderError), 438 | 439 | #[error("entry #{0} on cluster #{1} is not a file name")] 440 | NotFileName(usize, usize), 441 | 442 | #[error("entry #{0} on cluster #{1} has wrong number of file names")] 443 | WrongFileNames(usize, usize), 444 | 445 | #[error("entry #{0} on cluster #{1} is not a valid file name")] 446 | InvalidFileName(usize, usize), 447 | } 448 | 449 | /// Represents an error for [`load()`][ClusterAllocation::load()]. 450 | #[derive(Debug, Error)] 451 | pub enum ClusterAllocationError { 452 | #[error("invalid FirstCluster")] 453 | InvalidFirstCluster, 454 | 455 | #[error("invalid DataLength")] 456 | InvalidDataLength, 457 | } 458 | -------------------------------------------------------------------------------- /src/fat.rs: -------------------------------------------------------------------------------- 1 | use crate::disk::DiskPartition; 2 | use crate::param::Params; 3 | use byteorder::{ByteOrder, LE}; 4 | use core::fmt::Debug; 5 | use thiserror::Error; 6 | 7 | pub(crate) struct Fat { 8 | entries: Vec, 9 | } 10 | 11 | impl Fat { 12 | pub fn load( 13 | params: &Params, 14 | partition: &P, 15 | index: usize, 16 | ) -> Result> { 17 | // Get FAT region offset. 18 | let sector = match params.fat_length.checked_mul(index as u64) { 19 | Some(v) => match params.fat_offset.checked_add(v) { 20 | Some(v) => v, 21 | None => return Err(LoadError::InvalidFatOffset), 22 | }, 23 | None => return Err(LoadError::InvalidFatLength), 24 | }; 25 | 26 | let offset = match sector.checked_mul(params.bytes_per_sector) { 27 | Some(v) => v, 28 | None => return Err(LoadError::InvalidFatOffset), 29 | }; 30 | 31 | // Load entries. 32 | let count = params.cluster_count + 2; 33 | let mut data = vec![0u8; count * 4]; 34 | 35 | if let Err(e) = partition.read_exact(offset, &mut data) { 36 | return Err(LoadError::ReadFailed(offset, e)); 37 | } 38 | 39 | // Convert each entry from little endian to native endian. 40 | let mut entries = vec![0u32; count]; 41 | 42 | LE::read_u32_into(&data, &mut entries); 43 | 44 | Ok(Self { entries }) 45 | } 46 | 47 | pub fn get_cluster_chain(&self, first: usize) -> ClusterChain<'_> { 48 | ClusterChain { 49 | entries: &self.entries, 50 | next: first, 51 | } 52 | } 53 | } 54 | 55 | pub(crate) struct ClusterChain<'fat> { 56 | entries: &'fat [u32], 57 | next: usize, 58 | } 59 | 60 | impl<'fat> Iterator for ClusterChain<'fat> { 61 | type Item = usize; 62 | 63 | fn next(&mut self) -> Option { 64 | // Check next entry. 65 | let entries = self.entries; 66 | let next = self.next; 67 | 68 | if next < 2 || next >= entries.len() || entries[next] == 0xfffffff7 { 69 | return None; 70 | } 71 | 72 | // Move to next entry. 73 | self.next = entries[next] as usize; 74 | 75 | Some(next) 76 | } 77 | } 78 | 79 | /// Represents an error for [`Fat::load()`]. 80 | #[derive(Error)] 81 | pub enum LoadError { 82 | #[error("invalid FatLength")] 83 | InvalidFatLength, 84 | 85 | #[error("invalid FatOffset")] 86 | InvalidFatOffset, 87 | 88 | #[error("cannot read the data at {0:#x}")] 89 | ReadFailed(u64, #[source] P::Err), 90 | } 91 | 92 | impl Debug for LoadError

{ 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | match self { 95 | Self::InvalidFatLength => write!(f, "InvalidFatLength"), 96 | Self::InvalidFatOffset => write!(f, "InvalidFatOffset"), 97 | Self::ReadFailed(arg0, arg1) => { 98 | f.debug_tuple("ReadFailed").field(arg0).field(arg1).finish() 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/file.rs: -------------------------------------------------------------------------------- 1 | use crate::cluster::ClustersReader; 2 | use crate::disk::DiskPartition; 3 | use crate::entries::StreamEntry; 4 | use crate::fat::Fat; 5 | use crate::param::Params; 6 | use crate::timestamp::Timestamps; 7 | use alloc::sync::Arc; 8 | use core::cmp::min; 9 | use thiserror::Error; 10 | 11 | /// Represents a file in an exFAT filesystem. 12 | pub struct File { 13 | name: String, 14 | len: u64, 15 | reader: Option, Arc>>, 16 | timestamps: Timestamps, 17 | } 18 | 19 | impl File { 20 | pub(crate) fn new( 21 | disk: &Arc, 22 | params: &Arc, 23 | fat: &Fat, 24 | name: String, 25 | stream: StreamEntry, 26 | timestamps: Timestamps, 27 | ) -> Result { 28 | // Create a cluster reader. 29 | let alloc = stream.allocation(); 30 | let first_cluster = alloc.first_cluster(); 31 | let len = stream.valid_data_length(); 32 | let reader = if first_cluster == 0 { 33 | None 34 | } else { 35 | match ClustersReader::new( 36 | disk.clone(), 37 | params.clone(), 38 | fat, 39 | first_cluster, 40 | Some(len), 41 | Some(stream.no_fat_chain()), 42 | ) { 43 | Ok(v) => Some(v), 44 | Err(e) => return Err(NewError::CreateClustersReaderFailed(first_cluster, len, e)), 45 | } 46 | }; 47 | 48 | Ok(Self { 49 | name, 50 | len, 51 | reader, 52 | timestamps, 53 | }) 54 | } 55 | 56 | pub fn name(&self) -> &str { 57 | self.name.as_ref() 58 | } 59 | 60 | pub fn is_empty(&self) -> bool { 61 | self.len == 0 62 | } 63 | 64 | pub fn len(&self) -> u64 { 65 | self.len 66 | } 67 | 68 | pub fn timestamps(&self) -> &Timestamps { 69 | &self.timestamps 70 | } 71 | } 72 | 73 | #[cfg(feature = "std")] 74 | impl std::io::Seek for File { 75 | fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { 76 | use std::io::{Error, ErrorKind, SeekFrom}; 77 | 78 | // Check if empty file. 79 | let r = match &mut self.reader { 80 | Some(v) => v, 81 | None => return std::io::empty().seek(pos), 82 | }; 83 | 84 | // Get absolute offset. 85 | let o = match pos { 86 | SeekFrom::Start(v) => min(v, r.data_length()), 87 | SeekFrom::End(v) => { 88 | if v >= 0 { 89 | r.data_length() 90 | } else if let Some(v) = r.data_length().checked_sub(v.unsigned_abs()) { 91 | v 92 | } else { 93 | return Err(Error::from(ErrorKind::InvalidInput)); 94 | } 95 | } 96 | SeekFrom::Current(v) => v.try_into().map_or_else( 97 | |_| { 98 | r.stream_position() 99 | .checked_sub(v.unsigned_abs()) 100 | .ok_or_else(|| Error::from(ErrorKind::InvalidInput)) 101 | }, 102 | |v| Ok(min(r.stream_position().saturating_add(v), r.data_length())), 103 | )?, 104 | }; 105 | 106 | assert!(r.seek(o)); 107 | 108 | Ok(o) 109 | } 110 | 111 | fn rewind(&mut self) -> std::io::Result<()> { 112 | let r = match &mut self.reader { 113 | Some(v) => v, 114 | None => return Ok(()), 115 | }; 116 | 117 | r.rewind(); 118 | 119 | Ok(()) 120 | } 121 | 122 | fn stream_position(&mut self) -> std::io::Result { 123 | let r = match &mut self.reader { 124 | Some(v) => v, 125 | None => return Ok(0), 126 | }; 127 | 128 | Ok(r.stream_position()) 129 | } 130 | } 131 | 132 | #[cfg(feature = "std")] 133 | impl std::io::Read for File { 134 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 135 | match &mut self.reader { 136 | Some(v) => v.read(buf), 137 | None => Ok(0), 138 | } 139 | } 140 | } 141 | 142 | /// Represents an error for [`File::new()`]. 143 | #[derive(Debug, Error)] 144 | pub enum NewError { 145 | #[error("cannot create a clusters reader for allocation {0}:{1}")] 146 | CreateClustersReaderFailed(usize, u64, #[source] crate::cluster::NewError), 147 | } 148 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::directory::*; 2 | pub use self::disk::*; 3 | 4 | use self::cluster::ClustersReader; 5 | use self::entries::{ClusterAllocation, EntriesReader, EntryType, FileEntry}; 6 | use self::fat::Fat; 7 | use self::file::File; 8 | use self::param::Params; 9 | use byteorder::{ByteOrder, LE}; 10 | use core::fmt::Debug; 11 | use std::sync::Arc; 12 | use thiserror::Error; 13 | 14 | mod cluster; 15 | mod directory; 16 | mod disk; 17 | mod entries; 18 | pub mod fat; 19 | pub mod file; 20 | pub mod param; 21 | pub mod timestamp; 22 | 23 | extern crate alloc; 24 | 25 | /// Represents a root directory in exFAT. 26 | /// 27 | /// This implementation follows the official specs 28 | /// https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification. 29 | pub struct Root { 30 | volume_label: Option, 31 | items: Vec>, 32 | } 33 | 34 | impl Root

{ 35 | pub fn open(partition: P) -> Result> { 36 | // Read boot sector. 37 | let mut boot = [0u8; 512]; 38 | 39 | if let Err(e) = partition.read_exact(0, &mut boot) { 40 | return Err(RootError::ReadMainBootFailed(e)); 41 | } 42 | 43 | // Check type. 44 | if &boot[3..11] != b"EXFAT " || !boot[11..64].iter().all(|&b| b == 0) { 45 | return Err(RootError::NotExFat); 46 | } 47 | 48 | // Load fields. 49 | let params = Arc::new(Params { 50 | fat_offset: LE::read_u32(&boot[80..]) as u64, 51 | fat_length: LE::read_u32(&boot[84..]) as u64, 52 | cluster_heap_offset: LE::read_u32(&boot[88..]) as u64, 53 | cluster_count: LE::read_u32(&boot[92..]) as usize, 54 | first_cluster_of_root_directory: LE::read_u32(&boot[96..]) as usize, 55 | volume_flags: LE::read_u16(&boot[106..]).into(), 56 | bytes_per_sector: { 57 | let v = boot[108]; 58 | 59 | if (9..=12).contains(&v) { 60 | 1u64 << v 61 | } else { 62 | return Err(RootError::InvalidBytesPerSectorShift); 63 | } 64 | }, 65 | sectors_per_cluster: { 66 | let v = boot[109]; 67 | 68 | // No need to check if subtraction is underflow because we already checked for the 69 | // valid value on the above. 70 | if v <= (25 - boot[108]) { 71 | 1u64 << v 72 | } else { 73 | return Err(RootError::InvalidSectorsPerClusterShift); 74 | } 75 | }, 76 | number_of_fats: { 77 | let v = boot[110]; 78 | 79 | if v == 1 || v == 2 { 80 | v 81 | } else { 82 | return Err(RootError::InvalidNumberOfFats); 83 | } 84 | }, 85 | }); 86 | 87 | // Read FAT region. 88 | let active_fat = params.volume_flags.active_fat(); 89 | let fat = if active_fat == 0 || params.number_of_fats == 2 { 90 | match Fat::load(¶ms, &partition, active_fat) { 91 | Ok(v) => Arc::new(v), 92 | Err(e) => return Err(RootError::ReadFatRegionFailed(e)), 93 | } 94 | } else { 95 | return Err(RootError::InvalidNumberOfFats); 96 | }; 97 | 98 | // Create a entries reader for the root directory. 99 | let disk = Arc::new(partition); 100 | let root_cluster = params.first_cluster_of_root_directory; 101 | let mut reader = match ClustersReader::new(&disk, ¶ms, &fat, root_cluster, None, None) { 102 | Ok(v) => EntriesReader::new(v), 103 | Err(e) => return Err(RootError::CreateClustersReaderFailed(e)), 104 | }; 105 | 106 | // Load root directory. 107 | let mut allocation_bitmaps: [Option; 2] = [None, None]; 108 | let mut upcase_table: Option<()> = None; 109 | let mut volume_label: Option = None; 110 | let mut items: Vec> = Vec::new(); 111 | 112 | loop { 113 | // Read primary entry. 114 | let entry = match reader.read() { 115 | Ok(v) => v, 116 | Err(e) => return Err(RootError::ReadEntryFailed(e)), 117 | }; 118 | 119 | // Check entry type. 120 | let ty = entry.ty(); 121 | 122 | if !ty.is_regular() { 123 | break; 124 | } else if ty.type_category() != EntryType::PRIMARY { 125 | return Err(RootError::NotPrimaryEntry(entry.index(), entry.cluster())); 126 | } 127 | 128 | // Parse primary entry. 129 | match (ty.type_importance(), ty.type_code()) { 130 | (EntryType::CRITICAL, 1) => { 131 | // Get next index. 132 | let index = if allocation_bitmaps[1].is_some() { 133 | return Err(RootError::TooManyAllocationBitmap); 134 | } else if allocation_bitmaps[0].is_some() { 135 | 1 136 | } else { 137 | 0 138 | }; 139 | 140 | // Load fields. 141 | let data = entry.data(); 142 | let bitmap_flags = data[1] as usize; 143 | 144 | if (bitmap_flags & 1) != index { 145 | return Err(RootError::WrongAllocationBitmap); 146 | } 147 | 148 | allocation_bitmaps[index] = match ClusterAllocation::load(&entry) { 149 | Ok(v) => Some(v), 150 | Err(e) => { 151 | return Err(RootError::ReadClusterAllocationFailed( 152 | entry.index(), 153 | entry.cluster(), 154 | e, 155 | )); 156 | } 157 | }; 158 | } 159 | (EntryType::CRITICAL, 2) => { 160 | // Check if more than one up-case table. 161 | if upcase_table.is_some() { 162 | return Err(RootError::MultipleUpcaseTable); 163 | } 164 | 165 | // Load fields. 166 | if let Err(e) = ClusterAllocation::load(&entry) { 167 | return Err(RootError::ReadClusterAllocationFailed( 168 | entry.index(), 169 | entry.cluster(), 170 | e, 171 | )); 172 | } 173 | 174 | upcase_table = Some(()); 175 | } 176 | (EntryType::CRITICAL, 3) => { 177 | // Check if more than one volume label. 178 | if volume_label.is_some() { 179 | return Err(RootError::MultipleVolumeLabel); 180 | } 181 | 182 | // Load fields. 183 | let data = entry.data(); 184 | let character_count = data[1] as usize; 185 | 186 | if character_count > 11 { 187 | return Err(RootError::InvalidVolumeLabel); 188 | } 189 | 190 | let raw_label = &data[2..(2 + character_count * 2)]; 191 | 192 | // Convert the label from little endian to native endian. 193 | let mut label = [0u16; 11]; 194 | let label = &mut label[..character_count]; 195 | 196 | LE::read_u16_into(raw_label, label); 197 | 198 | volume_label = Some(String::from_utf16_lossy(label)); 199 | } 200 | (EntryType::CRITICAL, 5) => { 201 | // Load the entry. 202 | let file = match FileEntry::load(&entry, &mut reader) { 203 | Ok(v) => v, 204 | Err(e) => return Err(RootError::LoadFileEntryFailed(e)), 205 | }; 206 | 207 | let name = file.name; 208 | let attrs = file.attributes; 209 | let stream = file.stream; 210 | let timestamps = file.timestamps; 211 | 212 | // Add to the list. 213 | items.push(if attrs.is_directory() { 214 | Item::Directory(Directory::new( 215 | disk.clone(), 216 | params.clone(), 217 | fat.clone(), 218 | name, 219 | stream, 220 | timestamps, 221 | )) 222 | } else { 223 | match File::new(&disk, ¶ms, &fat, name, stream, timestamps) { 224 | Ok(v) => Item::File(v), 225 | Err(e) => { 226 | return Err(RootError::CreateFileObjectFailed( 227 | entry.index(), 228 | entry.cluster(), 229 | e, 230 | )); 231 | } 232 | } 233 | }); 234 | } 235 | _ => return Err(RootError::UnknownEntry(entry.index(), entry.cluster())), 236 | } 237 | } 238 | 239 | // Check allocation bitmap count. 240 | if params.number_of_fats == 2 { 241 | if allocation_bitmaps[1].is_none() { 242 | return Err(RootError::NoAllocationBitmap); 243 | } 244 | } else if allocation_bitmaps[0].is_none() { 245 | return Err(RootError::NoAllocationBitmap); 246 | } 247 | 248 | // Check Up-case Table. 249 | if upcase_table.is_none() { 250 | return Err(RootError::NoUpcaseTable); 251 | } 252 | 253 | Ok(Self { 254 | volume_label, 255 | items, 256 | }) 257 | } 258 | 259 | pub fn volume_label(&self) -> Option<&str> { 260 | self.volume_label.as_deref() 261 | } 262 | } 263 | 264 | impl IntoIterator for Root

{ 265 | type Item = Item

; 266 | type IntoIter = std::vec::IntoIter>; 267 | 268 | fn into_iter(self) -> Self::IntoIter { 269 | self.items.into_iter() 270 | } 271 | } 272 | 273 | /// Represents FileAttributes in the File Directory Entry. 274 | #[derive(Clone, Copy)] 275 | #[repr(transparent)] 276 | pub struct FileAttributes(u16); 277 | 278 | impl FileAttributes { 279 | pub fn is_read_only(self) -> bool { 280 | (self.0 & 0x0001) != 0 281 | } 282 | 283 | pub fn is_hidden(self) -> bool { 284 | (self.0 & 0x0002) != 0 285 | } 286 | 287 | pub fn is_system(self) -> bool { 288 | (self.0 & 0x0004) != 0 289 | } 290 | 291 | pub fn is_directory(self) -> bool { 292 | (self.0 & 0x0010) != 0 293 | } 294 | 295 | pub fn is_archive(self) -> bool { 296 | (self.0 & 0x0020) != 0 297 | } 298 | } 299 | 300 | /// Represents an error when [`Root::open()`] fails. 301 | #[derive(Error)] 302 | pub enum RootError { 303 | #[error("cannot read main boot region")] 304 | ReadMainBootFailed(#[source] P::Err), 305 | 306 | #[error("image is not exFAT")] 307 | NotExFat, 308 | 309 | #[error("invalid BytesPerSectorShift")] 310 | InvalidBytesPerSectorShift, 311 | 312 | #[error("invalid SectorsPerClusterShift")] 313 | InvalidSectorsPerClusterShift, 314 | 315 | #[error("invalid NumberOfFats")] 316 | InvalidNumberOfFats, 317 | 318 | #[error("cannot read FAT region")] 319 | ReadFatRegionFailed(#[source] self::fat::LoadError

), 320 | 321 | #[error("cannot create a clusters reader")] 322 | CreateClustersReaderFailed(#[source] cluster::NewError), 323 | 324 | #[error("cannot read a directory entry")] 325 | ReadEntryFailed(#[source] entries::ReaderError), 326 | 327 | #[error("directory entry #{0} on cluster #{1} is not a primary entry")] 328 | NotPrimaryEntry(usize, usize), 329 | 330 | #[error("more than 2 allocation bitmaps exists in the root directory")] 331 | TooManyAllocationBitmap, 332 | 333 | #[error("allocation bitmap in the root directory is not for its corresponding FAT")] 334 | WrongAllocationBitmap, 335 | 336 | #[error("multiple up-case table exists in the root directory")] 337 | MultipleUpcaseTable, 338 | 339 | #[error("multiple volume label exists in the root directory")] 340 | MultipleVolumeLabel, 341 | 342 | #[error("invalid volume label")] 343 | InvalidVolumeLabel, 344 | 345 | #[error("cannot load file entry in the root directory")] 346 | LoadFileEntryFailed(#[source] entries::FileEntryError), 347 | 348 | #[error("cannot create a file object for directory entry #{0} on cluster #{1}")] 349 | CreateFileObjectFailed(usize, usize, #[source] file::NewError), 350 | 351 | #[error("cannot read cluster allocation for entry #{0} on cluster #{1}")] 352 | ReadClusterAllocationFailed(usize, usize, #[source] entries::ClusterAllocationError), 353 | 354 | #[error("unknown directory entry #{0} on cluster #{1}")] 355 | UnknownEntry(usize, usize), 356 | 357 | #[error("no Allocation Bitmap available for active FAT")] 358 | NoAllocationBitmap, 359 | 360 | #[error("no Up-case Table available")] 361 | NoUpcaseTable, 362 | } 363 | 364 | impl Debug for RootError

{ 365 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 366 | match self { 367 | Self::ReadMainBootFailed(arg0) => { 368 | f.debug_tuple("ReadMainBootFailed").field(arg0).finish() 369 | } 370 | Self::NotExFat => write!(f, "NotExFat"), 371 | Self::InvalidBytesPerSectorShift => write!(f, "InvalidBytesPerSectorShift"), 372 | Self::InvalidSectorsPerClusterShift => write!(f, "InvalidSectorsPerClusterShift"), 373 | Self::InvalidNumberOfFats => write!(f, "InvalidNumberOfFats"), 374 | Self::ReadFatRegionFailed(arg0) => { 375 | f.debug_tuple("ReadFatRegionFailed").field(arg0).finish() 376 | } 377 | Self::CreateClustersReaderFailed(arg0) => f 378 | .debug_tuple("CreateClustersReaderFailed") 379 | .field(arg0) 380 | .finish(), 381 | Self::ReadEntryFailed(arg0) => f.debug_tuple("ReadEntryFailed").field(arg0).finish(), 382 | Self::NotPrimaryEntry(arg0, arg1) => f 383 | .debug_tuple("NotPrimaryEntry") 384 | .field(arg0) 385 | .field(arg1) 386 | .finish(), 387 | Self::TooManyAllocationBitmap => write!(f, "TooManyAllocationBitmap"), 388 | Self::WrongAllocationBitmap => write!(f, "WrongAllocationBitmap"), 389 | Self::MultipleUpcaseTable => write!(f, "MultipleUpcaseTable"), 390 | Self::MultipleVolumeLabel => write!(f, "MultipleVolumeLabel"), 391 | Self::InvalidVolumeLabel => write!(f, "InvalidVolumeLabel"), 392 | Self::LoadFileEntryFailed(arg0) => { 393 | f.debug_tuple("LoadFileEntryFailed").field(arg0).finish() 394 | } 395 | Self::CreateFileObjectFailed(arg0, arg1, arg2) => f 396 | .debug_tuple("CreateFileObjectFailed") 397 | .field(arg0) 398 | .field(arg1) 399 | .field(arg2) 400 | .finish(), 401 | Self::ReadClusterAllocationFailed(arg0, arg1, arg2) => f 402 | .debug_tuple("ReadClusterAllocationFailed") 403 | .field(arg0) 404 | .field(arg1) 405 | .field(arg2) 406 | .finish(), 407 | Self::UnknownEntry(arg0, arg1) => f 408 | .debug_tuple("UnknownEntry") 409 | .field(arg0) 410 | .field(arg1) 411 | .finish(), 412 | Self::NoAllocationBitmap => write!(f, "NoAllocationBitmap"), 413 | Self::NoUpcaseTable => write!(f, "NoUpcaseTable"), 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/param.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct Params { 2 | pub fat_offset: u64, // in sector 3 | pub fat_length: u64, // in sector 4 | pub cluster_heap_offset: u64, // in sector 5 | pub cluster_count: usize, // not including the first 2 pseudo clusters 6 | pub first_cluster_of_root_directory: usize, 7 | pub volume_flags: VolumeFlags, 8 | pub bytes_per_sector: u64, 9 | pub sectors_per_cluster: u64, 10 | pub number_of_fats: u8, 11 | } 12 | 13 | impl Params { 14 | /// Calculates offset in the image of a specified cluster. 15 | pub fn cluster_offset(&self, index: usize) -> Option { 16 | if index < 2 { 17 | return None; 18 | } 19 | 20 | let index = index - 2; 21 | 22 | if index >= self.cluster_count { 23 | return None; 24 | } 25 | 26 | let sector = self.cluster_heap_offset + self.sectors_per_cluster * index as u64; 27 | let offset = self.bytes_per_sector * sector; 28 | 29 | Some(offset) 30 | } 31 | 32 | /// Gets the size of cluster, in bytes. 33 | pub fn cluster_size(&self) -> u64 { 34 | self.bytes_per_sector * self.sectors_per_cluster 35 | } 36 | } 37 | 38 | #[derive(Clone, Copy)] 39 | #[repr(transparent)] 40 | pub(crate) struct VolumeFlags(u16); 41 | 42 | impl VolumeFlags { 43 | pub fn active_fat(self) -> usize { 44 | (self.0 & 1) as usize 45 | } 46 | } 47 | 48 | impl From for VolumeFlags { 49 | fn from(v: u16) -> Self { 50 | Self(v) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/timestamp.rs: -------------------------------------------------------------------------------- 1 | pub struct Timestamps { 2 | created: Timestamp, 3 | modified: Timestamp, 4 | accessed: Timestamp, 5 | } 6 | 7 | impl Timestamps { 8 | pub fn new(created: Timestamp, modified: Timestamp, accessed: Timestamp) -> Self { 9 | Timestamps { 10 | created, 11 | modified, 12 | accessed, 13 | } 14 | } 15 | 16 | pub fn created(&self) -> &Timestamp { 17 | &self.created 18 | } 19 | 20 | pub fn modified(&self) -> &Timestamp { 21 | &self.modified 22 | } 23 | 24 | pub fn accessed(&self) -> &Timestamp { 25 | &self.accessed 26 | } 27 | } 28 | 29 | pub struct Timestamp { 30 | timestamp: u32, 31 | ms_increment: u8, 32 | // Offset from UTC in 15 minute intervals 33 | utc_offset: i8, 34 | } 35 | 36 | pub struct Date { 37 | pub day: u8, 38 | pub month: u8, 39 | pub year: u16, 40 | } 41 | 42 | pub struct Time { 43 | pub hour: u8, 44 | pub minute: u8, 45 | pub second: u8, 46 | } 47 | 48 | // See https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#748-timestamp-fields 49 | impl Timestamp { 50 | pub fn new(timestamp: u32, ms_increment: u8, utc_offset: i8) -> Self { 51 | Timestamp { 52 | timestamp, 53 | ms_increment, 54 | utc_offset, 55 | } 56 | } 57 | 58 | pub fn date(&self) -> Date { 59 | Date { 60 | day: ((self.timestamp >> 16) & 0x1F) as u8, 61 | month: ((self.timestamp >> 21) & 0xF) as u8, 62 | year: 1980 + ((self.timestamp >> 25) & 0x7F) as u16, 63 | } 64 | } 65 | 66 | pub fn time(&self) -> Time { 67 | Time { 68 | second: (self.ms_increment as u16 / 1000) as u8 + (self.timestamp & 0x1F) as u8 * 2, 69 | minute: ((self.timestamp >> 5) & 0x3f) as u8, 70 | hour: ((self.timestamp >> 11) & 0x1F) as u8, 71 | } 72 | } 73 | 74 | pub fn utc_offset(&self) -> i8 { 75 | self.utc_offset 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/exfat.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obhq/exfat/b930023e06363371b1f26df1f45e17eaa0c7a290/tests/exfat.img -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use exfat::timestamp::Timestamp; 2 | use exfat::{Item, Root}; 3 | use std::fs::File; 4 | use std::io::Read; 5 | use std::path::PathBuf; 6 | 7 | fn check_timestamp( 8 | ts: &Timestamp, 9 | day: u8, 10 | month: u8, 11 | year: u16, 12 | hour: u8, 13 | minute: u8, 14 | second: u8, 15 | utc_offset: i8, 16 | ) { 17 | assert_eq!(day, ts.date().day); 18 | assert_eq!(month, ts.date().month); 19 | assert_eq!(year, ts.date().year); 20 | assert_eq!(hour, ts.time().hour); 21 | assert_eq!(minute, ts.time().minute); 22 | assert_eq!(second, ts.time().second); 23 | assert_eq!(utc_offset, ts.utc_offset()); 24 | } 25 | 26 | #[test] 27 | fn read_image() { 28 | // Open the image. 29 | let image: PathBuf = ["tests", "exfat.img"].iter().collect(); 30 | let image = File::open(image).expect("cannot open exfat.img"); 31 | 32 | // Open root directory. 33 | let root = Root::open(image).expect("cannot open the root directory"); 34 | 35 | // Check image properties. 36 | assert_eq!(Some("Test image"), root.volume_label()); 37 | 38 | // Check items in the root of image. 39 | let items = Vec::from_iter(root.into_iter()); 40 | 41 | assert_eq!(2, items.len()); 42 | 43 | for i in items { 44 | match i { 45 | Item::Directory(d) => { 46 | // Check directory properties. 47 | assert_eq!("dir1", d.name()); 48 | 49 | // Check timestamps 50 | check_timestamp(d.timestamps().created(), 6, 3, 2023, 13, 2, 32, 0); 51 | check_timestamp(d.timestamps().modified(), 6, 3, 2023, 13, 3, 18, 0); 52 | check_timestamp(d.timestamps().accessed(), 6, 3, 2023, 13, 2, 32, 0); 53 | 54 | // Check items. 55 | let mut items = d.open().expect("cannot open dir1"); 56 | 57 | assert_eq!(1, items.len()); 58 | 59 | match items.remove(0) { 60 | Item::Directory(_) => panic!("unexpected item in dir1"), 61 | Item::File(mut f) => { 62 | // Check file properties. 63 | assert_eq!("file2", f.name()); 64 | assert_eq!(13, f.len()); 65 | 66 | // Check file content. 67 | let mut c = String::new(); 68 | 69 | f.read_to_string(&mut c).expect("cannot read file2"); 70 | 71 | assert_eq!("Test file 2.\n", c); 72 | 73 | // Check timestamps 74 | check_timestamp(f.timestamps().created(), 6, 3, 2023, 13, 3, 18, 0); 75 | check_timestamp(f.timestamps().modified(), 6, 3, 2023, 13, 3, 18, 0); 76 | check_timestamp(f.timestamps().accessed(), 6, 3, 2023, 13, 3, 18, 0); 77 | } 78 | }; 79 | } 80 | Item::File(mut f) => { 81 | // Check file properties. 82 | assert_eq!("file1", f.name()); 83 | assert_eq!(13, f.len()); 84 | 85 | // Check file content. 86 | let mut c = String::new(); 87 | 88 | f.read_to_string(&mut c).expect("cannot read file1"); 89 | 90 | assert_eq!("Test file 1.\n", c); 91 | 92 | // Check timestamps 93 | check_timestamp(f.timestamps().created(), 6, 3, 2023, 13, 3, 6, 0); 94 | check_timestamp(f.timestamps().modified(), 6, 3, 2023, 13, 3, 6, 0); 95 | check_timestamp(f.timestamps().accessed(), 6, 3, 2023, 13, 3, 6, 0); 96 | } 97 | } 98 | } 99 | } 100 | --------------------------------------------------------------------------------