1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright (c) 2020 Thomas Lambertz, RWTH Aachen University
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use crate::synch::spinlock::Spinlock;
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;

/*
Design:
- want to support different backends. One of them virtiofs.
- want to support multiple mounted filesystems at once.
- for simplicity: no overlays. All 'folders' in / are mountpoints!
- manage all files in a global map. Do not hand out references, let syscalls operate by passing in closures (fd_op())

- we internally treat all file systems as posix filesystems.
- Have two traits. One representing a filesystem, another a file: PosixFileSystem and PosixFile
- filesystem.open creates new file
- trait methods like open return Result<....>, so we can catch errors on eg open() and NOT permanently assign an fd to it!

- have a FUSE filesystem, which implements both PosixFileSystem and PosixFile
- fuse can have various FuseInterface backends. These only have to provide fuse command send/receive capabilites.
- virtiofs implements FuseInterface and sends commands via virtio queues.

- fd management is only relevant for "user" facing code. We don't care how fuse etc. manages nodes internally.
- But we still want to have a list of open files and mounted filesystems (here in fs.rs).

Open Questions:
- what is the maximum number of open files I want to support? if small, could have static allocation, no need for hashmap?
- create Stdin/out virtual files, assign fd's 0-2. Instanciate them on program start. currently fd 0-2 are hardcoded exceptions.
- optimize callchain? how does LTO work here?:
	- app calls rust.open (which is stdlib hermit/fs.rs) [https://github.com/rust-lang/rust/blob/master/src/libstd/sys/hermit/fs.rs#L267]
	- abi::open() (hermit-sys crate)
	- [KERNEL BORDER] (uses C-interface. needed? Could just be alternative to native rust?)
	- hermit-lib/....rs/sys_open()
	- SyscallInterface.open (via &'static dyn ref)
	- Filesystem::open()
	- Fuse::open()
	- VirtiofsDriver::send_command(...)
	- [HYPERVISOR BORDER] (via virtio)
	- virtiofsd receives fuse command and sends reply

TODO:
- FileDescriptor newtype
*/

// TODO: lazy static could be replaced with explicit init on OS boot.
pub static FILESYSTEM: Spinlock<Filesystem> = Spinlock::new(Filesystem::new());

pub struct Filesystem {
	// Keep track of mount-points
	mounts: BTreeMap<String, Box<dyn PosixFileSystem>>,

	// Keep track of open files
	files: BTreeMap<u64, Box<dyn PosixFile>>,
}

impl Filesystem {
	pub const fn new() -> Self {
		Self {
			mounts: BTreeMap::new(),
			files: BTreeMap::new(),
		}
	}

	/// Returns next free file-descriptor. We map index in files BTreeMap as fd's.
	/// Done determining the current biggest stored index.
	/// This is efficient, since BTreeMap's iter() calculates min and max key directly.
	/// see https://github.com/rust-lang/rust/issues/62924
	fn assign_new_fd(&self) -> u64 {
		// BTreeMap has efficient max/min index calculation. One way to access these is the following iter.
		// Add 1 to get next never-assigned fd num
		if let Some((fd, _)) = self.files.iter().next_back() {
			fd + 1
		} else {
			3 // start at 3, to reserve stdin/out/err
		}
	}

	/// Gets a new fd for a file and inserts it into open files.
	/// Returns file descriptor
	fn add_file(&mut self, file: Box<dyn PosixFile>) -> u64 {
		let fd = self.assign_new_fd();
		self.files.insert(fd, file);
		fd
	}

	/// parses path `/MOUNTPOINT/internal-path` into mount-filesystem and internal_path
	/// Returns (PosixFileSystem, internal_path) or Error on failure.
	fn parse_path<'a, 'b>(
		&'a self,
		path: &'b str,
	) -> Result<(&'a Box<dyn PosixFileSystem>, &'b str), FileError> {
		// assert start with / (no pwd relative!), split path at /, look first element. Determine backing fs. If non existent, -ENOENT
		if !path.starts_with('/') {
			warn!("Relative paths not allowed!");
			return Err(FileError::ENOENT());
		}
		let mut pathsplit = path.splitn(3, '/');
		pathsplit.next(); // always empty, since first char is /
		let mount = pathsplit.next().unwrap();
		let internal_path = pathsplit.next().unwrap(); //TODO: this can fail from userspace, eg when passing "/test"

		if let Some(fs) = self.mounts.get(mount) {
			Ok((fs, internal_path))
		} else {
			info!(
				"Trying to open file on non-existing mount point '{}'!",
				mount
			);
			Err(FileError::ENOENT())
		}
	}

	/// Tries to open file at given path (/MOUNTPOINT/internal-path).
	/// Looks up MOUNTPOINT in mounted dirs, passes internal-path to filesystem backend
	/// Returns the file descriptor of the newly opened file, or an error on failure
	pub fn open(&mut self, path: &str, perms: FilePerms) -> Result<u64, FileError> {
		debug!("Opening file {} {:?}", path, perms);
		let (fs, internal_path) = self.parse_path(path)?;
		let file = fs.open(internal_path, perms)?;
		Ok(self.add_file(file))
	}

	pub fn close(&mut self, fd: u64) {
		debug!("Closing fd {}", fd);
		if let Some(file) = self.files.get_mut(&fd) {
			file.close().unwrap(); // TODO: handle error
		}
		self.files.remove(&fd);
	}

	/// Unlinks a file given by path
	pub fn unlink(&mut self, path: &str) -> Result<(), FileError> {
		info!("Unlinking file {}", path);
		let (fs, internal_path) = self.parse_path(path)?;
		fs.unlink(internal_path)?;
		Ok(())
	}

	/// Create new backing-fs at mountpoint mntpath
	pub fn mount(&mut self, mntpath: &str, mntobj: Box<dyn PosixFileSystem>) -> Result<(), ()> {
		info!("Mounting {}", mntpath);
		if mntpath.contains('/') {
			warn!(
				"Trying to mount at '{}', but slashes in name are not supported!",
				mntpath
			);
			return Err(());
		}

		// if mounts contains path already abort
		if self.mounts.contains_key(mntpath) {
			warn!("Mountpoint already exists!");
			return Err(());
		}

		// insert filesystem into mounts, done
		self.mounts.insert(mntpath.to_owned(), mntobj);
		Ok(())
	}

	/// Run closure on file referenced by file descriptor.
	pub fn fd_op(&mut self, fd: u64, f: impl FnOnce(&mut Box<dyn PosixFile>)) {
		f(self.files.get_mut(&fd).unwrap());
	}
}

#[derive(Debug)]
pub enum FileError {
	ENOENT(),
	ENOSYS(),
}

pub trait PosixFileSystem {
	fn open(&self, _path: &str, _perms: FilePerms) -> Result<Box<dyn PosixFile>, FileError>;
	fn unlink(&self, _path: &str) -> Result<(), FileError>;
}

pub trait PosixFile {
	fn close(&mut self) -> Result<(), FileError>;
	fn read(&mut self, len: u32) -> Result<Vec<u8>, FileError>;
	fn write(&mut self, buf: &[u8]) -> Result<u64, FileError>;
	fn lseek(&mut self, offset: isize, whence: SeekWhence) -> Result<usize, FileError>;
}

// TODO: raw is partially redundant, create nicer interface
#[derive(Clone, Copy, Debug, Default)]
pub struct FilePerms {
	pub write: bool,
	pub creat: bool,
	pub excl: bool,
	pub trunc: bool,
	pub append: bool,
	pub directio: bool,
	pub raw: u32,
	pub mode: u32,
}

pub enum SeekWhence {
	Set,
	Cur,
	End,
}