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::partitionbecomes aRefCell<T>(like the fatfs crate does)- Advantages: All references to
FileSystemcan be constant.Iterator,io::Read/io::Seek, and evenio::Writecould 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-iocrate (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::Writecan 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 simultaneousFileinstances 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 toFileSystemevery time we access the file content- Advantages: I can implement
Iteratorand have two simultaneousFileinstances. - Disadvantages: I cannot implement
io::Read/io::Seek/io::WriteonFile, as their function signatures don't take a&mut FileSystem. A caller may also pass me a different&mut FileSystemthan 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