Skip to main content

hermit/fs/
mod.rs

1pub(crate) mod dev_directory;
2pub(crate) mod mem;
3#[cfg(feature = "uhyve")]
4pub(crate) mod uhyve;
5#[cfg(feature = "virtio-fs")]
6pub(crate) mod virtio_fs;
7
8use alloc::borrow::ToOwned;
9use alloc::boxed::Box;
10use alloc::string::String;
11use alloc::sync::Arc;
12use alloc::vec::Vec;
13use core::fmt;
14use core::mem::MaybeUninit;
15use core::ops::BitAnd;
16
17use embedded_io::{Read, Write};
18use hermit_sync::{InterruptSpinMutex, OnceCell};
19use mem::MemDirectory;
20use num_enum::{IntoPrimitive, TryFromPrimitive};
21
22use crate::errno::Errno;
23use crate::executor::block_on;
24use crate::fd::{AccessPermission, Fd, ObjectInterface, OpenOption, insert_object, remove_object};
25use crate::io;
26use crate::syscalls::DirentFormat;
27use crate::time::{SystemTime, timespec};
28
29static FILESYSTEM: OnceCell<Filesystem> = OnceCell::new();
30
31static WORKING_DIRECTORY: InterruptSpinMutex<Option<String>> = InterruptSpinMutex::new(None);
32
33static UMASK: InterruptSpinMutex<AccessPermission> =
34	InterruptSpinMutex::new(AccessPermission::from_bits_retain(0o777));
35
36#[derive(Debug, Clone)]
37pub struct DirectoryEntry {
38	pub name: String,
39}
40
41impl DirectoryEntry {
42	pub fn new(name: String) -> Self {
43		Self { name }
44	}
45}
46
47/// Type of the VNode
48#[derive(Copy, Clone, Debug, PartialEq, Eq)]
49pub(crate) enum NodeKind {
50	/// Node represent a file
51	File,
52	/// Node represent a directory
53	Directory,
54}
55
56/// VfsNode represents an internal node of the ramdisk.
57pub(crate) trait VfsNode: Send + Sync + fmt::Debug {
58	/// Determines the current node type
59	fn get_kind(&self) -> NodeKind;
60
61	/// Determines the current file attribute
62	fn get_file_attributes(&self) -> io::Result<FileAttr> {
63		Err(Errno::Nosys)
64	}
65
66	/// Determines the syscall interface
67	fn get_object(&self) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
68		Err(Errno::Nosys)
69	}
70
71	/// Creates a new directory node
72	fn traverse_mkdir(&self, _path: &str, _mode: AccessPermission) -> io::Result<()> {
73		Err(Errno::Nosys)
74	}
75
76	/// Deletes a directory node
77	fn traverse_rmdir(&self, _path: &str) -> io::Result<()> {
78		Err(Errno::Nosys)
79	}
80
81	/// Removes the specified file
82	fn traverse_unlink(&self, _path: &str) -> io::Result<()> {
83		Err(Errno::Nosys)
84	}
85
86	/// Opens a directory
87	fn traverse_readdir(&self, _path: &str) -> io::Result<Vec<DirectoryEntry>> {
88		Err(Errno::Nosys)
89	}
90
91	/// Gets file status
92	fn traverse_lstat(&self, _path: &str) -> io::Result<FileAttr> {
93		Err(Errno::Nosys)
94	}
95
96	/// Gets file status
97	fn traverse_stat(&self, _path: &str) -> io::Result<FileAttr> {
98		Err(Errno::Nosys)
99	}
100
101	/// Mounts a file system
102	fn traverse_mount(&self, _path: &str, _obj: Box<dyn VfsNode>) -> io::Result<()> {
103		Err(Errno::Nosys)
104	}
105
106	/// Opens a file
107	fn traverse_open(
108		&self,
109		_path: &str,
110		_option: OpenOption,
111		_mode: AccessPermission,
112	) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
113		Err(Errno::Nosys)
114	}
115
116	/// Creates a read-only file
117	fn traverse_create_file(
118		&self,
119		_path: &str,
120		_data: &'static [u8],
121		_mode: AccessPermission,
122	) -> io::Result<()> {
123		Err(Errno::Nosys)
124	}
125}
126
127pub(crate) struct DirectoryReader {
128	entries: Vec<DirectoryEntry>,
129	read_idx: async_lock::Mutex<usize>,
130}
131
132impl DirectoryReader {
133	pub fn new(entries: Vec<DirectoryEntry>) -> Self {
134		Self {
135			entries,
136			read_idx: async_lock::Mutex::new(0),
137		}
138	}
139}
140
141impl ObjectInterface for DirectoryReader {
142	async fn getdents(
143		&self,
144		buf: &mut [MaybeUninit<u8>],
145		format: DirentFormat,
146	) -> io::Result<usize> {
147		let mut buf_offset: usize = 0;
148		let mut read_idx = self.read_idx.lock().await;
149		for entry in self.entries.iter().skip(*read_idx) {
150			let Some(next_dirent) =
151				format.write_entry(buf, buf_offset, 1, FileType::Unknown, entry.name.as_bytes())
152			else {
153				if buf_offset == 0 {
154					// Buffer too small to hold even one entry
155					return Err(Errno::Inval);
156				}
157				// Buffer full -> return bytes written so far; caller retries from read_idx
158				break;
159			};
160
161			*read_idx += 1;
162			buf_offset = next_dirent;
163		}
164		Ok(buf_offset)
165	}
166
167	async fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
168		if whence != SeekWhence::Set && offset != 0 {
169			error!("Invalid offset for directory lseek ({offset})");
170			return Err(Errno::Inval);
171		}
172		*self.read_idx.lock().await = offset as usize;
173		Ok(offset)
174	}
175}
176
177#[derive(Debug)]
178pub(crate) struct Filesystem {
179	root: MemDirectory,
180}
181
182impl Filesystem {
183	pub fn new() -> Self {
184		Self {
185			root: MemDirectory::new(AccessPermission::from_bits(0o777).unwrap()),
186		}
187	}
188
189	/// Tries to open file at given path.
190	pub fn open(
191		&self,
192		path: &str,
193		opt: OpenOption,
194		mode: AccessPermission,
195	) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
196		debug!("Open file {path} with {opt:?}");
197
198		let path = path.strip_prefix('/').unwrap_or(path);
199
200		self.root.traverse_open(path, opt, mode)
201	}
202
203	/// Unlinks a file given by path
204	pub fn unlink(&self, path: &str) -> io::Result<()> {
205		debug!("Unlinking file {path}");
206
207		let path = path.strip_prefix('/').unwrap_or(path);
208
209		self.root.traverse_unlink(path)
210	}
211
212	/// Remove directory given by path
213	pub fn rmdir(&self, path: &str) -> io::Result<()> {
214		debug!("Removing directory {path}");
215
216		let path = path.strip_prefix('/').unwrap_or(path);
217
218		self.root.traverse_rmdir(path)
219	}
220
221	/// Create directory given by path
222	pub fn mkdir(&self, path: &str, mode: AccessPermission) -> io::Result<()> {
223		debug!("Create directory {path}");
224
225		let path = path.strip_prefix('/').unwrap_or(path);
226
227		self.root.traverse_mkdir(path, mode)
228	}
229
230	pub fn opendir(&self, path: &str) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
231		debug!("Open directory {path}");
232		Ok(Arc::new(async_lock::RwLock::new(
233			DirectoryReader::new(self.readdir(path)?).into(),
234		)))
235	}
236
237	/// List given directory
238	pub fn readdir(&self, path: &str) -> io::Result<Vec<DirectoryEntry>> {
239		debug!("Readdir {path}");
240
241		let path = path.strip_prefix('/').unwrap_or(path);
242
243		self.root.traverse_readdir(path)
244	}
245
246	/// stat
247	pub fn stat(&self, path: &str) -> io::Result<FileAttr> {
248		debug!("Getting stats {path}");
249
250		let path = path.strip_prefix('/').unwrap_or(path);
251
252		self.root.traverse_stat(path)
253	}
254
255	/// lstat
256	pub fn lstat(&self, path: &str) -> io::Result<FileAttr> {
257		debug!("Getting lstats {path}");
258
259		let path = path.strip_prefix('/').unwrap_or(path);
260
261		self.root.traverse_lstat(path)
262	}
263
264	/// Create new backing-fs at mountpoint mntpath
265	pub fn mount(&self, path: &str, obj: Box<dyn VfsNode>) -> io::Result<()> {
266		debug!("Mounting {path}");
267
268		let path = path.strip_prefix('/').unwrap_or(path);
269
270		self.root.traverse_mount(path, obj)
271	}
272
273	/// Create read-only file
274	pub fn create_file(
275		&self,
276		path: &str,
277		data: &'static [u8],
278		mode: AccessPermission,
279	) -> io::Result<()> {
280		debug!("Create read-only file {path}");
281
282		let path = path.strip_prefix('/').unwrap_or(path);
283
284		self.root.traverse_create_file(path, data, mode)
285	}
286}
287
288#[repr(C)]
289#[derive(Debug, Default, Copy, Clone)]
290pub struct FileAttr {
291	pub st_dev: u64,
292	pub st_ino: u64,
293	pub st_nlink: u64,
294	/// Access permissions
295	pub st_mode: AccessPermission,
296	/// User id
297	pub st_uid: u32,
298	/// Group id
299	pub st_gid: u32,
300	/// Device id
301	pub st_rdev: u64,
302	/// Size in bytes
303	pub st_size: i64,
304	/// Block size
305	pub st_blksize: i64,
306	/// Size in blocks
307	pub st_blocks: i64,
308	/// Time of last access
309	pub st_atim: timespec,
310	/// Time of last modification
311	pub st_mtim: timespec,
312	/// Time of last status change
313	pub st_ctim: timespec,
314}
315
316#[derive(TryFromPrimitive, IntoPrimitive, PartialEq, Eq, Clone, Copy, Debug)]
317#[repr(u8)]
318pub enum FileType {
319	Unknown = 0,         // DT_UNKNOWN
320	Fifo = 1,            // DT_FIFO
321	CharacterDevice = 2, // DT_CHR
322	Directory = 4,       // DT_DIR
323	BlockDevice = 6,     // DT_BLK
324	RegularFile = 8,     // DT_REG
325	SymbolicLink = 10,   // DT_LNK
326	Socket = 12,         // DT_SOCK
327	Whiteout = 14,       // DT_WHT
328}
329
330#[derive(TryFromPrimitive, IntoPrimitive, PartialEq, Eq, Clone, Copy, Debug)]
331#[repr(u8)]
332pub enum SeekWhence {
333	Set = 0,
334	Cur = 1,
335	End = 2,
336	Data = 3,
337	Hole = 4,
338}
339
340pub(crate) fn init() {
341	const VERSION: &str = env!("CARGO_PKG_VERSION");
342	const UTC_BUILT_TIME: &str = build_time::build_time_utc!();
343
344	let root_filesystem = Filesystem::new();
345
346	root_filesystem
347		.mkdir("/tmp", AccessPermission::from_bits(0o777).unwrap())
348		.expect("Unable to create /tmp");
349	root_filesystem
350		.mkdir("/proc", AccessPermission::from_bits(0o777).unwrap())
351		.expect("Unable to create /proc");
352
353	FILESYSTEM.set(root_filesystem).unwrap();
354
355	if let Ok(mut file) = File::create("/proc/version") {
356		if write!(file, "Hermit version {VERSION} # UTC {UTC_BUILT_TIME}").is_err() {
357			error!("Unable to write in /proc/version");
358		}
359	} else {
360		error!("Unable to create /proc/version");
361	}
362
363	*WORKING_DIRECTORY.lock() = Some("/tmp".to_owned());
364
365	#[cfg(feature = "virtio-fs")]
366	virtio_fs::init();
367
368	#[cfg(feature = "uhyve")]
369	use crate::env::UhyveStartInfo;
370
371	#[cfg(feature = "uhyve")]
372	if crate::env::start_info().is_uhyve() {
373		uhyve::init();
374	}
375
376	dev_directory::init();
377}
378
379pub fn create_file(name: &str, data: &'static [u8], mode: AccessPermission) -> io::Result<()> {
380	with_relative_filename(name, |name| {
381		FILESYSTEM
382			.get()
383			.ok_or(Errno::Inval)?
384			.create_file(name, data, mode)
385	})
386}
387
388/// Removes an empty directory.
389pub fn remove_dir(path: &str) -> io::Result<()> {
390	with_relative_filename(path, |path| {
391		FILESYSTEM.get().ok_or(Errno::Inval)?.rmdir(path)
392	})
393}
394
395pub fn unlink(path: &str) -> io::Result<()> {
396	with_relative_filename(path, |path| {
397		FILESYSTEM.get().ok_or(Errno::Inval)?.unlink(path)
398	})
399}
400
401/// Creates a new, empty directory at the provided path
402pub fn create_dir(path: &str, mode: AccessPermission) -> io::Result<()> {
403	let mask = *UMASK.lock();
404
405	with_relative_filename(path, |path| {
406		FILESYSTEM
407			.get()
408			.ok_or(Errno::Inval)?
409			.mkdir(path, mode.bitand(mask))
410	})
411}
412
413/// Returns an vector with all the entries within a directory.
414pub fn readdir(name: &str) -> io::Result<Vec<DirectoryEntry>> {
415	debug!("Read directory {name}");
416
417	with_relative_filename(name, |name| {
418		FILESYSTEM.get().ok_or(Errno::Inval)?.readdir(name)
419	})
420}
421
422pub fn read_stat(name: &str) -> io::Result<FileAttr> {
423	with_relative_filename(name, |name| {
424		FILESYSTEM.get().ok_or(Errno::Inval)?.stat(name)
425	})
426}
427
428pub fn read_lstat(name: &str) -> io::Result<FileAttr> {
429	with_relative_filename(name, |name| {
430		FILESYSTEM.get().ok_or(Errno::Inval)?.lstat(name)
431	})
432}
433
434fn with_relative_filename<F, T>(name: &str, callback: F) -> io::Result<T>
435where
436	F: FnOnce(&str) -> io::Result<T>,
437{
438	if name.starts_with("/") {
439		return callback(name);
440	}
441
442	let cwd = WORKING_DIRECTORY.lock();
443
444	let Some(cwd) = cwd.as_ref() else {
445		// Relative path with no CWD, this is weird/impossible
446		return Err(Errno::Badf);
447	};
448
449	let mut path = String::with_capacity(cwd.len() + name.len() + 1);
450	path.push_str(cwd);
451	path.push('/');
452	path.push_str(name);
453
454	callback(&path)
455}
456
457pub fn truncate(name: &str, size: usize) -> io::Result<()> {
458	with_relative_filename(name, |name| {
459		let fs = FILESYSTEM.get().ok_or(Errno::Inval)?;
460		let file = fs
461			.open(name, OpenOption::O_TRUNC, AccessPermission::empty())
462			.map_err(|_| Errno::Badf)?;
463
464		block_on(async { file.read().await.truncate(size).await }, None)
465	})
466}
467
468pub fn open(name: &str, flags: OpenOption, mode: AccessPermission) -> io::Result<RawFd> {
469	// mode is 0x777 (0b0111_0111_0111), when flags | O_CREAT, else 0
470	// flags is bitmask of O_DEC_* defined above.
471	// (taken from rust stdlib/sys hermit target )
472	let mask = *UMASK.lock();
473
474	with_relative_filename(name, |name| {
475		debug!("Open {name}, {flags:?}, {mode:?}");
476
477		let fs = FILESYSTEM.get().ok_or(Errno::Inval)?;
478		let file = fs.open(name, flags, mode.bitand(mask))?;
479		let fd = insert_object(file)?;
480		Ok(fd)
481	})
482}
483
484pub fn get_cwd() -> io::Result<String> {
485	let cwd = WORKING_DIRECTORY.lock();
486	let cwd = cwd.as_ref().ok_or(Errno::Noent)?;
487	Ok(cwd.clone())
488}
489
490pub fn set_cwd(cwd: &str) -> io::Result<()> {
491	// TODO: check that the directory exists and that permission flags are correct
492
493	let mut working_dir = WORKING_DIRECTORY.lock();
494	if cwd.starts_with("/") {
495		*working_dir = Some(cwd.to_owned());
496	} else {
497		let working_dir = working_dir.as_mut().ok_or(Errno::Badf)?;
498		working_dir.push('/');
499		working_dir.push_str(cwd);
500	}
501
502	Ok(())
503}
504
505pub fn umask(new_mask: AccessPermission) -> AccessPermission {
506	let mut lock = UMASK.lock();
507	let old = *lock;
508	*lock = new_mask;
509	old
510}
511
512/// Open a directory to read the directory entries
513pub(crate) fn opendir(name: &str) -> io::Result<RawFd> {
514	let obj = FILESYSTEM.get().ok_or(Errno::Inval)?.opendir(name)?;
515	insert_object(obj)
516}
517
518use crate::fd::{self, RawFd};
519
520pub fn file_attributes(path: &str) -> io::Result<FileAttr> {
521	FILESYSTEM.get().ok_or(Errno::Inval)?.lstat(path)
522}
523
524#[allow(clippy::len_without_is_empty)]
525#[derive(Debug, Copy, Clone)]
526pub struct Metadata(FileAttr);
527
528impl Metadata {
529	/// Returns the size of the file, in bytes
530	pub fn len(&self) -> usize {
531		self.0.st_size.try_into().unwrap()
532	}
533
534	/// Returns true if this metadata is for a file.
535	pub fn is_file(&self) -> bool {
536		self.0.st_mode.contains(AccessPermission::S_IFREG)
537	}
538
539	/// Returns true if this metadata is for a directory.
540	pub fn is_dir(&self) -> bool {
541		self.0.st_mode.contains(AccessPermission::S_IFDIR)
542	}
543
544	/// Returns the last modification time listed in this metadata.
545	pub fn modified(&self) -> io::Result<SystemTime> {
546		Ok(SystemTime::from(self.0.st_mtim))
547	}
548
549	/// Returns the last modification time listed in this metadata.
550	pub fn accessed(&self) -> io::Result<SystemTime> {
551		Ok(SystemTime::from(self.0.st_atim))
552	}
553}
554
555/// Given a path, query the file system to get information about a file, directory, etc.
556pub fn metadata(path: &str) -> io::Result<Metadata> {
557	Ok(Metadata(file_attributes(path)?))
558}
559
560#[derive(Debug)]
561pub struct File {
562	fd: RawFd,
563	path: String,
564}
565
566impl File {
567	/// Opens a file in write-only mode.
568	///
569	/// This function will create a file if it does not exist, and will truncate it if it does.
570	fn create(path: &str) -> io::Result<File> {
571		let fd = open(
572			path,
573			OpenOption::O_CREAT | OpenOption::O_TRUNC | OpenOption::O_WRONLY,
574			AccessPermission::from_bits(0o666).unwrap(),
575		)?;
576
577		Ok(File {
578			fd,
579			path: path.to_owned(),
580		})
581	}
582
583	/// Creates a new file in read-write mode; error if the file exists.
584	///
585	/// This function will create a file if it does not exist, or return
586	/// an error if it does. This way, if the call succeeds, the file
587	/// returned is guaranteed to be new.
588	pub fn create_new(path: &str) -> io::Result<Self> {
589		let fd = open(
590			path,
591			OpenOption::O_CREAT | OpenOption::O_EXCL | OpenOption::O_RDWR,
592			AccessPermission::from_bits(0o666).unwrap(),
593		)?;
594
595		Ok(File {
596			fd,
597			path: path.to_owned(),
598		})
599	}
600
601	/// Attempts to open a file in read-write mode.
602	pub fn open(path: &str) -> io::Result<Self> {
603		let fd = open(
604			path,
605			OpenOption::O_RDWR,
606			AccessPermission::from_bits(0o666).unwrap(),
607		)?;
608
609		Ok(File {
610			fd,
611			path: path.to_owned(),
612		})
613	}
614
615	pub fn metadata(&self) -> io::Result<Metadata> {
616		metadata(&self.path)
617	}
618}
619
620impl embedded_io::ErrorType for File {
621	type Error = Errno;
622}
623
624impl Read for File {
625	fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
626		fd::read(self.fd, buf)
627	}
628}
629
630impl Write for File {
631	fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
632		fd::write(self.fd, buf)
633	}
634
635	fn flush(&mut self) -> Result<(), Self::Error> {
636		Ok(())
637	}
638}
639
640impl Drop for File {
641	fn drop(&mut self) {
642		if let Err(err) = remove_object(self.fd) {
643			error!("File::drop failed: {err}");
644		}
645	}
646}