use alloc::string::String;
use alloc::vec::Vec;
use hadris_common::types::{
endian::{Endian, LittleEndian},
number::{U16, U32, U64},
};
use super::time::ExFatTimestamp;
pub mod entry_type {
pub const END_OF_DIRECTORY: u8 = 0x00;
pub const ALLOCATION_BITMAP: u8 = 0x81;
pub const UPCASE_TABLE: u8 = 0x82;
pub const VOLUME_LABEL: u8 = 0x83;
pub const FILE_DIRECTORY: u8 = 0x85;
pub const VOLUME_GUID: u8 = 0xA0;
pub const TEXFAT_PADDING: u8 = 0xA1;
pub const ACCESS_CONTROL: u8 = 0xA2;
pub const STREAM_EXTENSION: u8 = 0xC0;
pub const FILE_NAME: u8 = 0xC1;
pub const DELETED_FILE: u8 = 0x05;
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileAttributes: u16 {
const READ_ONLY = 0x0001;
const HIDDEN = 0x0002;
const SYSTEM = 0x0004;
const DIRECTORY = 0x0010;
const ARCHIVE = 0x0020;
}
}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawFileDirectoryEntry {
pub entry_type: u8,
pub secondary_count: u8,
pub set_checksum: U16<LittleEndian>,
pub file_attributes: U16<LittleEndian>,
pub reserved1: U16<LittleEndian>,
pub create_timestamp: U32<LittleEndian>,
pub last_modified_timestamp: U32<LittleEndian>,
pub last_accessed_timestamp: U32<LittleEndian>,
pub create_10ms_increment: u8,
pub last_modified_10ms_increment: u8,
pub create_utc_offset: u8,
pub last_modified_utc_offset: u8,
pub last_accessed_utc_offset: u8,
pub reserved2: [u8; 7],
}
unsafe impl bytemuck::NoUninit for RawFileDirectoryEntry {}
unsafe impl bytemuck::Zeroable for RawFileDirectoryEntry {}
unsafe impl bytemuck::AnyBitPattern for RawFileDirectoryEntry {}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawStreamExtensionEntry {
pub entry_type: u8,
pub general_secondary_flags: u8,
pub reserved1: u8,
pub name_length: u8,
pub name_hash: U16<LittleEndian>,
pub reserved2: U16<LittleEndian>,
pub valid_data_length: U64<LittleEndian>,
pub reserved3: U32<LittleEndian>,
pub first_cluster: U32<LittleEndian>,
pub data_length: U64<LittleEndian>,
}
unsafe impl bytemuck::NoUninit for RawStreamExtensionEntry {}
unsafe impl bytemuck::Zeroable for RawStreamExtensionEntry {}
unsafe impl bytemuck::AnyBitPattern for RawStreamExtensionEntry {}
impl RawStreamExtensionEntry {
pub fn is_contiguous(&self) -> bool {
(self.general_secondary_flags & 0x02) != 0
}
}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawFileNameEntry {
pub entry_type: u8,
pub general_secondary_flags: u8,
pub file_name: [u8; 30],
}
unsafe impl bytemuck::NoUninit for RawFileNameEntry {}
unsafe impl bytemuck::Zeroable for RawFileNameEntry {}
unsafe impl bytemuck::AnyBitPattern for RawFileNameEntry {}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawAllocationBitmapEntry {
pub entry_type: u8,
pub bitmap_flags: u8,
pub reserved: [u8; 18],
pub first_cluster: U32<LittleEndian>,
pub data_length: U64<LittleEndian>,
}
unsafe impl bytemuck::NoUninit for RawAllocationBitmapEntry {}
unsafe impl bytemuck::Zeroable for RawAllocationBitmapEntry {}
unsafe impl bytemuck::AnyBitPattern for RawAllocationBitmapEntry {}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawUpcaseTableEntry {
pub entry_type: u8,
pub reserved1: [u8; 3],
pub table_checksum: U32<LittleEndian>,
pub reserved2: [u8; 12],
pub first_cluster: U32<LittleEndian>,
pub data_length: U64<LittleEndian>,
}
unsafe impl bytemuck::NoUninit for RawUpcaseTableEntry {}
unsafe impl bytemuck::Zeroable for RawUpcaseTableEntry {}
unsafe impl bytemuck::AnyBitPattern for RawUpcaseTableEntry {}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct RawVolumeLabelEntry {
pub entry_type: u8,
pub character_count: u8,
pub volume_label: [u8; 22],
pub reserved: [u8; 8],
}
unsafe impl bytemuck::NoUninit for RawVolumeLabelEntry {}
unsafe impl bytemuck::Zeroable for RawVolumeLabelEntry {}
unsafe impl bytemuck::AnyBitPattern for RawVolumeLabelEntry {}
#[repr(C)]
#[derive(Clone, Copy)]
pub union RawDirectoryEntry {
pub entry_type: u8,
pub file: RawFileDirectoryEntry,
pub stream: RawStreamExtensionEntry,
pub name: RawFileNameEntry,
pub bitmap: RawAllocationBitmapEntry,
pub upcase: RawUpcaseTableEntry,
pub label: RawVolumeLabelEntry,
pub bytes: [u8; 32],
}
unsafe impl bytemuck::NoUninit for RawDirectoryEntry {}
unsafe impl bytemuck::Zeroable for RawDirectoryEntry {}
unsafe impl bytemuck::AnyBitPattern for RawDirectoryEntry {}
#[derive(Debug, Clone)]
pub struct ExFatFileEntry {
pub name: String,
pub attributes: FileAttributes,
pub first_cluster: u32,
pub data_length: u64,
pub valid_data_length: u64,
pub no_fat_chain: bool,
pub name_hash: u16,
pub created: ExFatTimestamp,
pub modified: ExFatTimestamp,
pub accessed: ExFatTimestamp,
pub(crate) parent_cluster: u32,
pub(crate) entry_offset: u64,
}
impl ExFatFileEntry {
pub fn is_directory(&self) -> bool {
self.attributes.contains(FileAttributes::DIRECTORY)
}
pub fn is_file(&self) -> bool {
!self.is_directory()
}
pub fn size(&self) -> u64 {
self.valid_data_length
}
}
pub fn compute_entry_set_checksum(entries: &[RawDirectoryEntry]) -> u16 {
let mut checksum: u16 = 0;
for (entry_idx, entry) in entries.iter().enumerate() {
let bytes = unsafe { &entry.bytes };
for (byte_idx, &byte) in bytes.iter().enumerate() {
if entry_idx == 0 && (byte_idx == 2 || byte_idx == 3) {
continue;
}
checksum = checksum.rotate_right(1).wrapping_add(byte as u16);
}
}
checksum
}
pub fn parse_entry_set(entries: &[RawDirectoryEntry]) -> Option<(ExFatFileEntry, usize)> {
if entries.is_empty() {
return None;
}
let primary = unsafe { &entries[0].file };
if primary.entry_type != entry_type::FILE_DIRECTORY {
return None;
}
let secondary_count = primary.secondary_count as usize;
if secondary_count < 2 || secondary_count > 18 {
return None; }
let total_entries = 1 + secondary_count;
if entries.len() < total_entries {
return None; }
let stream = unsafe { &entries[1].stream };
if stream.entry_type != entry_type::STREAM_EXTENSION {
return None;
}
let name_length = stream.name_length as usize;
let mut name_chars: Vec<u16> = Vec::with_capacity(name_length);
for i in 2..total_entries {
let name_entry = unsafe { &entries[i].name };
if name_entry.entry_type != entry_type::FILE_NAME {
return None; }
for j in 0..15 {
if name_chars.len() >= name_length {
break;
}
let char_offset = j * 2;
let code_unit = u16::from_le_bytes([
name_entry.file_name[char_offset],
name_entry.file_name[char_offset + 1],
]);
name_chars.push(code_unit);
}
}
let name = String::from_utf16_lossy(&name_chars);
let created = ExFatTimestamp::new(
primary.create_timestamp.get(),
primary.create_10ms_increment,
primary.create_utc_offset,
);
let modified = ExFatTimestamp::new(
primary.last_modified_timestamp.get(),
primary.last_modified_10ms_increment,
primary.last_modified_utc_offset,
);
let accessed = ExFatTimestamp::new(
primary.last_accessed_timestamp.get(),
0, primary.last_accessed_utc_offset,
);
let entry = ExFatFileEntry {
name,
attributes: FileAttributes::from_bits_truncate(primary.file_attributes.get()),
first_cluster: stream.first_cluster.get(),
data_length: stream.data_length.get(),
valid_data_length: stream.valid_data_length.get(),
no_fat_chain: stream.is_contiguous(),
name_hash: stream.name_hash.get(),
created,
modified,
accessed,
parent_cluster: 0, entry_offset: 0, };
Some((entry, total_entries))
}
#[cfg(test)]
mod tests {
use super::*;
use core::mem::size_of;
use static_assertions::const_assert_eq;
const_assert_eq!(size_of::<RawFileDirectoryEntry>(), 32);
const_assert_eq!(size_of::<RawStreamExtensionEntry>(), 32);
const_assert_eq!(size_of::<RawFileNameEntry>(), 32);
const_assert_eq!(size_of::<RawAllocationBitmapEntry>(), 32);
const_assert_eq!(size_of::<RawUpcaseTableEntry>(), 32);
const_assert_eq!(size_of::<RawVolumeLabelEntry>(), 32);
const_assert_eq!(size_of::<RawDirectoryEntry>(), 32);
}