Files
CappuccinOS/src/drivers/fs/fat.rs

838 lines
27 KiB
Rust
Executable File

use alloc::{
boxed::Box,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use crate::drivers::storage::Partition;
use super::vfs::{FsOps, VNode, VNodeOperations};
// The first Cluster (perhaps 0xF0FFFF0F) is the FAT ID
// The second cluster stores the end-of-cluster-chain marker
// The third entry and further holds the directory table
//
// Fat Clusters are either one of these types:
//
// | fat12 | fat16 | fat32 | Description |
// |---------------|-----------------|-------------------------|-------------------------------|
// | 0xFF8-0xFFF | 0xFFF8-0xFFFF | 0x0FFFFFF8-0x0FFFFFFF | End Of cluster Chain |
// | 0xFF7 | 0xFFF7 | 0x0FFFFFF7 | Bad Cluster |
// | 0x002-0xFEF | 0x0002-0xFFEF | 0x00000002-0x0FFFFFEF | In use Cluster |
// | 0x000 | 0x0000 | 0x00000000 | Free Cluster |
// End Of Chain
const EOC_12: u32 = 0x0FF8;
const EOC_16: u32 = 0xFFF8;
const EOC_32: u32 = 0x0FFFFFF8;
#[derive(Clone, Copy, Debug)]
enum FatType {
Fat12(Fat16EBPB),
Fat16(Fat16EBPB),
Fat32(Fat32EBPB),
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct BIOSParameterBlock {
_jmp_instruction: [u8; 3], // EB 58 90
pub oem_identifier: [u8; 8], // MTOO4043 (hey, mtools)
pub bytes_per_sector: u16, // 00 02 (little endian so 512)
pub sectors_per_cluster: u8, // 01
pub reserved_sectors: u16, // 20 00 (32)
pub fat_count: u8, // 02
pub root_directory_count: u16, // 00 00 (what)
pub total_sectors: u16, // equal to zero when sector count is more than 65535
pub media_descriptor_type: u8, // F0
pub sectors_per_fat: u16, // Fat12/Fat16 only
pub sectors_per_track: u16, // 3F 00 (63)
pub head_count: u16, // 10 00 (16)
pub hidden_sectors: u32, // 00 00 00 00
pub large_sector_count: u32, // 00 F8 01 00 (129024)
pub ebpb_bytes: [u8; 54],
}
// Fat 12 and Fat 16 EBPB
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct Fat16EBPB {
pub drive_number: u8,
_reserved: u8,
pub signature: u8,
pub volume_id: u32,
pub volume_label: [u8; 11],
pub system_identifier_string: [u8; 8],
}
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct Fat32EBPB {
pub sectors_per_fat_ext: u32, // E1 03 00 00 (993, wtf)
pub flags: [u8; 2], // 00 00
pub fat_version: u16, // 00 00
pub root_dir_cluster: u32, // 2C 00 00 00 (2)
pub fsinfo_sector: u16, // 01 00 (1)
pub backup_bootsector: u16, // 06 00 (6)
_reserved: [u8; 12], // all zero
pub drive_number: u8, // 00
_reserved2: u8, // 00
pub signature: u8, // either 0x28 of 0x29: 29
pub volume_id: u32, // Varies
pub volume_label: [u8; 11], // "NO NAME "
pub system_identifier_string: [u8; 8], // Always "FAT32 " but never trust the contents of this string (for some reason)
}
#[repr(C, packed)]
#[derive(Debug)]
pub struct FSInfo {
pub lead_signature: u32,
pub mid_signature: u32,
pub last_known_free_cluster: u32,
pub look_for_free_clusters: u32,
_reserved2: [u8; 12],
pub trail_signature: u32,
}
impl FSInfo {
pub fn from_bytes(bytes: Arc<[u8]>) -> Self {
assert!(bytes.len() >= 512);
let lead_signature = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
let mid_signature = u32::from_le_bytes(bytes[484..488].try_into().unwrap());
let last_known_free_cluster = u32::from_le_bytes(bytes[488..492].try_into().unwrap());
let look_for_free_clusters = u32::from_le_bytes(bytes[492..496].try_into().unwrap());
let _reserved2 = bytes[496..508].try_into().unwrap();
let trail_signature = u32::from_le_bytes(bytes[508..].try_into().unwrap());
return Self {
lead_signature,
mid_signature,
last_known_free_cluster,
look_for_free_clusters,
_reserved2,
trail_signature,
};
}
}
#[repr(u8)]
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
enum FileEntryAttributes {
ReadOnly = 0x01,
Hidden = 0x02,
System = 0x04,
VolumeId = 0x08,
Directory = 0x10,
Archive = 0x20, // basically any file
LongFileName = 0x0F,
}
#[repr(C, packed)]
#[derive(Debug)]
struct LongFileName {
entry_order: u8,
first_characters: [u16; 5],
attribute: u8, // always 0x0F
long_entry_type: u8, // zero for name entries
checksum: u8,
second_characters: [u16; 6],
_always_zero: [u8; 2],
final_characters: [u16; 2],
}
#[repr(C, packed)]
#[derive(Debug)]
pub struct FileEntry {
file_name: [u8; 8],
extension: [u8; 3],
attributes: u8,
_reserved: u8,
creation_tenths: u8,
creation_time: u16,
creation_date: u16,
accessed_date: u16,
high_first_cluster_number: u16, // The high 16 bits of this entry's first cluster number. For FAT 12 and FAT 16 this is always zero.
modified_time: u16,
modified_date: u16,
low_first_cluster_number: u16,
file_size: u32,
}
impl FileEntry {
pub fn cluster(&self) -> u32 {
let mut cluster = self.low_first_cluster_number as u32;
cluster |= (self.high_first_cluster_number as u32) << 16;
return cluster;
}
}
pub struct FatFs {
partition: Partition,
// FAT info
#[allow(dead_code)]
fs_info: Option<FSInfo>,
fat: Option<Arc<[u32]>>,
bpb: BIOSParameterBlock,
fat_start: u64,
fat_type: FatType,
cluster_size: usize,
sectors_per_fat: usize,
}
impl FatFs {
pub fn new(partition: Partition) -> Result<Self, ()> {
let bpb_bytes = partition
.read(0, 1)
.expect("Failed to read FAT32 BIOS Parameter Block!");
let bpb = unsafe { *(bpb_bytes.as_ptr().cast::<BIOSParameterBlock>()) };
let (total_sectors, fat_size) = if bpb.total_sectors == 0 {
(bpb.large_sector_count, unsafe {
(*bpb.ebpb_bytes.as_ptr().cast::<Fat32EBPB>()).sectors_per_fat_ext
})
} else {
(bpb.total_sectors as u32, bpb.sectors_per_fat as u32)
};
let root_dir_sectors =
((bpb.root_directory_count * 32) + (bpb.bytes_per_sector - 1)) / bpb.bytes_per_sector;
let total_data_sectors = total_sectors
- (bpb.reserved_sectors as u32
+ (bpb.fat_count as u32 * fat_size)
+ root_dir_sectors as u32);
let total_clusters = total_data_sectors / bpb.sectors_per_cluster as u32;
let fat_type = if total_clusters < 4085 {
FatType::Fat12(unsafe { *bpb.ebpb_bytes.as_ptr().cast::<Fat16EBPB>() })
} else if total_clusters < 65525 {
FatType::Fat16(unsafe { *bpb.ebpb_bytes.as_ptr().cast::<Fat16EBPB>() })
} else {
FatType::Fat32(unsafe { *bpb.ebpb_bytes.as_ptr().cast::<Fat32EBPB>() })
};
let system_ident = match fat_type {
FatType::Fat12(ebpb) => ebpb.system_identifier_string,
FatType::Fat16(ebpb) => ebpb.system_identifier_string,
FatType::Fat32(ebpb) => ebpb.system_identifier_string,
};
let system_identifier = core::str::from_utf8(&system_ident);
if system_identifier.is_err() {
return Err(());
}
if let Ok(system_identifier_string) = system_identifier {
match fat_type {
FatType::Fat12(_) => {
if !system_identifier_string.contains("FAT12") {
return Err(());
}
}
FatType::Fat16(_) => {
if !system_identifier_string.contains("FAT16") {
return Err(());
}
}
FatType::Fat32(_) => {
if !system_identifier_string.contains("FAT32") {
return Err(());
}
}
}
}
let fs_info = match fat_type {
FatType::Fat32(ebpb) => {
let fsinfo_bytes = partition
.read(ebpb.fsinfo_sector as u64, 1)
.expect("Failed to read FSInfo sector!");
Some(FSInfo::from_bytes(fsinfo_bytes))
}
_ => None,
};
let fat_start = bpb.reserved_sectors as u64;
let sectors_per_fat = match fat_type {
FatType::Fat32(ebpb) => ebpb.sectors_per_fat_ext as usize,
_ => bpb.sectors_per_fat as usize,
};
let bytes_per_fat = 512 * sectors_per_fat;
let mut fat: Option<Arc<[u32]>> = None;
if crate::KERNEL_FEATURES.fat_in_mem {
let cluster_bytes = match fat_type {
FatType::Fat32(_) => 4,
_ => 2,
};
let mut fat_vec: Vec<u32> = Vec::with_capacity(bytes_per_fat / cluster_bytes);
for i in 0..sectors_per_fat {
let sector = partition
.read(fat_start + i as u64, 1)
.expect("Failed to read FAT");
for j in 0..(512 / cluster_bytes) {
match fat_type {
FatType::Fat32(_) => fat_vec.push(u32::from_le_bytes(
sector[j * cluster_bytes..(j * cluster_bytes + cluster_bytes)]
.try_into()
.unwrap(),
)),
_ => fat_vec.push(u16::from_le_bytes(
sector[j * cluster_bytes..(j * cluster_bytes + cluster_bytes)]
.try_into()
.unwrap(),
) as u32),
}
}
}
fat = Some(Arc::from(fat_vec));
} else {
crate::log_info!(
"\x1B[33mWARNING\x1B[0m: FAT is not being stored in memory, this feature is experimental and file reads are expected to be slower."
)
}
crate::println!("Found {fat_type:?} FS");
let cluster_size = bpb.sectors_per_cluster as usize * 512;
return Ok(Self {
partition,
fs_info,
fat,
bpb,
fat_start,
fat_type,
cluster_size,
sectors_per_fat,
});
}
fn find_entry_in_directory(&self, cluster: usize, name: &str) -> Result<FileEntry, ()> {
let mut i: usize = 0;
// Long file name is stored outsize because long filename and the real entry on separate entries
let mut long_filename: Vec<LongFileName> = Vec::new();
let mut long_filename_string: Option<String> = None;
let data_sector = self.read_cluster(cluster)?;
loop {
let bytes: [u8; core::mem::size_of::<FileEntry>()] =
data_sector[(i * 32)..((i + 1) * 32)].try_into().unwrap();
let first_byte = bytes[0];
let file_entry: FileEntry;
i += 1;
// Step 1
if first_byte == 0x00 {
break; // End of directory listing
}
// Step 2
if first_byte == 0xE5 {
continue; // Directory is unused, ignore it
} else if bytes[11] == FileEntryAttributes::LongFileName as u8 {
// Entry is LFN (step 3)
// read long filename (step 4)
let long_filename_part: LongFileName;
unsafe {
long_filename_part = core::mem::transmute(bytes);
}
long_filename.push(long_filename_part);
continue;
} else {
// step 5
unsafe {
file_entry = core::mem::transmute(bytes);
}
// step 6
if !long_filename.is_empty() {
// Make fileEntry with LFN (step 7)
let mut string: Vec<u16> = Vec::with_capacity(long_filename.len() * 13);
for i in 0..long_filename.len() {
let i = (long_filename.len() - 1) - i;
let long_filename = &long_filename[i];
let mut character_bytes = Vec::new();
let characters = long_filename.first_characters;
character_bytes.extend_from_slice(&characters);
let characters = long_filename.second_characters;
character_bytes.extend_from_slice(&characters);
let characters = long_filename.final_characters;
character_bytes.extend_from_slice(&characters);
// remove 0x0000 characters and 0xFFFF characters
character_bytes.retain(|&x| x != 0xFFFF && x != 0x0000);
for &le_character in character_bytes.iter() {
// Convert little-endian u16 to native-endian u16
let native_endian_value = u16::from_le(le_character);
string.push(native_endian_value);
}
}
long_filename_string = Some(String::from_utf16(&string).unwrap());
long_filename.clear();
}
}
if name.replacen('.', "", 1).len() <= 11 {
let search_parts: Vec<&str> = name.split('.').collect();
let filename = core::str::from_utf8(&file_entry.file_name).unwrap();
let extension = core::str::from_utf8(&file_entry.extension).unwrap();
if (search_parts.len() == 1
&& !filename.contains(&search_parts[0].to_ascii_uppercase()))
|| (search_parts.len() > 1
&& (!filename.contains(&search_parts[0].to_ascii_uppercase())
|| !extension.contains(&search_parts[1].to_ascii_uppercase())))
{
continue;
}
return Ok(file_entry);
} else {
// Long file name
if long_filename_string != Some(name.to_string()) {
continue;
}
return Ok(file_entry);
}
}
return Err(());
}
pub fn read_cluster(&self, cluster: usize) -> Result<Arc<[u8]>, ()> {
return self.partition.read(
self.cluster_to_sector(cluster) as u64,
self.bpb.sectors_per_cluster as usize,
);
}
fn cluster_to_sector(&self, cluster: usize) -> usize {
crate::println!("bytes per sector: {}", unsafe {
core::ptr::read_unaligned(core::ptr::addr_of!(self.bpb.bytes_per_sector))
});
let fat_size = self.sectors_per_fat;
let root_dir_sectors = ((self.bpb.root_directory_count * 32)
+ (self.bpb.bytes_per_sector - 1))
/ self.bpb.bytes_per_sector;
let first_data_sector = self.bpb.reserved_sectors as usize
+ (self.bpb.fat_count as usize * fat_size)
+ root_dir_sectors as usize;
return ((((cluster.wrapping_sub(2)) as isize)
.wrapping_mul(self.bpb.sectors_per_cluster as isize)) as usize)
.wrapping_add(first_data_sector);
}
fn sector_to_cluster(&self, sector: usize) -> usize {
let fat_size = self.sectors_per_fat;
let root_dir_sectors = ((self.bpb.root_directory_count * 32)
+ (self.bpb.bytes_per_sector - 1))
/ self.bpb.bytes_per_sector;
let first_data_sector = self.bpb.reserved_sectors as usize
+ (self.bpb.fat_count as usize * fat_size)
+ root_dir_sectors as usize;
return (((sector).wrapping_sub(first_data_sector))
.wrapping_div(self.bpb.sectors_per_cluster as usize))
.wrapping_add(2);
}
fn get_next_cluster(&self, cluster: usize) -> u32 {
if crate::KERNEL_FEATURES.fat_in_mem {
return match self.fat_type {
FatType::Fat12(_) => self.fat.as_ref().unwrap()[cluster] & 0x0FFF,
FatType::Fat16(_) => self.fat.as_ref().unwrap()[cluster] & 0xFFFF,
FatType::Fat32(_) => self.fat.as_ref().unwrap()[cluster] & 0x0FFFFFFF,
};
} else {
let fat_entry_size = match self.fat_type {
FatType::Fat12(_) => 2, // 12 bits per entry
FatType::Fat16(_) => 2, // 16 bits per entry
FatType::Fat32(_) => 4, // 28 bits per entry
};
let entry_offset = cluster * fat_entry_size;
let entry_offset_in_sector = entry_offset % 512;
// needs two incase we "straddle a sector"
let sector_data = self
.partition
.read(self.fat_start + entry_offset as u64 / 512, 2)
.expect("Failed to read from FAT!");
match self.fat_type {
FatType::Fat12(_) => {
let cluster_entry_bytes: [u8; 2] = sector_data
[entry_offset_in_sector..entry_offset_in_sector + 2]
.try_into()
.unwrap();
return (u16::from_le_bytes(cluster_entry_bytes) & 0x0FFF) as u32;
}
FatType::Fat16(_) => {
let cluster_entry_bytes: [u8; 2] = sector_data
[entry_offset_in_sector..entry_offset_in_sector + 2]
.try_into()
.unwrap();
return (u16::from_le_bytes(cluster_entry_bytes)) as u32;
}
FatType::Fat32(_) => {
let cluster_entry_bytes: [u8; 4] = sector_data
[entry_offset_in_sector..entry_offset_in_sector + 4]
.try_into()
.unwrap();
return u32::from_le_bytes(cluster_entry_bytes) & 0x0FFFFFFF;
}
}
}
}
}
impl FsOps for FatFs {
fn mount(&mut self, _path: &str, data: &mut *mut u8, _vfsp: *const super::vfs::Vfs) {
// TODO: load the FAT into memory here
*data = core::ptr::addr_of!(*self) as *mut u8;
}
fn unmount(&mut self, _vfsp: *const super::vfs::Vfs) {
// TODO: unload the FAT form memory
}
fn root(&mut self, vfsp: *const super::vfs::Vfs) -> super::vfs::VNode {
let root_cluster = match self.fat_type {
FatType::Fat32(ebpb) => ebpb.root_dir_cluster as usize,
_ => self.sector_to_cluster(
self.bpb.reserved_sectors as usize
+ (self.bpb.fat_count as usize * self.sectors_per_fat),
),
};
let file = File::Dir(FatDirectory {
directory_cluster: root_cluster,
});
return super::vfs::VNode {
flags: 0,
ref_count: 0,
shared_lock_count: 0,
exclusive_lock_count: 0,
ops: Box::new(file),
node_data: None,
parent: vfsp,
typ: super::vfs::VNodeType::Directory,
data: core::ptr::null_mut(),
};
}
fn fid(&mut self, _path: &str, _vfsp: *const super::vfs::Vfs) -> Option<super::vfs::FileId> {
todo!("FAT FID");
}
fn statfs(&mut self, _vfsp: *const super::vfs::Vfs) -> super::vfs::StatFs {
todo!("FAT STATFS");
}
fn sync(&mut self, _vfsp: *const super::vfs::Vfs) {
todo!("FAT SYNC");
}
fn vget(
&mut self,
_fid: super::vfs::FileId,
_vfsp: *const super::vfs::Vfs,
) -> super::vfs::VNode {
todo!("FAT VGET");
}
}
enum File {
Archive(FatFile),
Dir(FatDirectory),
}
impl<'a> VNodeOperations for File {
fn access(&mut self, _m: u32, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn bmap(&mut self, _block_number: u32, _bnp: (), _vp: *const VNode) -> super::vfs::VNode {
todo!("VNODE OPERATIONS");
}
fn bread(&mut self, _block_number: u32, _vp: *const VNode) -> Arc<[u8]> {
todo!("VNODE OPERATIONS");
}
fn close(&mut self, _f: u32, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn create(
&mut self,
_nm: &str,
_va: super::vfs::VAttr,
_e: u32,
_m: u32,
_c: super::vfs::UserCred,
_vp: *const VNode,
) -> Result<super::vfs::VNode, ()> {
todo!("VNODE OPERATIONS");
}
fn fsync(&mut self, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn getattr(&mut self, _c: super::vfs::UserCred, _vp: *const VNode) -> super::vfs::VAttr {
todo!("VNODE OPERATIONS");
}
fn inactive(&mut self, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn ioctl(
&mut self,
_com: u32,
_d: *mut u8,
_f: u32,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn link(
&mut self,
_target_dir: *mut super::vfs::VNode,
_target_name: &str,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn lookup(
&mut self,
nm: &str,
_c: super::vfs::UserCred,
vp: *const VNode,
) -> Result<super::vfs::VNode, ()> {
let fat_fs = unsafe { (*(*vp).parent).data.cast::<FatFs>() };
match self {
File::Dir(directory) => unsafe {
let file_entry =
(*fat_fs).find_entry_in_directory(directory.directory_cluster, nm)?;
let file_typ = if file_entry.attributes == FileEntryAttributes::Directory as u8 {
crate::drivers::fs::vfs::VNodeType::Directory
} else {
crate::drivers::fs::vfs::VNodeType::Regular
};
let file = if file_entry.attributes == FileEntryAttributes::Directory as u8 {
File::Dir(FatDirectory {
directory_cluster: file_entry.cluster() as usize,
})
} else {
File::Archive(FatFile { file_entry })
};
let vnode = VNode {
flags: 0,
ref_count: 0,
shared_lock_count: 0,
exclusive_lock_count: 0,
ops: Box::new(file),
node_data: None,
parent: (*vp).parent,
typ: file_typ,
data: core::ptr::null_mut(),
};
Ok(vnode)
},
_ => panic!("tried to lookup on a file"),
}
}
fn mkdir(
&mut self,
_nm: &str,
_va: super::vfs::VAttr,
_c: super::vfs::UserCred,
_vp: *const VNode,
) -> Result<super::vfs::VNode, ()> {
todo!("VNODE OPERATIONS");
}
fn open(
&mut self,
_f: u32,
_c: super::vfs::UserCred,
vp: *const VNode,
) -> Result<Arc<[u8]>, ()> {
match self {
File::Archive(archive) => {
let fat_fs = unsafe { (*(*vp).parent).data.cast::<FatFs>() };
let mut file: Vec<u8> = Vec::with_capacity(archive.file_entry.file_size as usize);
let mut file_ptr_index = 0;
let mut cluster = ((archive.file_entry.high_first_cluster_number as u32) << 16)
| archive.file_entry.low_first_cluster_number as u32;
let cluster_size = unsafe { (*fat_fs).cluster_size };
let mut copied_bytes = 0;
loop {
let cluster_data = unsafe { (*fat_fs).read_cluster(cluster as usize)? };
let remaining = archive.file_entry.file_size as usize - copied_bytes;
let to_copy = if remaining > cluster_size {
cluster_size
} else {
remaining
};
unsafe {
core::ptr::copy_nonoverlapping(
cluster_data.as_ptr(),
file.as_mut_ptr().add(file_ptr_index),
to_copy,
);
file.set_len(file.len() + to_copy);
}
file_ptr_index += cluster_size;
copied_bytes += to_copy;
cluster = unsafe { (*fat_fs).get_next_cluster(cluster as usize) };
match unsafe { (*fat_fs).fat_type } {
FatType::Fat12(_) => {
if cluster >= EOC_12 {
break;
}
}
FatType::Fat16(_) => {
if cluster >= EOC_16 {
break;
}
}
FatType::Fat32(_) => {
if cluster >= EOC_32 {
break;
}
}
}
}
return Ok(Arc::from(file));
}
_ => panic!("Cannot open non archives"),
}
}
fn rdwr(
&mut self,
_uiop: *const super::vfs::UIO,
_direction: super::vfs::IODirection,
_f: u32,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn readdir(
&mut self,
_uiop: *const super::vfs::UIO,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn readlink(
&mut self,
_uiop: *const super::vfs::UIO,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn rename(
&mut self,
_nm: &str,
_target_dir: *mut super::vfs::VNode,
_target_name: &str,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
fn select(&mut self, _w: super::vfs::IODirection, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn setattr(&mut self, _va: super::vfs::VAttr, _c: super::vfs::UserCred, _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn strategy(&mut self, _bp: (), _vp: *const VNode) {
todo!("VNODE OPERATIONS");
}
fn symlink(
&mut self,
_link_name: &str,
_va: super::vfs::VAttr,
_target_name: &str,
_c: super::vfs::UserCred,
_vp: *const VNode,
) {
todo!("VNODE OPERATIONS");
}
}
struct FatFile {
file_entry: FileEntry,
}
struct FatDirectory {
directory_cluster: usize,
}