use alloc::string::String;
use alloc::vec::Vec;
use hadris_common::types::endian::{Endian, LittleEndian};
use hadris_common::types::number::{U16, U32, U64};
use super::entry::{
FileAttributes, RawDirectoryEntry, RawFileDirectoryEntry, RawFileNameEntry,
RawStreamExtensionEntry, compute_entry_set_checksum, entry_type,
};
use super::time::ExFatTimestamp;
use super::upcase::UpcaseTable;
use crate::error::{FatError, Result};
pub const MAX_NAME_LENGTH: usize = 255;
const CHARS_PER_NAME_ENTRY: usize = 15;
#[derive(Debug, Clone)]
pub struct EntrySetBuilder {
name: String,
attributes: FileAttributes,
first_cluster: u32,
valid_data_length: u64,
data_length: u64,
is_contiguous: bool,
created: ExFatTimestamp,
modified: ExFatTimestamp,
accessed: ExFatTimestamp,
}
impl EntrySetBuilder {
pub fn file(name: &str) -> Result<Self> {
Self::new(name, FileAttributes::ARCHIVE)
}
pub fn directory(name: &str) -> Result<Self> {
Self::new(name, FileAttributes::DIRECTORY)
}
fn new(name: &str, attributes: FileAttributes) -> Result<Self> {
let name_len: usize = name.encode_utf16().count();
if name_len == 0 || name_len > MAX_NAME_LENGTH {
return Err(FatError::InvalidFilename);
}
for c in name.chars() {
if is_invalid_char(c) {
return Err(FatError::InvalidFilename);
}
}
let now = ExFatTimestamp::now();
Ok(Self {
name: name.into(),
attributes,
first_cluster: 0,
valid_data_length: 0,
data_length: 0,
is_contiguous: true, created: now.clone(),
modified: now.clone(),
accessed: now,
})
}
pub fn with_cluster(mut self, cluster: u32) -> Self {
self.first_cluster = cluster;
self
}
pub fn with_size(mut self, valid_length: u64, allocated_length: u64) -> Self {
self.valid_data_length = valid_length;
self.data_length = allocated_length;
self
}
pub fn with_contiguous(mut self, contiguous: bool) -> Self {
self.is_contiguous = contiguous;
self
}
pub fn with_timestamps(
mut self,
created: ExFatTimestamp,
modified: ExFatTimestamp,
accessed: ExFatTimestamp,
) -> Self {
self.created = created;
self.modified = modified;
self.accessed = accessed;
self
}
pub fn build(&self, upcase: &UpcaseTable) -> Vec<RawDirectoryEntry> {
let name_utf16: Vec<u16> = self.name.encode_utf16().collect();
let name_entry_count = (name_utf16.len() + CHARS_PER_NAME_ENTRY - 1) / CHARS_PER_NAME_ENTRY;
let secondary_count = 1 + name_entry_count;
let mut entries = Vec::with_capacity(1 + secondary_count);
let file_entry = self.build_file_entry(secondary_count as u8);
entries.push(to_raw_entry(&file_entry));
let name_hash = upcase.name_hash(&self.name);
let stream_entry = self.build_stream_entry(name_utf16.len() as u8, name_hash);
entries.push(to_raw_entry(&stream_entry));
for (i, chunk) in name_utf16.chunks(CHARS_PER_NAME_ENTRY).enumerate() {
let name_entry = self.build_name_entry(chunk, i == 0);
entries.push(to_raw_entry(&name_entry));
}
let checksum = compute_entry_set_checksum(&entries);
let file_entry_bytes = unsafe { &mut entries[0].file };
file_entry_bytes.set_checksum = U16::<LittleEndian>::new(checksum);
entries
}
pub fn entry_count(&self) -> usize {
let name_utf16_len = self.name.encode_utf16().count();
let name_entry_count = (name_utf16_len + CHARS_PER_NAME_ENTRY - 1) / CHARS_PER_NAME_ENTRY;
1 + 1 + name_entry_count }
fn build_file_entry(&self, secondary_count: u8) -> RawFileDirectoryEntry {
let (create_ts, create_10ms, create_utc) = self.created.to_raw();
let (modify_ts, modify_10ms, modify_utc) = self.modified.to_raw();
let (access_ts, _, access_utc) = self.accessed.to_raw();
RawFileDirectoryEntry {
entry_type: entry_type::FILE_DIRECTORY,
secondary_count,
set_checksum: U16::<LittleEndian>::new(0), file_attributes: U16::<LittleEndian>::new(self.attributes.bits()),
reserved1: U16::<LittleEndian>::new(0),
create_timestamp: U32::<LittleEndian>::new(create_ts),
last_modified_timestamp: U32::<LittleEndian>::new(modify_ts),
last_accessed_timestamp: U32::<LittleEndian>::new(access_ts),
create_10ms_increment: create_10ms,
last_modified_10ms_increment: modify_10ms,
create_utc_offset: create_utc,
last_modified_utc_offset: modify_utc,
last_accessed_utc_offset: access_utc,
reserved2: [0; 7],
}
}
fn build_stream_entry(&self, name_length: u8, name_hash: u16) -> RawStreamExtensionEntry {
let flags = 0x01 | if self.is_contiguous { 0x02 } else { 0x00 };
RawStreamExtensionEntry {
entry_type: entry_type::STREAM_EXTENSION,
general_secondary_flags: flags,
reserved1: 0,
name_length,
name_hash: U16::<LittleEndian>::new(name_hash),
reserved2: U16::<LittleEndian>::new(0),
valid_data_length: U64::<LittleEndian>::new(self.valid_data_length),
reserved3: U32::<LittleEndian>::new(0),
first_cluster: U32::<LittleEndian>::new(self.first_cluster),
data_length: U64::<LittleEndian>::new(self.data_length),
}
}
fn build_name_entry(&self, chars: &[u16], _is_first: bool) -> RawFileNameEntry {
let mut file_name = [0u8; 30];
for (i, &code_unit) in chars.iter().enumerate() {
if i >= CHARS_PER_NAME_ENTRY {
break;
}
let bytes = code_unit.to_le_bytes();
file_name[i * 2] = bytes[0];
file_name[i * 2 + 1] = bytes[1];
}
RawFileNameEntry {
entry_type: entry_type::FILE_NAME,
general_secondary_flags: 0, file_name,
}
}
}
fn is_invalid_char(c: char) -> bool {
matches!(
c,
'\0'..='\x1F' | '"' | '*' | '/' | ':' | '<' | '>' | '?' | '\\' | '|'
)
}
fn to_raw_entry<T: bytemuck::NoUninit>(entry: &T) -> RawDirectoryEntry {
let mut raw = RawDirectoryEntry { bytes: [0; 32] };
let bytes = bytemuck::bytes_of(entry);
unsafe {
raw.bytes[..bytes.len()].copy_from_slice(bytes);
}
raw
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_set_builder() {
let upcase = UpcaseTable::create_default();
let builder = EntrySetBuilder::file("test.txt").unwrap();
let entries = builder.build(&upcase);
assert_eq!(entries.len(), 3);
assert_eq!(unsafe { entries[0].entry_type }, entry_type::FILE_DIRECTORY);
assert_eq!(
unsafe { entries[1].entry_type },
entry_type::STREAM_EXTENSION
);
assert_eq!(unsafe { entries[2].entry_type }, entry_type::FILE_NAME);
}
#[test]
fn test_long_name_multiple_entries() {
let upcase = UpcaseTable::create_default();
let builder = EntrySetBuilder::file("this_is_a_very_long_filename.txt").unwrap();
let entries = builder.build(&upcase);
assert_eq!(entries.len(), 5);
}
#[test]
fn test_invalid_filename() {
assert!(EntrySetBuilder::file("test*.txt").is_err());
assert!(EntrySetBuilder::file("test:file").is_err());
assert!(EntrySetBuilder::file("").is_err());
}
}