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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#![no_std]
#![feature(allocator_internals)]
#![needs_allocator]
#![allow(incomplete_features)]
#![feature(alloc_prelude)]
#![feature(trait_upcasting)]
#![feature(get_mut_unchecked)]

extern crate alloc;
#[cfg(feature = "std")]
extern crate std;

#[macro_use]
extern crate slos_log;

#[allow(unused_imports)]
use self::alloc_prelude::*;
use alloc::prelude::v1 as alloc_prelude;

use core::fmt::{self, Debug};
use lasso::{Rodeo, Spur};
use slos_helpers::{StaticCollection, UnsafeContainer};

lazy_static::lazy_static! {
	/// String interner used for path segments
	static ref INTERNED: UnsafeContainer<Rodeo> = UnsafeContainer::new(Rodeo::new());
}

mod errors;
pub use self::errors::*;
pub mod path;

pub mod impls;

/// Directory read functions
///
/// The default implementations of functions in this trait will always return
/// [`FsError::InvalidArgument`].
pub trait FsReadDir {
	/// Return an [`FsNode`] reference for each node in this directory
	fn readdir(&mut self) -> Result<Vec<&mut dyn FsNode>, FsError> {
		Err(FsError::InvalidArgument)
	}
}

/// Directory write functions
///
/// The default implementations of functions in this trait will always return
/// [`FsError::InvalidArgument`].
pub trait FsWriteDir {
	/// Create a new empty file in this directory
	fn touch(&mut self, _name: &str) -> Result<&mut dyn FsNode, FsError> {
		Err(FsError::InvalidArgument)
	}

	/// Create a new empty directory in this directory
	fn mkdir(&mut self, _name: &str) -> Result<&mut dyn FsNode, FsError> {
		Err(FsError::InvalidArgument)
	}
}

/// Filesystem node
pub trait FsNode: Debug {
	/// Get the inode value for this node
	fn inode(&self) -> usize;

	/// Get the filename of this node
	fn name(&self) -> &str;

	/// Get the permissions of this node
	fn permissions(&self) -> u16;

	/// Try to get this node as a [`FsRoot`] trait object reference
	///
	/// Will always return [`None`][Option::None] if this node is not the
	/// root of a filesystem.
	fn try_root(&mut self) -> Option<&mut dyn FsRoot> {
		None
	}

	/// Try to get this node as a [`FsDirectory`] trait object reference
	///
	/// Will always return [`None`][Option::None] if this node is a file.
	fn try_directory(&mut self) -> Option<&mut dyn FsDirectory> {
		None
	}

	/// Try to get this node as a [`FsFile`] trait object reference
	///
	/// Will always return [`None`][Option::None] if this node is a directory.
	fn try_file(&mut self) -> Option<&mut dyn FsFile> {
		None
	}
}

/// A directory on a filesystem
pub trait FsDirectory: FsNode + FsReadDir + FsWriteDir {}

/// A file on a filesystem
pub trait FsFile: FsNode {
	fn open(&mut self) -> Result<&mut dyn FsFileHandle, FsError>;
}

/// Mountable filesystem root
pub trait FsRoot: Send + FsDirectory + Debug {}

/// Read/write handle to a [`FsFile`]
pub trait FsFileHandle: Debug {
	/// Try to read from the file
	///
	/// Attempts to read `length` bytes from the `offset` into the file.
	/// If `length` is [`None`][Option::None], read from `offset` to the
	/// end of the file.
	///
	/// If the file is write-only, this should return [`FsError::InvalidArgument`].
	///
	/// If the file is only able to be read from as a stream (for example,
	/// character devices such as terminals), in the case where `offset` is
	/// non-zero and/or `length` is not [`None`][Option::None], this should
	/// return [`FsError::InvalidArgument`].
	fn raw_read(&mut self, offset: usize, length: Option<usize>) -> Result<Vec<u8>, FsError>;

	/// Try to write to the file
	///
	/// Attempts to write the `data` to the `offset` into the file, replacing
	/// any existing content.
	///
	/// It is implementation-dependent whether this function will truncate the
	/// supplied data if it is too large to fit in the file (for example, block
	/// devices), however implementations **should** try to allocate more space
	/// on the physical filesystem and extend the file to fit the entire data.
	///
	/// If the file is read-only, this should return [`FsError::InvalidArgument`].
	///
	/// If the file is only able to be written to as a stream (for example,
	/// character devices such as terminals), in the case where `offset` is
	/// non-zero, this should return [`FsError::InvalidArgument`].
	fn raw_write(&mut self, offset: usize, data: &[u8]) -> Result<(), FsError>;
}

/// Container for filesystem mountpoint roots
///
/// This mostly exists as an implementation detail of [`FilesystemBase`].
#[derive(Default)]
pub struct FilesystemMountpoint {
	/// Segments of the path making up the mountpoint
	///
	/// Paths are stored internally as interned strings, see the
	/// [`path_string`][FilesystemMountpoint::path_string] method to get the
	/// path in a format that can be used outside of this module.
	pub path: StaticCollection<Option<Spur>>,

	/// Filesystem root, as a contained [`FsRoot`] trait object
	pub root: Option<UnsafeContainer<Box<dyn FsRoot>>>,
}

impl FilesystemMountpoint {
	/// Get the absolute path to this mountpoint
	///
	/// This function takes care of resolving the interned strings that make
	/// up the `path` field of this structure, but no post-conversion path
	/// normalization is performed before returning.
	///
	/// If you're planning to use the path for anything other than passing straight
	/// back into [`FilesystemBase`], unless you _really_ know what you're doing,
	/// please use the [`path_string`][FilesystemMountpoint::path_string] method
	/// instead.
	pub fn path_vec(&self) -> Vec<&'static str> {
		self.path
			.as_slice()
			.iter()
			.map(|x| x.unwrap_or(INTERNED.get().get_or_intern("[unknown]")))
			.map(|x| INTERNED.resolve(&x))
			.collect::<Vec<&str>>()
	}

	/// Get the absolute path to this mountpoint as a String
	///
	/// This function performs path normalization, so the returned path is safe
	/// to use in non-internal methods.
	pub fn path_string(&self) -> String {
		let segs = self
			.path_vec()
			.iter()
			.map(|x| String::from(*x))
			.collect::<Vec<String>>();

		path::join(&segs)
	}
}

impl Debug for FilesystemMountpoint {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.debug_struct("FilesystemMountpoint")
			.field("path", &self.path_vec())
			.finish()
	}
}

/// Base structure for mounting filesystems to
#[derive(Debug)]
pub struct FilesystemBase {
	/// Collection of mounted filesystems
	pub mountpoints: StaticCollection<UnsafeContainer<FilesystemMountpoint>>,
}

impl FilesystemBase {
	/// Create a new empty `FilesystemBase` instance.
	pub fn new() -> Self {
		Self {
			mountpoints: StaticCollection::new(),
		}
	}

	/// Mount a filesystem root at `path`.
	pub fn mount(&mut self, path: &[&str], root: Box<dyn FsRoot>) -> Result<(), MountError> {
		let mut path_segments: StaticCollection<Option<Spur>> = StaticCollection::new();
		for seg in path.into_iter() {
			path_segments.push(Some(INTERNED.get().get_or_intern(seg)));
		}

		let mountpoint = FilesystemMountpoint {
			path: path_segments,
			root: Some(UnsafeContainer::new(root)),
		};

		trace!("mountpoint={:?}", &mountpoint);
		self.mountpoints.push(UnsafeContainer::new(mountpoint));
		Ok(())
	}

	/// Return an [`FsNode`] for the given `path`, if one exists.
	///
	/// If the given `path` is exactly the root of a mounted filesystem, returns
	/// the filesystem root object, casted to an [`FsNode`] (so you can treat it
	/// as a normal directory).
	///
	/// If the given `path` is not the root of a mounted filesystem, traverses
	/// the parent filesystem of the path to find the node at the path within
	/// that filesystem.
	///
	/// Returns [`FsError::FileNotFound`] if a node could not be found at the path,
	/// or [`FsError::FilesystemRootError`] if the parent filesystem of the path
	/// was not able to be cast to an [`FsNode`] (which would only ever happen if
	/// mounting the filesystem failed spectactularly, or the mountpoint table had
	/// been messed with).
	pub fn node_at_path<'a>(&mut self, path: &[&str]) -> Result<&'a mut (dyn FsNode), FsError> {
		let path = crate::path::split(&crate::path::join(
			&path
				.iter()
				.map(|x| String::from(*x))
				.collect::<Vec<String>>(),
		));

		// find closest parent mountpoint …
		let mut closest: Option<&UnsafeContainer<FilesystemMountpoint>> = None;

		// … starting with the root filesystem …
		'ep: for fs in self.mountpoints.as_slice().iter() {
			if fs.get().path_vec().is_empty() {
				trace!("fs={:?}", fs);
				closest = Some(fs);
				break 'ep;
			}
		}

		// … and then checking for path prefixes
		let mut xsc = 0usize;
		for fs in self.mountpoints.as_slice().iter() {
			let pathvec = fs.get().path_vec();
			let mut startcount = 0usize;

			'uidx: for (unit, idx) in pathvec.iter().zip(0..) {
				if path.len() >= idx && &path[idx] == unit {
					startcount += 1;
				} else {
					break 'uidx;
				}
			}

			if startcount > xsc {
				trace!("startcount={:?} xsc={:?} fs={:?}", startcount, xsc, fs);
				closest = Some(fs);
				xsc = startcount;
			}
		}

		// if none closest (which would only happen if we have no rootfs) then
		// return a FileNotFound
		if closest.is_none() {
			trace!("couldn't find a mountpoint close to {:?}", &path);
			return Err(FsError::FileNotFound);
		}

		// unwrap the mountpoint
		let mountpoint = closest.unwrap();

		// get the remaining path segments after this mountpoint
		let path_remaining = {
			if mountpoint.get().path.as_slice().is_empty() {
				path
			} else {
				let (_, r) = path.split_at(mountpoint.get().path_vec().len());
				r.to_vec()
			}
		};

		trace!(
			"mountpoint={:?} path_remaining={:?}",
			mountpoint,
			path_remaining
		);

		// get the mountpoint as an FsNode
		let mount_root = match &mountpoint.get().root {
			Some(root) => root.get().as_mut() as &mut dyn FsNode,

			None => {
				return Err(FsError::FilesystemRootError);
			}
		};

		// traverse the mountpoint for the node
		match traverse_node(mount_root, path_remaining.clone(), false) {
			Some(node) => Ok(node),
			None => Err(FsError::FileNotFound),
		}
	}
}

/// Traverse the directory node `root` to find the node at `subpath`
///
/// If `ignore_root` is `true`, this method will return `None` instead of
/// `Some(root)` when `subpath` is empty.
pub fn traverse_node<'x>(
	root: &'x mut dyn FsNode,
	mut subpath: Vec<String>,
	ignore_root: bool,
) -> Option<&'x mut dyn FsNode> {
	subpath.reverse();
	let root_inode = root.inode();

	let current_node: UnsafeContainer<&'x mut dyn FsNode> = UnsafeContainer::new(root);
	'fsearch: while let Some(path_seg) = subpath.pop() {
		if let Some(dir) = current_node.get().try_directory() {
			if let Ok(rd) = dir.readdir() {
				for new in rd {
					if new.name() == path_seg {
						trace!("found next node, name={:?}", path_seg);
						current_node.replace(new);
						continue 'fsearch;
					}
				}
			}
		}

		// if we have no path segments left at this point, we have our node
		if subpath.is_empty() {
			break 'fsearch;
		}
	}

	let node = current_node.into_inner();
	if ignore_root && node.try_root().is_some() && node.inode() == root_inode {
		trace!("ignore_root set, returning None");
		return None;
	} else {
		trace!("we've got our node, returning {:?}", node);
		return Some(node);
	}
}