Skip to main content

reifydb_runtime/io/fs/
memory.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#[cfg(any(test, feature = "testing"))]
5use std::mem;
6use std::{
7	collections::{BTreeMap, BTreeSet},
8	path::{Path, PathBuf},
9	sync::Arc,
10};
11
12use crate::{
13	io::fs::{
14		Create, Filesystem, FsError, Len, Mkdir, Open, OpenMut, Pread, Pwrite, ReadDir, RemoveDir, Rename,
15		Result, SyncData, SyncDir, Truncate, Unlink,
16	},
17	sync::mutex::Mutex,
18};
19
20pub const SECTOR_BYTES: usize = 512;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SectorState {
24	Unwritten,
25	Dirty,
26	Durable,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct SectorMask(Vec<bool>);
31
32impl SectorMask {
33	pub fn full(sectors: usize) -> Self {
34		Self(vec![true; sectors])
35	}
36
37	pub fn empty(sectors: usize) -> Self {
38		Self(vec![false; sectors])
39	}
40
41	pub fn set(&mut self, sector: usize) {
42		if sector < self.0.len() {
43			self.0[sector] = true;
44		}
45	}
46
47	pub fn clear(&mut self, sector: usize) {
48		if sector < self.0.len() {
49			self.0[sector] = false;
50		}
51	}
52
53	pub fn is_set(&self, sector: usize) -> bool {
54		self.0.get(sector).copied().unwrap_or(false)
55	}
56
57	pub fn sectors(&self) -> usize {
58		self.0.len()
59	}
60}
61
62pub(crate) fn sector_span(offset: u64, len: usize, sector_bytes: usize) -> usize {
63	if len == 0 {
64		return 0;
65	}
66	let first = offset as usize / sector_bytes;
67	let last = (offset as usize + len - 1) / sector_bytes;
68	last - first + 1
69}
70
71struct FileData {
72	len: usize,
73	bytes: Vec<u8>,
74	sectors: Vec<SectorState>,
75	shadow: BTreeMap<usize, (SectorState, Vec<u8>)>,
76}
77
78impl FileData {
79	fn new(len: usize, sector_bytes: usize) -> Self {
80		Self {
81			len,
82			bytes: Vec::new(),
83			sectors: vec![SectorState::Unwritten; len.div_ceil(sector_bytes)],
84			shadow: BTreeMap::new(),
85		}
86	}
87
88	fn resize(&mut self, len: usize, sector_bytes: usize) {
89		self.len = len;
90		self.bytes.truncate(len);
91		self.sectors.resize(len.div_ceil(sector_bytes), SectorState::Unwritten);
92		let sectors = self.sectors.len();
93		self.shadow.retain(|sector, _| *sector < sectors);
94	}
95
96	fn materialize(&mut self, end: usize) {
97		if end > self.bytes.len() {
98			self.bytes.resize(end, 0);
99		}
100	}
101
102	fn dirty(&mut self, sector: usize, sector_bytes: usize) {
103		if !self.shadow.contains_key(&sector) {
104			let lo = sector * sector_bytes;
105			let hi = (lo + sector_bytes).min(self.len);
106			self.materialize(hi);
107			self.shadow.insert(sector, (self.sectors[sector], self.bytes[lo..hi].to_vec()));
108		}
109		self.sectors[sector] = SectorState::Dirty;
110	}
111
112	fn sync(&mut self) {
113		for state in self.sectors.iter_mut() {
114			if *state == SectorState::Dirty {
115				*state = SectorState::Durable;
116			}
117		}
118		self.shadow.clear();
119	}
120
121	#[cfg(any(test, feature = "testing"))]
122	fn revert(&mut self, sector_bytes: usize) {
123		while let Some((sector, (state, bytes))) = self.shadow.pop_first() {
124			let lo = sector * sector_bytes;
125			if lo >= self.len {
126				continue;
127			}
128			let hi = (lo + bytes.len()).min(self.len);
129			self.materialize(hi);
130			self.bytes[lo..hi].copy_from_slice(&bytes[..hi - lo]);
131			if sector < self.sectors.len() {
132				self.sectors[sector] = state;
133			}
134		}
135	}
136}
137
138pub(crate) struct MemoryFileState {
139	sector_bytes: usize,
140	data: Mutex<FileData>,
141}
142
143impl MemoryFileState {
144	fn new(len: usize, sector_bytes: usize) -> Self {
145		Self {
146			sector_bytes,
147			data: Mutex::new(FileData::new(len, sector_bytes)),
148		}
149	}
150
151	fn pread(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
152		let data = self.data.lock();
153		let offset = offset as usize;
154		if offset >= data.len {
155			return Ok(0);
156		}
157		let read = buf.len().min(data.len - offset);
158		let stored = data.bytes.len().saturating_sub(offset).min(read);
159		if stored > 0 {
160			buf[..stored].copy_from_slice(&data.bytes[offset..offset + stored]);
161		}
162		buf[stored..read].fill(0);
163		Ok(read)
164	}
165
166	fn write_masked(&self, offset: u64, buf: &[u8], mask: &SectorMask) -> Result<usize> {
167		if buf.is_empty() {
168			return Ok(0);
169		}
170		let sector_bytes = self.sector_bytes;
171		let offset = offset as usize;
172		let end = offset + buf.len();
173		let mut data = self.data.lock();
174		if end > data.len {
175			data.resize(end, sector_bytes);
176		}
177		data.materialize(end);
178		let first = offset / sector_bytes;
179		let last = (end - 1) / sector_bytes;
180		for sector in first..=last {
181			if !mask.is_set(sector - first) {
182				continue;
183			}
184			let lo = (sector * sector_bytes).max(offset);
185			let hi = (sector * sector_bytes + sector_bytes).min(end);
186			data.dirty(sector, sector_bytes);
187			data.bytes[lo..hi].copy_from_slice(&buf[lo - offset..hi - offset]);
188		}
189		Ok(buf.len())
190	}
191
192	fn sync_data(&self) -> Result<()> {
193		self.data.lock().sync();
194		Ok(())
195	}
196
197	fn truncate(&self, len: u64) -> Result<()> {
198		self.data.lock().resize(len as usize, self.sector_bytes);
199		Ok(())
200	}
201
202	fn len(&self) -> Result<u64> {
203		Ok(self.data.lock().len as u64)
204	}
205
206	#[cfg(any(test, feature = "testing"))]
207	fn revert(&self) {
208		self.data.lock().revert(self.sector_bytes);
209	}
210
211	#[cfg(any(test, feature = "testing"))]
212	fn sector_states(&self) -> Vec<SectorState> {
213		self.data.lock().sectors.clone()
214	}
215}
216
217struct State {
218	files: BTreeMap<PathBuf, Arc<MemoryFileState>>,
219	dirs: BTreeSet<PathBuf>,
220	dirty_dirs: BTreeSet<PathBuf>,
221	sector_bytes: usize,
222}
223
224impl Default for State {
225	fn default() -> Self {
226		Self {
227			files: BTreeMap::new(),
228			dirs: BTreeSet::from([PathBuf::from("/")]),
229			dirty_dirs: BTreeSet::new(),
230			sector_bytes: SECTOR_BYTES,
231		}
232	}
233}
234
235impl State {
236	fn exists(&self, path: &Path) -> bool {
237		self.files.contains_key(path) || self.dirs.contains(path)
238	}
239
240	fn parent_is_dir(&self, path: &Path) -> bool {
241		match path.parent() {
242			Some(parent) => self.dirs.contains(parent),
243			None => false,
244		}
245	}
246}
247
248#[derive(Default)]
249struct Inner {
250	state: Mutex<State>,
251}
252
253#[derive(Clone, Default)]
254pub struct MemoryFs(Arc<Inner>);
255
256impl MemoryFs {
257	pub fn new() -> Self {
258		Self::default()
259	}
260
261	pub fn with_sector_bytes(sector_bytes: usize) -> Self {
262		Self(Arc::new(Inner {
263			state: Mutex::new(State {
264				sector_bytes,
265				..State::default()
266			}),
267		}))
268	}
269
270	#[cfg(any(test, feature = "testing"))]
271	pub fn crash(&self) {
272		{
273			let mut state = self.0.state.lock();
274			let dirty: Vec<PathBuf> = mem::take(&mut state.dirty_dirs).into_iter().collect();
275			for path in dirty {
276				state.dirs.retain(|dir| !dir.starts_with(&path));
277				state.files.retain(|key, _| !key.starts_with(&path));
278			}
279		}
280		let files: Vec<Arc<MemoryFileState>> = self.0.state.lock().files.values().cloned().collect();
281		for file in files {
282			file.revert();
283		}
284	}
285
286	#[cfg(feature = "testing")]
287	pub(crate) fn detach(&self, path: &Path) -> Result<Arc<MemoryFileState>> {
288		self.0.state.lock().files.remove(path).ok_or_else(|| FsError::NotFound(path.to_path_buf()))
289	}
290
291	#[cfg(feature = "testing")]
292	pub(crate) fn attach(&self, path: &Path, state: Arc<MemoryFileState>) {
293		self.0.state.lock().files.insert(path.to_path_buf(), state);
294	}
295}
296
297pub struct MemoryFile {
298	path: PathBuf,
299	state: Arc<MemoryFileState>,
300}
301
302pub struct MemoryFileMut {
303	path: PathBuf,
304	state: Arc<MemoryFileState>,
305}
306
307impl MemoryFileMut {
308	pub fn write_masked(&self, offset: u64, buf: &[u8], mask: &SectorMask) -> Result<usize> {
309		self.state.write_masked(offset, buf, mask)
310	}
311
312	pub fn path(&self) -> &Path {
313		&self.path
314	}
315
316	pub fn sector_bytes(&self) -> usize {
317		self.state.sector_bytes
318	}
319
320	#[cfg(any(test, feature = "testing"))]
321	pub fn sector_states(&self) -> Vec<SectorState> {
322		self.state.sector_states()
323	}
324}
325
326impl MemoryFile {
327	pub fn path(&self) -> &Path {
328		&self.path
329	}
330
331	pub fn sector_bytes(&self) -> usize {
332		self.state.sector_bytes
333	}
334
335	#[cfg(any(test, feature = "testing"))]
336	pub fn sector_states(&self) -> Vec<SectorState> {
337		self.state.sector_states()
338	}
339}
340
341impl Filesystem for MemoryFs {
342	type File = MemoryFile;
343	type FileMut = MemoryFileMut;
344}
345
346impl Mkdir for MemoryFs {
347	fn mkdir(&self, path: &Path) -> Result<()> {
348		let mut state = self.0.state.lock();
349		if !state.parent_is_dir(path) {
350			return Err(FsError::NotFound(path.to_path_buf()));
351		}
352		if state.exists(path) {
353			return Err(FsError::AlreadyExists(path.to_path_buf()));
354		}
355		state.dirs.insert(path.to_path_buf());
356		state.dirty_dirs.insert(path.to_path_buf());
357		Ok(())
358	}
359}
360
361impl Create for MemoryFs {
362	fn create(&self, path: &Path, len: u64) -> Result<MemoryFileMut> {
363		let mut state = self.0.state.lock();
364		if !state.parent_is_dir(path) {
365			return Err(FsError::NotFound(path.to_path_buf()));
366		}
367		if state.exists(path) {
368			return Err(FsError::AlreadyExists(path.to_path_buf()));
369		}
370		let file = Arc::new(MemoryFileState::new(len as usize, state.sector_bytes));
371		state.files.insert(path.to_path_buf(), Arc::clone(&file));
372		Ok(MemoryFileMut {
373			path: path.to_path_buf(),
374			state: file,
375		})
376	}
377}
378
379impl Open for MemoryFs {
380	fn open(&self, path: &Path) -> Result<MemoryFile> {
381		let state = self.0.state.lock();
382		state.files
383			.get(path)
384			.map(|file| MemoryFile {
385				path: path.to_path_buf(),
386				state: Arc::clone(file),
387			})
388			.ok_or_else(|| FsError::NotFound(path.to_path_buf()))
389	}
390}
391
392impl OpenMut for MemoryFs {
393	fn open_mut(&self, path: &Path) -> Result<MemoryFileMut> {
394		let state = self.0.state.lock();
395		state.files
396			.get(path)
397			.map(|file| MemoryFileMut {
398				path: path.to_path_buf(),
399				state: Arc::clone(file),
400			})
401			.ok_or_else(|| FsError::NotFound(path.to_path_buf()))
402	}
403}
404
405impl ReadDir for MemoryFs {
406	fn read_dir(&self, path: &Path) -> Result<Vec<PathBuf>> {
407		let state = self.0.state.lock();
408		if !state.dirs.contains(path) {
409			if state.files.contains_key(path) {
410				return Err(FsError::NotADirectory(path.to_path_buf()));
411			}
412			return Err(FsError::NotFound(path.to_path_buf()));
413		}
414		let mut entries: Vec<PathBuf> = state
415			.files
416			.keys()
417			.chain(state.dirs.iter())
418			.filter(|entry| entry.parent() == Some(path))
419			.cloned()
420			.collect();
421		entries.sort();
422		Ok(entries)
423	}
424}
425
426impl Rename for MemoryFs {
427	fn rename(&self, from: &Path, to: &Path) -> Result<()> {
428		let mut state = self.0.state.lock();
429		let Some(file) = state.files.remove(from) else {
430			return Err(FsError::NotFound(from.to_path_buf()));
431		};
432		if !state.parent_is_dir(to) {
433			state.files.insert(from.to_path_buf(), file);
434			return Err(FsError::NotFound(to.to_path_buf()));
435		}
436		state.files.insert(to.to_path_buf(), file);
437		Ok(())
438	}
439}
440
441impl Unlink for MemoryFs {
442	fn unlink(&self, path: &Path) -> Result<()> {
443		let mut state = self.0.state.lock();
444		state.files.remove(path).map(|_| ()).ok_or_else(|| FsError::NotFound(path.to_path_buf()))
445	}
446}
447
448impl RemoveDir for MemoryFs {
449	fn remove_dir(&self, path: &Path) -> Result<()> {
450		let mut state = self.0.state.lock();
451		if !state.dirs.contains(path) {
452			return Err(FsError::NotFound(path.to_path_buf()));
453		}
454		state.dirs.retain(|dir| !dir.starts_with(path));
455		state.dirty_dirs.retain(|dir| !dir.starts_with(path));
456		state.files.retain(|file, _| !file.starts_with(path));
457		Ok(())
458	}
459}
460
461impl SyncDir for MemoryFs {
462	fn sync_dir(&self, path: &Path) -> Result<()> {
463		let mut state = self.0.state.lock();
464		if !state.dirs.contains(path) {
465			return Err(FsError::NotFound(path.to_path_buf()));
466		}
467		state.dirty_dirs.retain(|dirty| dirty.parent() != Some(path));
468		Ok(())
469	}
470}
471
472impl Pread for MemoryFile {
473	fn pread(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
474		self.state.pread(offset, buf)
475	}
476}
477
478impl Len for MemoryFile {
479	fn len(&self) -> Result<u64> {
480		self.state.len()
481	}
482}
483
484impl Pread for MemoryFileMut {
485	fn pread(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
486		self.state.pread(offset, buf)
487	}
488}
489
490impl Pwrite for MemoryFileMut {
491	fn pwrite(&self, offset: u64, buf: &[u8]) -> Result<usize> {
492		let mask = SectorMask::full(sector_span(offset, buf.len(), self.state.sector_bytes));
493		self.state.write_masked(offset, buf, &mask)
494	}
495}
496
497impl SyncData for MemoryFileMut {
498	fn sync_data(&self) -> Result<()> {
499		self.state.sync_data()
500	}
501}
502
503impl Truncate for MemoryFileMut {
504	fn truncate(&self, len: u64) -> Result<()> {
505		self.state.truncate(len)
506	}
507}
508
509impl Len for MemoryFileMut {
510	fn len(&self) -> Result<u64> {
511		self.state.len()
512	}
513}
514
515#[cfg(test)]
516mod tests {
517	use super::*;
518
519	fn read_all(file: &MemoryFileMut) -> Vec<u8> {
520		let len = file.len().unwrap() as usize;
521		let mut buf = vec![0u8; len];
522		assert_eq!(file.pread(0, &mut buf).unwrap(), len);
523		buf
524	}
525
526	#[test]
527	fn create_leaves_every_sector_unwritten_and_zeroed() {
528		// a fresh file owns no durable and no dirty state, so a crash must leave it exactly as created.
529		let fs = MemoryFs::new();
530		let file = fs.create(Path::new("/a"), 1024).unwrap();
531		assert_eq!(file.sector_states(), vec![SectorState::Unwritten; 2]);
532		assert_eq!(read_all(&file), vec![0u8; 1024]);
533	}
534
535	#[test]
536	fn write_dirties_every_grazed_sector_whole() {
537		// a device cannot make half a sector durable, so a one-byte overlap must dirty the entire sector.
538		let fs = MemoryFs::new();
539		let file = fs.create(Path::new("/a"), 2048).unwrap();
540		assert_eq!(file.pwrite(500, &[7u8; 20]).unwrap(), 20);
541		assert_eq!(
542			file.sector_states(),
543			vec![SectorState::Dirty, SectorState::Dirty, SectorState::Unwritten, SectorState::Unwritten]
544		);
545	}
546
547	#[test]
548	fn sync_promotes_every_dirty_sector() {
549		// only a sync may turn dirty into durable; untouched sectors must stay unwritten.
550		let fs = MemoryFs::new();
551		let file = fs.create(Path::new("/a"), 2048).unwrap();
552		file.pwrite(0, &[1u8; 600]).unwrap();
553		file.sync_data().unwrap();
554		assert_eq!(
555			file.sector_states(),
556			vec![
557				SectorState::Durable,
558				SectorState::Durable,
559				SectorState::Unwritten,
560				SectorState::Unwritten
561			]
562		);
563	}
564
565	#[test]
566	fn write_masked_skips_unmasked_sectors_but_reports_full_length() {
567		// a torn write must tell the caller it succeeded while only some sectors reached the platter.
568		let fs = MemoryFs::new();
569		let file = fs.create(Path::new("/a"), 1536).unwrap();
570		let mut mask = SectorMask::empty(3);
571		mask.set(0);
572		mask.set(2);
573		assert_eq!(file.write_masked(0, &[0xABu8; 1536], &mask).unwrap(), 1536);
574		let bytes = read_all(&file);
575		assert!(bytes[0..512].iter().all(|byte| *byte == 0xAB));
576		assert!(bytes[512..1024].iter().all(|byte| *byte == 0));
577		assert!(bytes[1024..1536].iter().all(|byte| *byte == 0xAB));
578		assert_eq!(file.sector_states(), vec![SectorState::Dirty, SectorState::Unwritten, SectorState::Dirty]);
579	}
580
581	#[test]
582	fn mask_ignores_out_of_range_indices() {
583		// a mask shorter than the span reads as unset rather than panicking, so no fault plan aborts a run.
584		let mut mask = SectorMask::empty(2);
585		mask.set(5);
586		assert_eq!(mask.sectors(), 2);
587		assert!(!mask.is_set(5));
588		let mut full = SectorMask::full(2);
589		full.clear(1);
590		assert!(full.is_set(0));
591		assert!(!full.is_set(1));
592	}
593
594	#[test]
595	fn pread_round_trips_clamps_and_stops_at_the_end() {
596		// a short read at the end must report the count copied, and a read past the end is not an error.
597		let fs = MemoryFs::new();
598		let file = fs.create(Path::new("/a"), 10).unwrap();
599		file.pwrite(0, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).unwrap();
600		let mut buf = [0u8; 4];
601		assert_eq!(file.pread(2, &mut buf).unwrap(), 4);
602		assert_eq!(buf, [3, 4, 5, 6]);
603		let mut tail = [0u8; 16];
604		assert_eq!(file.pread(5, &mut tail).unwrap(), 5);
605		assert_eq!(&tail[..5], &[6, 7, 8, 9, 10]);
606		assert_eq!(file.pread(10, &mut tail).unwrap(), 0);
607		assert_eq!(file.pread(9999, &mut tail).unwrap(), 0);
608	}
609
610	#[test]
611	fn write_past_the_end_grows_bytes_and_sectors_together() {
612		// the sector vector must track the byte vector, or a crash would index a sector that does not exist.
613		let fs = MemoryFs::new();
614		let file = fs.create(Path::new("/a"), 0).unwrap();
615		assert!(file.sector_states().is_empty());
616		assert_eq!(file.pwrite(1000, &[9u8; 10]).unwrap(), 10);
617		assert_eq!(file.len().unwrap(), 1010);
618		assert_eq!(file.sector_states(), vec![SectorState::Unwritten, SectorState::Dirty]);
619		assert_eq!(fs.open(Path::new("/a")).unwrap().sector_states().len(), 2);
620	}
621
622	#[test]
623	fn truncate_shortens_bytes_and_sector_states_together() {
624		// a shrink must drop the sectors it removed, otherwise a crash would restore bytes past the new end.
625		let fs = MemoryFs::new();
626		let file = fs.create(Path::new("/a"), 2048).unwrap();
627		file.pwrite(0, &[3u8; 2048]).unwrap();
628		file.truncate(600).unwrap();
629		assert_eq!(file.len().unwrap(), 600);
630		assert_eq!(file.sector_states().len(), 2);
631		file.truncate(100).unwrap();
632		assert_eq!(file.sector_states().len(), 1);
633		assert_eq!(read_all(&file), vec![3u8; 100]);
634		file.truncate(1024).unwrap();
635		assert_eq!(file.sector_states(), vec![SectorState::Dirty, SectorState::Unwritten]);
636	}
637
638	#[test]
639	fn crash_reverts_dirty_sectors_to_their_prior_bytes_and_state() {
640		// an unsynced write must vanish exactly, leaving the sector in the state it held before it went dirty.
641		let fs = MemoryFs::new();
642		let file = fs.create(Path::new("/a"), 1024).unwrap();
643		file.pwrite(0, &[1u8; 512]).unwrap();
644		file.sync_data().unwrap();
645		file.pwrite(0, &[2u8; 512]).unwrap();
646		file.pwrite(512, &[2u8; 512]).unwrap();
647		fs.crash();
648		let bytes = read_all(&file);
649		assert!(bytes[0..512].iter().all(|byte| *byte == 1));
650		assert!(bytes[512..1024].iter().all(|byte| *byte == 0));
651		assert_eq!(file.sector_states(), vec![SectorState::Durable, SectorState::Unwritten]);
652	}
653
654	#[test]
655	fn remove_dir_takes_every_nested_file_and_dir_and_leaves_siblings() {
656		// A half-created commit log is wiped this way; a survivor inside it would make the rebuild collide.
657		let fs = MemoryFs::new();
658		fs.mkdir(Path::new("/wal")).unwrap();
659		fs.mkdir(Path::new("/wal/p0")).unwrap();
660		fs.create(Path::new("/wal/p0/seg"), 8).unwrap();
661		fs.mkdir(Path::new("/walnut")).unwrap();
662		fs.create(Path::new("/walnut/keep"), 8).unwrap();
663
664		fs.remove_dir(Path::new("/wal")).unwrap();
665
666		assert!(matches!(fs.open(Path::new("/wal/p0/seg")), Err(FsError::NotFound(_))));
667		assert!(matches!(fs.read_dir(Path::new("/wal")), Err(FsError::NotFound(_))));
668		assert!(fs.open(Path::new("/walnut/keep")).is_ok(), "a sibling sharing the name prefix must survive");
669		fs.mkdir(Path::new("/wal")).expect("the removed path is free for a rebuild");
670	}
671
672	#[test]
673	fn crash_after_truncate_drops_shadow_beyond_the_new_end() {
674		// a shadow entry past the new end must not be replayed, or the crash would resurrect truncated bytes.
675		let fs = MemoryFs::new();
676		let file = fs.create(Path::new("/a"), 2048).unwrap();
677		file.pwrite(1536, &[8u8; 512]).unwrap();
678		file.truncate(512).unwrap();
679		fs.crash();
680		assert_eq!(file.len().unwrap(), 512);
681		assert_eq!(file.sector_states(), vec![SectorState::Unwritten]);
682	}
683
684	#[test]
685	fn sync_clears_the_shadow_so_a_later_crash_keeps_the_data() {
686		// once a sync covers a sector the bytes are durable, so a crash after it must change nothing.
687		let fs = MemoryFs::new();
688		let file = fs.create(Path::new("/a"), 512).unwrap();
689		file.pwrite(0, &[5u8; 512]).unwrap();
690		file.sync_data().unwrap();
691		fs.crash();
692		assert_eq!(read_all(&file), vec![5u8; 512]);
693		assert_eq!(file.sector_states(), vec![SectorState::Durable]);
694	}
695
696	#[test]
697	fn sector_size_is_configurable_and_consistent() {
698		// the sector size decides the blast radius of a torn write, so every handle must agree on it.
699		let fs = MemoryFs::with_sector_bytes(64);
700		let file = fs.create(Path::new("/a"), 100).unwrap();
701		assert_eq!(file.sector_bytes(), 64);
702		assert_eq!(file.sector_states().len(), 2);
703		file.pwrite(0, &[1u8; 100]).unwrap();
704		assert_eq!(file.sector_states(), vec![SectorState::Dirty, SectorState::Dirty]);
705
706		let wide = MemoryFs::new();
707		let file = wide.create(Path::new("/a"), 100).unwrap();
708		assert_eq!(file.sector_bytes(), SECTOR_BYTES);
709		file.pwrite(0, &[1u8; 100]).unwrap();
710		assert_eq!(file.sector_states(), vec![SectorState::Dirty]);
711	}
712
713	#[test]
714	fn mkdir_needs_a_parent_directory_and_rejects_an_existing_path() {
715		// a tree that accepts an orphan path lets a walk see files under a parent that never existed.
716		let fs = MemoryFs::new();
717		assert_eq!(fs.mkdir(Path::new("/a/b")), Err(FsError::NotFound(PathBuf::from("/a/b"))));
718		fs.mkdir(Path::new("/a")).unwrap();
719		assert_eq!(fs.mkdir(Path::new("/a")), Err(FsError::AlreadyExists(PathBuf::from("/a"))));
720		fs.create(Path::new("/a/f"), 0).unwrap();
721		assert_eq!(fs.mkdir(Path::new("/a/f")), Err(FsError::AlreadyExists(PathBuf::from("/a/f"))));
722	}
723
724	#[test]
725	fn create_needs_a_parent_directory_and_rejects_an_existing_path() {
726		let fs = MemoryFs::new();
727		assert!(fs.create(Path::new("/a/f"), 0).is_err());
728		fs.mkdir(Path::new("/a")).unwrap();
729		fs.create(Path::new("/a/f"), 0).unwrap();
730		assert_eq!(fs.create(Path::new("/a/f"), 0).err(), Some(FsError::AlreadyExists(PathBuf::from("/a/f"))));
731	}
732
733	#[test]
734	fn open_and_open_mut_report_a_missing_file() {
735		let fs = MemoryFs::new();
736		assert_eq!(fs.open(Path::new("/a")).err(), Some(FsError::NotFound(PathBuf::from("/a"))));
737		assert_eq!(fs.open_mut(Path::new("/a")).err(), Some(FsError::NotFound(PathBuf::from("/a"))));
738		fs.create(Path::new("/a"), 8).unwrap();
739		let file = fs.open(Path::new("/a")).unwrap();
740		assert_eq!(file.path(), Path::new("/a"));
741		assert_eq!(file.len().unwrap(), 8);
742		assert_eq!(file.sector_bytes(), SECTOR_BYTES);
743		assert_eq!(file.sector_states(), vec![SectorState::Unwritten]);
744	}
745
746	#[test]
747	fn open_shares_the_state_with_the_creating_handle() {
748		// two handles onto one path are one file, so a write through either must be visible through the other.
749		let fs = MemoryFs::new();
750		let writer = fs.create(Path::new("/a"), 512).unwrap();
751		writer.pwrite(0, &[4u8; 8]).unwrap();
752		let reader = fs.open(Path::new("/a")).unwrap();
753		let mut buf = [0u8; 8];
754		assert_eq!(reader.pread(0, &mut buf).unwrap(), 8);
755		assert_eq!(buf, [4u8; 8]);
756		assert_eq!(reader.sector_states(), vec![SectorState::Dirty]);
757	}
758
759	#[test]
760	fn read_dir_lists_direct_children_in_ascending_order() {
761		// the walk order feeds seed replay, so it must be sorted rather than whatever a hash gives back.
762		let fs = MemoryFs::new();
763		fs.mkdir(Path::new("/d")).unwrap();
764		fs.mkdir(Path::new("/d/sub")).unwrap();
765		fs.create(Path::new("/d/c"), 0).unwrap();
766		fs.create(Path::new("/d/a"), 0).unwrap();
767		fs.create(Path::new("/d/sub/deep"), 0).unwrap();
768		assert_eq!(
769			fs.read_dir(Path::new("/d")).unwrap(),
770			vec![PathBuf::from("/d/a"), PathBuf::from("/d/c"), PathBuf::from("/d/sub")]
771		);
772		assert_eq!(fs.read_dir(Path::new("/nope")).err(), Some(FsError::NotFound(PathBuf::from("/nope"))));
773		assert_eq!(fs.read_dir(Path::new("/d/a")).err(), Some(FsError::NotADirectory(PathBuf::from("/d/a"))));
774	}
775
776	#[test]
777	fn rename_moves_the_entry_and_keeps_the_contents() {
778		let fs = MemoryFs::new();
779		let file = fs.create(Path::new("/a"), 512).unwrap();
780		file.pwrite(0, &[6u8; 4]).unwrap();
781		fs.rename(Path::new("/a"), Path::new("/b")).unwrap();
782		assert_eq!(fs.open(Path::new("/a")).err(), Some(FsError::NotFound(PathBuf::from("/a"))));
783		let moved = fs.open(Path::new("/b")).unwrap();
784		let mut buf = [0u8; 4];
785		assert_eq!(moved.pread(0, &mut buf).unwrap(), 4);
786		assert_eq!(buf, [6u8; 4]);
787		assert_eq!(
788			fs.rename(Path::new("/a"), Path::new("/c")).err(),
789			Some(FsError::NotFound(PathBuf::from("/a")))
790		);
791	}
792
793	#[test]
794	fn rename_into_a_missing_directory_leaves_the_source_in_place() {
795		// a failed rename must not lose the file, or recovery would find neither name.
796		let fs = MemoryFs::new();
797		fs.create(Path::new("/a"), 0).unwrap();
798		assert_eq!(
799			fs.rename(Path::new("/a"), Path::new("/d/b")).err(),
800			Some(FsError::NotFound(PathBuf::from("/d/b")))
801		);
802		assert!(fs.open(Path::new("/a")).is_ok());
803	}
804
805	#[test]
806	fn unlink_removes_the_entry_or_reports_it_missing() {
807		let fs = MemoryFs::new();
808		fs.create(Path::new("/a"), 0).unwrap();
809		fs.unlink(Path::new("/a")).unwrap();
810		assert_eq!(fs.unlink(Path::new("/a")).err(), Some(FsError::NotFound(PathBuf::from("/a"))));
811	}
812
813	#[test]
814	fn sync_dir_checks_the_directory_exists() {
815		let fs = MemoryFs::new();
816		fs.sync_dir(Path::new("/")).unwrap();
817		assert_eq!(fs.sync_dir(Path::new("/d")).err(), Some(FsError::NotFound(PathBuf::from("/d"))));
818		fs.mkdir(Path::new("/d")).unwrap();
819		fs.sync_dir(Path::new("/d")).unwrap();
820	}
821
822	#[test]
823	fn a_cloned_handle_shares_one_filesystem() {
824		// the wasm build hands the same disk to several owners, so a clone must not fork the namespace.
825		let fs = MemoryFs::new();
826		let clone = fs.clone();
827		clone.mkdir(Path::new("/d")).unwrap();
828		assert_eq!(fs.read_dir(Path::new("/")).unwrap(), vec![PathBuf::from("/d")]);
829	}
830}