Expand description
§Extended fs
An OS and architecture independent implementation of some Unix filesystems in Rust.
/!\ Warning /!\ : this crate is not at all sure enough to be used in a real context. Do NOT manage any important data with this library, and make backups before using it!
The purpose of this library is not to be production-ready, but to help people who make toy OS (with Rust OSDev for example).
§Details
This crate provides a general interface to deal with some UNIX filesytems, and adds supports for some of them.
Currently, only the Second Extended Filesystem (ext2) is supported, but one can implements its own filesystem with this interface.
This crate does NOT provide a virtual filesystem: you can either make one or use another crate on top on this one.
Every structure, trait and function in this crate is documented and contains source if needed. If you find something unclear, do not hesitate to create an issue at https://codeberg.org/RatCornu/efs/issues.
§File interfaces
-
As defined in POSIX, a file can either be a
Regular, aDirectory, aSymbolicLink, aFifo, aCharacterDevice, aBlockDeviceor aSocket. Traits are available for each one of them, with basic read and write operations. Moreover, a read-only version of each trait is available. -
Fileis the base trait of all other file traits. It provides an interface to retrieve and modify general attributes of a POSIX file (basically everything returned by thestatcommand on a UNIX OS). -
A
Regular(file) is a basic file containing a sequence of bytes, which can be read into a string (or not, depending on its content). As this file isno_stdoriented, the use of [std::io] is not possible, this is why has to be manipulated throughefs::io(you can findRead,WriteandSeekas in [std::io]). -
A
Directoryis a node in the tree-like hierarchy of a filesystem. You can retrieve, add and remove entries (which are other files). -
A
SymbolicLinkis a file pointing an other file. It can be interpreted as the symbolic link or the pointed file in theFileSystemtrait. -
Other file types are defined but cannot be much manipulated as their implementation depends on the virtual file system and on the OS.
§Filesystem interface
All the manipulations needed in a filesystem can be made through the file traits. The FileSystem is
here to provide two things : an entry point to the filesystem with the root method, and
high-level functions to make the file manipulations easier.
§Paths
As the Rust’s native Path implementation is in [std::path], this crates provides an other
Path interface. It is based on UnixStr, which are the equivalent of
OsStr with a guarantee that: it is never empty nor contains the <NUL> character (‘\0’).
§Devices
In this crate, a Device is a sized structure that can be read, written directly at any point.
To ensure that a read-only filesystem is never actually written, the Device trait has a stronger
constraint that being Read + Write + Seek: it should be readable with a read-only borrow of the device (which is not the case
for [std::io::Read]).
§Usage
§High-level usage
You always need to provide two things to use this crate: a filesystem and a device.
For the filesystem, you can use the filesystems provided by this crate or make one by yourself (see the how to implement a
filesystem section). The usage of a filesystem does not depend on whether you are in a no_std
environment or not.
For the devices :
-
if you are in a
no_stdenvironment: you can make test withVec<u8>,&[u8], … if needed, but you will probably have to provide your own device implementation. See the part on how to implement a device if needed. -
if you are in a
stdenvironment: you can use every structure that implements [std::io::Read], [std::io::Write] and [std::io::Seek] through the use ofStdIOWrapper. Moreover, you can directly use std’sFilelike this:use core::cell::RefCell; use std::fs::File; use efs::dev::Device; let file = RefCell::new( File::options() .read(true) .write(true) .open("./tests/fs/ext2/io_operations.ext2") .unwrap(), ); // `file` is a `Device`
§Example
Here is a complete example of what can be do with the interfaces provided.
You can find this test file on efs’s codeberg repo.
use core::cell::RefCell;
use core::str::FromStr;
use efs::dev::celled::Celled;
use efs::file::{Directory, SymbolicLink, Type, TypeWithFile};
use efs::fs::ext2::Ext2;
use efs::fs::FileSystem;
use efs::io::{Read, Write};
use efs::path::{Path, UnixStr};
use efs::permissions::Permissions;
use efs::types::{Gid, Uid};
// `device` now contains a `Device`
let device = RefCell::new(
std::fs::File::options()
.read(true)
.write(true)
.open("./tests/fs/ext2/io_operations_copy_lib_example.ext2")
.unwrap(),
);
let ext2 = Ext2::new(device, 0).unwrap();
let fs = Celled::new(ext2);
// `fs` now contains a `FileSystem` with the following structure:
// /
// ├── bar.txt -> foo.txt
// ├── baz.txt
// ├── folder
// │ ├── ex1.txt
// │ └── ex2.txt -> ../foo.txt
// ├── foo.txt
// └── lost+found
/// The root of the filesystem
let root = fs.root().unwrap();
// We retrieve here `foo.txt` which is a regular file
let Some(TypeWithFile::Regular(mut foo_txt)) =
root.entry(UnixStr::new("foo.txt").unwrap()).unwrap()
else {
panic!("foo.txt is a regular file in the root folder");
};
// We read the content of `foo.txt`.
assert_eq!(foo_txt.read_all().unwrap(), b"Hello world!\n");
// We retrieve here `folder` which is a directory
let Some(TypeWithFile::Directory(mut folder)) =
root.entry(UnixStr::new("folder").unwrap()).unwrap()
else {
panic!("folder is a directory in the root folder");
};
// In `folder`, we retrieve `ex1.txt` as `/folder/ex1` points to the same
// file as `../folder/ex1.txt` when `/folder` is the current directory.
//
// Here, it is done by the complete path using the `FileSystem` trait.
let Ok(TypeWithFile::Regular(mut ex1_txt)) =
fs.get_file(&Path::from_str("../folder/ex1.txt").unwrap(), folder.clone(), false)
else {
panic!("ex1.txt is a regular file at /folder/ex1.txt");
};
// We read the content of `foo.txt`.
ex1_txt.write_all(b"Hello earth!\n").unwrap();
// We can also retrieve/create/delete a subentry with the `Directory`
// trait.
let TypeWithFile::SymbolicLink(mut boo) = folder
.add_entry(
UnixStr::new("boo.txt").unwrap(),
Type::SymbolicLink,
Permissions::from_bits_retain(0o777),
Uid(0),
Gid(0),
)
.unwrap()
else {
panic!("Could not create a symbolic link");
};
// We set the pointed file of the newly created `/folder/boo.txt` to
// `../baz.txt`.
boo.set_pointed_file("../baz.txt").unwrap();
// We ensure now that if we read `/folder/boo.txt` while following the
// symbolic links we get the content of `/baz.txt`.
let TypeWithFile::Regular(mut baz_txt) =
fs.get_file(&Path::from_str("/folder/boo.txt").unwrap(), root, true).unwrap()
else {
panic!("Could not retrieve baz.txt from boo.txt");
};
assert_eq!(ex1_txt.read_all().unwrap(), baz_txt.read_all().unwrap());
// Here is the state of the filesystem at the end of this example:
// /
// ├── bar.txt -> foo.txt
// ├── baz.txt
// ├── folder
// │ ├── boo.txt -> ../baz.txt
// │ ├── ex1.txt
// │ └── ex2.txt -> ../foo.txt
// ├── foo.txt
// └── lost+found
§How to implement a device?
To implement a device, you need to provide three methods:
-
sizewhich returns the size of the device in bytes -
commitwhich commits aCommitcreated from aSliceof the device
To help you, here is an example of how those methods can be used:
use std::vec;
use efs::dev::sector::Address;
use efs::dev::Device;
// Here, our device is a `Vec<usize>`
let mut device = vec![0_usize; 1024];
// We take a slice of the device: `slice` now contains a reference to the
// objects between the indices 256 (included) and 512 (not included) of the
// device.
let mut slice = Device::<usize, std::io::Error>::slice(
&device,
Address::try_from(256_u64).unwrap()..Address::try_from(512_u64).unwrap(),
)
.unwrap();
// We modify change each elements `0` to a `1` in the slice.
slice.iter_mut().for_each(|element| *element = 1);
// We commit the changes of slice: now this slice cannot be changed anymore.
let commit = slice.commit();
assert!(Device::<usize, std::io::Error>::commit(&mut device, commit).is_ok());
for (idx, &x) in device.iter().enumerate() {
assert_eq!(x, usize::from((256..512).contains(&idx)));
}Moreover, your implementation of a device should only returns DevErrors in case of a read/write
fail.
§How to implement a filesystem?
To implement a filesystem, you will need a lot of structures and methods. You can read the implementation of the ext2
filesystem as an example, but here is a general layout of what you need to do:
-
create a structure which will implement
FileSystem: it will be the core structure of your filesystem -
create an error structure, which implements
core::error::Error. This will contain every error that your filesystem will be able to return. -
create objects for every structure in your filesystem
-
create structures for
File,Regular,DirectoryandSymbolicLink. For each of this structure, create functions allowing to be parsed easily. ForFifo,CharacterDevice,BlockDeviceandSocket, you can use a simple struct likestruct Socket(File)as you will likely never use them with this crate -
implement the functions allowing to retrieve the
Regular,DirectoryandSymbolicLink, and therootparticularily. For thedouble_slash_root, if you don’t know what it means, you can just implement it asself.root()(and it will very probably be the right thing to do) -
implements all the other functions for the
Regular,DirectoryandSymbolicLinkstructures
Advice: start with the read-only functions and methods. It will be MUCH easier that the write methods.
Modules§
- Everything related to the devices
- Interface for
efspossible errors - General interface for Unix files.
- General interface for filesystems
- General traits for I/O interfaces.
- Path manipulation for UNIX-like filesystems.
- Interface for UNIX permissions.
- Definitions of needed types.