Skip to content

Commit 3050d50

Browse files
committed
high-level fs abstraction
1 parent f93f089 commit 3050d50

File tree

7 files changed

+678
-0
lines changed

7 files changed

+678
-0
lines changed

src/fs/dir_entry_iter.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crate::fs::file_info::{BoxedUefiFileInfo, UEFI_FILE_INFO_MAX_SIZE};
2+
use super::*;
3+
use alloc_api::vec::Vec;
4+
5+
/// Iterator over the low-level entries of a UEFI directory handle.
6+
/// Returns owning [BoxedUefiFileInfo].
7+
pub struct UefiDirectoryEntryIterator {
8+
handle: UefiDirectoryHandle,
9+
buffer: Vec<u8>,
10+
}
11+
12+
impl UefiDirectoryEntryIterator {
13+
pub fn new(handle: UefiDirectoryHandle) -> Self {
14+
// May contain 256 characters with an fixed size UTF-16 format. Null-byte included.
15+
let mut buffer = Vec::with_capacity(UEFI_FILE_INFO_MAX_SIZE);
16+
(0..buffer.capacity()).for_each(|_| buffer.push(0));
17+
Self { handle, buffer }
18+
}
19+
}
20+
21+
impl Iterator for UefiDirectoryEntryIterator {
22+
type Item = BoxedUefiFileInfo;
23+
24+
fn next(&mut self) -> Option<Self::Item> {
25+
// reset as the buffer gets used multiple times
26+
self.buffer.fill(0);
27+
28+
self.handle
29+
.read_entry(&mut self.buffer)
30+
// it is extremly ugly to marry the results from uefi with the option from the iterator..
31+
// thus, just panic =(
32+
.expect("Uefi Error")
33+
.map(|fileinfo| BoxedUefiFileInfo::new(fileinfo))
34+
}
35+
}

src/fs/file_info.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use crate::fs::path::MAX_COMPONENT_LENGTH;
2+
use alloc_api::vec::Vec;
3+
use core::ops::Deref;
4+
use uefi::proto::media::file::FileInfo as UefiFileInfo;
5+
6+
// Ugly but we can't use size_of() as the type isn't sized.
7+
// This is constant, as it is specified.
8+
/// Size of a [UefiFileInfo] without the filename and without a NULL byte.
9+
pub const UEFI_FILE_INFO_MAX_SIZE: usize = 80 + 2 * (1 + MAX_COMPONENT_LENGTH);
10+
11+
/// A UEFI file info that lives on the heap. Owns the data.
12+
#[derive(Debug)]
13+
pub struct BoxedUefiFileInfo {
14+
// owning buffer
15+
buffer: Vec<u8>,
16+
}
17+
18+
impl BoxedUefiFileInfo {
19+
pub fn new(info: &UefiFileInfo) -> Self {
20+
let mut buffer = Vec::<u8>::with_capacity(UEFI_FILE_INFO_MAX_SIZE);
21+
(0..buffer.capacity()).for_each(|_| buffer.push(0));
22+
23+
unsafe {
24+
let src_ptr = core::ptr::addr_of!(*info).cast::<u8>();
25+
let dest_ptr = buffer.as_mut_ptr().cast::<u8>();
26+
core::ptr::copy_nonoverlapping(src_ptr, dest_ptr, UEFI_FILE_INFO_MAX_SIZE);
27+
}
28+
Self { buffer }
29+
}
30+
}
31+
32+
impl Deref for BoxedUefiFileInfo {
33+
type Target = UefiFileInfo;
34+
fn deref(&self) -> &Self::Target {
35+
unsafe {
36+
let addr = self.buffer.as_ptr().cast::<()>();
37+
let ptr: *const UefiFileInfo =
38+
// todo metadata is here probably unused?!
39+
core::ptr::from_raw_parts(addr, self.buffer.capacity());
40+
ptr.as_ref().unwrap()
41+
}
42+
}
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
use uefi::proto::media::file::FromUefi;
49+
50+
#[test]
51+
fn test_put_into_box() {
52+
let mut buffer = [0_u8; UEFI_FILE_INFO_MAX_SIZE];
53+
54+
let boxed_fileinfo = unsafe {
55+
let ptr = buffer.as_mut_ptr();
56+
57+
// set some values to the "file_size" property
58+
*ptr.cast::<u64>().add(1) = 0x7733_0909_2345_1337;
59+
60+
let uefi_fileinfo = UefiFileInfo::from_uefi(ptr.cast());
61+
assert_eq!(uefi_fileinfo.file_size(), 0x7733_0909_2345_1337);
62+
BoxedUefiFileInfo::new(uefi_fileinfo)
63+
};
64+
65+
assert_eq!(boxed_fileinfo.file_size(), 0x7733_0909_2345_1337);
66+
}
67+
}

src/fs/file_system.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
//! Module for [FileSystem].
2+
3+
use super::*;
4+
use crate::fs::dir_entry_iter::UefiDirectoryEntryIterator;
5+
use crate::fs::file_info::BoxedUefiFileInfo;
6+
use crate::fs::path::{Path, PathError};
7+
use alloc_api::string::{FromUtf8Error, String, ToString};
8+
use alloc_api::vec::Vec;
9+
use core::fmt;
10+
use core::fmt::{Debug, Formatter};
11+
use uefi::proto::media::file::FileType;
12+
use uefi::table::boot::ScopedProtocol;
13+
use uefi::{CString16};
14+
15+
// #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
16+
#[derive(Debug, Clone)]
17+
pub enum FileSystemError {
18+
/// Can't open the root directory of the underlying volume.
19+
CantOpenVolume,
20+
/// The path is invalid because of the underlying [PathError].
21+
IllegalPath(PathError),
22+
/// The file or directory was not found in the underlying volume.
23+
FileNotFound(String),
24+
/// The path is existend but does not correspond to a directory when a directory was expected.
25+
NotADirectory(String),
26+
/// The path is existend but does not correspond to a file when a file was expected.
27+
NotAFile(String),
28+
/// Can't delete the file.
29+
CantDeleteFile(String),
30+
/// Error writing bytes.
31+
WriteFailure,
32+
/// Error flushing file.
33+
FlushFailure,
34+
/// Error reading file.
35+
ReadFailure,
36+
/// Can't parse file content as UTF-8.
37+
Utf8Error(FromUtf8Error),
38+
/// Could not open the given path.
39+
OpenError(String),
40+
}
41+
42+
impl From<PathError> for FileSystemError {
43+
fn from(err: PathError) -> Self {
44+
Self::IllegalPath(err)
45+
}
46+
}
47+
48+
/// Return type for public [FileSystem] operations.
49+
pub type FileSystemResult<T> = Result<T, FileSystemError>;
50+
51+
/// Entry point into the file system abstraction described by the module description. Accessor to
52+
/// an UEFI volume. Available methods try to be close to the `fs` module of `libstd`.
53+
///
54+
/// # Technical Background
55+
/// Some random interesting information how this abstraction helps.
56+
/// - To get metadata about UEFI files, we need to iterate the parent directory. This abstraction
57+
/// hides this complexity.
58+
pub struct FileSystem<'name, 'boot_services> {
59+
/// Name to identify this file system. For example "root" or "boot". Only a help for users.
60+
name: &'name str,
61+
/// Underlying UEFI
62+
proto: ScopedProtocol<'boot_services, SimpleFileSystemProtocol>,
63+
}
64+
65+
impl<'name, 'boot_services> FileSystem<'name, 'boot_services> {
66+
/// Constructor. The name is only used to help you as a user to identify this file system.
67+
/// This may be "root", "main", or "boot".
68+
pub fn new(
69+
name: &'name str,
70+
proto: ScopedProtocol<'boot_services, SimpleFileSystemProtocol>,
71+
) -> Self {
72+
Self { name, proto }
73+
}
74+
75+
/// Tests if the underlying file exists. If this returns `Ok`, the file exists.
76+
pub fn try_exists(&mut self, path: &str) -> FileSystemResult<()> {
77+
self.metadata(&path).map(|_| ())
78+
}
79+
80+
/// Copies the contents of one file to another. Creates the destination file if it doesn't
81+
/// exist and overwrites any content, if it exists.
82+
pub fn copy(&mut self, src_path: &str, dest_path: &str) -> FileSystemResult<()> {
83+
let read = self.read(src_path)?;
84+
self.write(dest_path, read)
85+
}
86+
87+
/// Creates a new, empty directory at the provided path
88+
pub fn create_dir(&mut self, path: &str) -> FileSystemResult<()> {
89+
let _path = Path::new(path)?;
90+
todo!()
91+
}
92+
93+
/// Recursively create a directory and all of its parent components if they are missing.
94+
pub fn create_dir_all(&mut self, path: &str) -> FileSystemResult<()> {
95+
let _path = Path::new(path)?;
96+
todo!()
97+
}
98+
99+
/// Given a path, query the file system to get information about a file, directory, etc.
100+
pub fn metadata(&mut self, path: &str) -> FileSystemResult<BoxedUefiFileInfo> {
101+
let path = Path::new(path)?;
102+
self.resolve_directory_entry_fileinfo(&path)
103+
}
104+
105+
/// Read the entire contents of a file into a bytes vector.
106+
pub fn read(&mut self, path: &str) -> FileSystemResult<Vec<u8>> {
107+
let path = Path::new(path)?;
108+
109+
let mut file = self
110+
.open_in_root(&path, UefiFileMode::Read)?
111+
.into_regular_file()
112+
.ok_or(FileSystemError::NotAFile(path.as_str().to_string()))?;
113+
let info = self.resolve_directory_entry_fileinfo(&path).unwrap();
114+
115+
let mut vec = Vec::with_capacity(info.file_size() as usize);
116+
vec.resize(vec.capacity(), 0);
117+
let read_bytes = file.read(vec.as_mut_slice()).map_err(|e| {
118+
log::error!("reading failed: {e:?}");
119+
FileSystemError::ReadFailure
120+
})?;
121+
122+
// we read the whole file at once!
123+
if read_bytes != info.file_size() as usize {
124+
log::error!("Did only read {}/{} bytes", info.file_size(), read_bytes);
125+
}
126+
127+
Ok(vec)
128+
}
129+
130+
/// Returns an iterator over the entries within a directory.
131+
pub fn read_dir(&mut self, path: &str) -> FileSystemResult<UefiDirectoryEntryIterator> {
132+
let path = Path::new(path)?;
133+
let parent_dir = self.open_parent_dir_handle_of_path(&path, UefiFileMode::Read)?;
134+
Ok(UefiDirectoryEntryIterator::new(parent_dir))
135+
}
136+
137+
/// Read the entire contents of a file into a string.
138+
pub fn read_to_string(&mut self, path: &str) -> FileSystemResult<String> {
139+
String::from_utf8(self.read(path)?).map_err(|x| FileSystemError::Utf8Error(x))
140+
}
141+
142+
/// Removes an empty directory.
143+
pub fn remove_dir(&mut self, path: &str) -> FileSystemResult<()> {
144+
let _path = Path::new(path)?;
145+
todo!()
146+
}
147+
148+
/// Removes a directory at this path, after removing all its contents. Use carefully!
149+
pub fn remove_dir_all(&mut self, path: &str) -> FileSystemResult<()> {
150+
let _path = Path::new(path)?;
151+
todo!()
152+
}
153+
154+
// Removes a file from the filesystem.
155+
pub fn remove_file(&mut self, path: &str) -> FileSystemResult<()> {
156+
let path = Path::new(path)?;
157+
158+
let file = self
159+
.open_in_root(&path, UefiFileMode::ReadWrite)?
160+
.into_type()
161+
.unwrap();
162+
163+
match file {
164+
FileType::Regular(file) => file.delete().map_err(|e| {
165+
log::error!("error removing file: {e:?}");
166+
FileSystemError::CantDeleteFile(path.as_str().to_string())
167+
}),
168+
FileType::Dir(_) => Err(FileSystemError::NotAFile(path.as_str().to_string())),
169+
}
170+
}
171+
172+
// Rename a file or directory to a new name, replacing the original file if it already exists.
173+
pub fn rename(&mut self, src_path: &str, dest_path: &str) -> FileSystemResult<()> {
174+
self.copy(src_path, dest_path)?;
175+
self.remove_file(src_path)
176+
}
177+
178+
/// Write a slice as the entire contents of a file. This function will create a file if it does
179+
/// not exist, and will entirely replace its contents if it does.
180+
pub fn write<C: AsRef<[u8]>>(&mut self, path: &str, content: C) -> FileSystemResult<()> {
181+
let path = Path::new(path)?;
182+
183+
// since there is no .truncate() in UEFI, we delete the file first it it exists.
184+
if self.try_exists(path.as_str()).is_ok() {
185+
self.remove_file(path.as_str())?;
186+
}
187+
188+
let mut handle = self
189+
.open_in_root(&path, UefiFileMode::CreateReadWrite)?
190+
.into_regular_file()
191+
.unwrap();
192+
193+
handle.write(content.as_ref()).map_err(|e| {
194+
log::error!("only wrote {e:?} bytes");
195+
FileSystemError::WriteFailure
196+
})?;
197+
handle.flush().map_err(|e| {
198+
log::error!("flush failure: {e:?}");
199+
FileSystemError::FlushFailure
200+
})?;
201+
Ok(())
202+
}
203+
204+
/// Resolves the [BoxedUefiFileInfo] of the final [Component].
205+
/// For example, looks up fileinfo of "bar" in "/foo/bar". The returned fileinfo belongs to a
206+
/// directory or a file.
207+
///
208+
/// # Parameters
209+
/// - `path`: whole user input for the path. Only required for error/debug information.
210+
fn resolve_directory_entry_fileinfo(
211+
&mut self,
212+
path: &Path,
213+
) -> FileSystemResult<BoxedUefiFileInfo> {
214+
let ucs2_filename = CString16::try_from(path.final_component().as_str()).unwrap();
215+
216+
let parent_dir = self.open_parent_dir_handle_of_path(path, UefiFileMode::Read)?;
217+
218+
// from my current knowledge, file info can only by obtained by iterating the parent dir
219+
UefiDirectoryEntryIterator::new(parent_dir)
220+
.find(|entry| entry.file_name() == ucs2_filename)
221+
.ok_or(FileSystemError::FileNotFound(path.as_str().to_string()))
222+
}
223+
224+
/// Opens a fresh handle to the root directory of the volume.
225+
fn open_root(&mut self) -> FileSystemResult<UefiDirectoryHandle> {
226+
let proto = unsafe { self.proto.interface.get().as_mut() }.unwrap();
227+
proto.open_volume().map_err(|e| {
228+
log::error!("Can't open root volume: {e:?}");
229+
FileSystemError::CantOpenVolume
230+
})
231+
}
232+
233+
/// Wrapper around [Self::open_root] that opens the specified path. May create a file if
234+
/// a corresonding [UefiFileMode] is set.
235+
fn open_in_root(
236+
&mut self,
237+
path: &Path,
238+
mode: UefiFileMode,
239+
) -> FileSystemResult<UefiFileHandle> {
240+
let ucs2_path = CString16::try_from(path.as_str_without_root()).unwrap();
241+
242+
self.open_root()?
243+
.open(&ucs2_path, mode, UefiFileAttribute::empty())
244+
.map_err(|x| {
245+
log::trace!("Can't open file {path}: {x:?}");
246+
FileSystemError::OpenError(path.as_str().to_string())
247+
})
248+
}
249+
250+
/// Opens a handle to the parent directory of the final component provided by `path`.
251+
fn open_parent_dir_handle_of_path(
252+
&mut self,
253+
path: &Path,
254+
mode: UefiFileMode,
255+
) -> FileSystemResult<UefiDirectoryHandle> {
256+
let mut parent_dir = self.open_root()?;
257+
if let Some(path_to_parent_dir) = path.to_path_of_parent() {
258+
// panic okay because the file must exist at that point (and thus, the parent dir)
259+
260+
let ucs2_path = CString16::try_from(path_to_parent_dir.as_str_without_root()).unwrap();
261+
let filetype = parent_dir
262+
.open(&ucs2_path, mode, UefiFileAttribute::empty())
263+
.map_err(|e| {
264+
log::error!("Can't open directory: {e:?}");
265+
FileSystemError::OpenError(path.as_str().to_string())
266+
})?
267+
.into_type()
268+
.expect("low level uefi error");
269+
assert!(matches!(filetype, UefiFileType::Dir(_)));
270+
}
271+
Ok(parent_dir)
272+
}
273+
}
274+
275+
impl<'name, 'boot_services> Debug for FileSystem<'name, 'boot_services> {
276+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
277+
f.debug_struct("FileSystem")
278+
.field("name", &self.name)
279+
.field("proto_ptr", &self.proto.interface.get())
280+
.finish()
281+
}
282+
}
283+
284+
/* nothing to clean up manually
285+
impl<'name, 'boot_services> Drop for FileSystem<'name, 'boot_services> {
286+
fn drop(&mut self) {
287+
//
288+
}
289+
}*/

0 commit comments

Comments
 (0)