Hi all,
After my Windows registry parsing crate, I'm currently writing an NTFS filesystem crate - read-only for now but open for write support later.
It centers around a FileSystem
struct that holds readable and seekable partition bytes like this:
pub struct FileSystem<T: io::Read + io::Seek> {
partition: T,
}
Among other things, this struct provides a method to iterate over all files of the root directory. The iterator is called Files
and each file is represented by an appropriate File
struct. File
shall be readable and seekable itself to make its content accessible.
All these properties together roughly lead to the following design:
pub struct Files<'a, T: io::Read + io::Seek> {
fs: &'a mut FileSystem<T>,
current_offset: u64,
last_offset: u64,
}
impl<'a, T> Iterator for Files<'a, T: io::Read + io::Seek> {
type Item = io::Result<File<'a, T>>;
fn next(&mut self) -> Option<Self::Item> {
...
}
}
pub struct File<'a, T: io::Read + io::Seek> {
fs: &'a mut FileSystem<T>,
name: [u8; 255],
}
impl<'a, T> io::Read for File<'a, T: io::Read + io::Seek> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
...
}
}
impl<'a, T> io::Seek for File<'a, T: io::Read + io::Seek> {
fn seek(&mut self, pos: SeekFrom) -> Result<u64, Self::Error> {
...
}
}
Now Rust's borrow checker and the design of std::io::Read
are giving me a hard time to actually implement my plan.
fn read
needs a &mut self
, so I need to carry mutable references to FileSystem
in Files
and File
just to be able to read.
However, when doing that, I cannot return a File
from Files::next
due to the lifetime limitations of Iterator
.
I have a few ideas around this, but none that really convinces me. It feels a lot like "pick your poison":
-
FileSystem::partition
becomes aRefCell<T>
(like the fatfs crate does)- Advantages: All references to
FileSystem
can be constant.Iterator
,io::Read
/io::Seek
, and evenio::Write
could be implemented without any hassles. - Disadvantages: Borrow-checking happens at runtime. I lose fundamental guarantees of the Rust compiler, and contributors need to be extra-cautious to not cause any panics.
- Advantages: All references to
-
Using the
positioned-io
crate (like the ext4 crate does)- Advantages: Reading is possible with a constant reference to
FileSystem
. - Disadvantages: This is no solution for later adding write support, and incompatible to
std::io
.
- Advantages: Reading is possible with a constant reference to
-
Not implementing
Iterator
, but justfn next
(like the streaming-iterator crate does)- Advantages:
io::Read
,io::Seek
, andio::Write
can be implemented. - Disadvantages: I lose all advantages of
Iterator
, like for loops or combining my iterator with others (map
,zip
, etc.).
By holding mutable references, I'm also subject to very rigid lifetime restrictions. Two simultaneousFile
instances aren't possible, and this is a likely case.
- Advantages:
-
Only store small frequently accessed data (like the name) inside
File
, pass a temporary mutable reference toFileSystem
every time we access the file content- Advantages: I can implement
Iterator
and have two simultaneousFile
instances. - Disadvantages: I cannot implement
io::Read
/io::Seek
/io::Write
onFile
, as their function signatures don't take a&mut FileSystem
. A caller may also pass me a different&mut FileSystem
than I expect.
- Advantages: I can implement
Am I missing anything? Do you have any other ideas?
I'd be grateful for any guidance out of this jungle.
Cheers,
Colin