//! Layer contracts, a memory model, and differential host operation sequences. use proptest::prelude::*; use sausage_vfs::*; use std::{ io::ErrorKind, sync::{ Arc, atomic::{AtomicUsize, Ordering}, }, }; fn rw() -> Open { Open { write: true, create: true, ..Open::default() } } async fn contract(layer: &dyn Layer) { layer.mkdir("/dir").await.unwrap(); let f = layer.open("/dir/a", rw()).await.unwrap(); f.write(2, b"abc".to_vec()).await.unwrap(); f.append(b"d".to_vec()).await.unwrap(); assert_eq!(f.read(0, 100).await.unwrap(), b"\0\0abcd"); f.set_size(3).await.unwrap(); assert_eq!(f.stat().await.unwrap().size, 3); f.sync().await.unwrap(); let timestamp = std::time::UNIX_EPOCH + std::time::Duration::from_secs(12345); f.set_times(Some(timestamp), Some(timestamp)).await.unwrap(); assert_eq!(f.stat().await.unwrap().modified, Some(timestamp)); assert_eq!( layer .open( "/dir/a", Open { exclusive: true, ..rw() } ) .await .err() .unwrap() .kind(), ErrorKind::AlreadyExists ); assert_eq!( layer .open( "/dir/a", Open { directory: true, ..Open::default() } ) .await .err() .unwrap() .kind(), ErrorKind::NotADirectory ); layer.rename("/dir/a", "/dir/b").await.unwrap(); assert_eq!(f.read(2, 1).await.unwrap(), b"a"); assert_eq!(layer.list("/dir").await.unwrap()[0].name, "b"); layer.remove_file("/dir/b").await.unwrap(); assert_eq!(f.read(2, 1).await.unwrap(), b"a"); layer.remove_dir("/dir").await.unwrap(); } #[tokio::test] async fn memory_contract() { contract(&Memory::default()).await; } #[tokio::test] async fn host_contract_and_sandbox() { let tmp = tempfile::tempdir().unwrap(); let h = Host::new(tmp.path(), Process::default()).unwrap(); contract(&h).await; assert!(h.open("/../escape", rw()).await.is_err()); } #[tokio::test] async fn shared_memory_handles_append_atomically() { let memory = Memory::default(); let other = memory.clone(); let first = memory.open("/shared", rw()).await.unwrap(); let second = other.open("/shared", rw()).await.unwrap(); let a = tokio::spawn(async move { for _ in 0..100 { first.append(b"a".to_vec()).await.unwrap(); } }); let b = tokio::spawn(async move { for _ in 0..100 { second.append(b"b".to_vec()).await.unwrap(); } }); a.await.unwrap(); b.await.unwrap(); let bytes = memory .open("/shared", Open::default()) .await .unwrap() .read(0, 1000) .await .unwrap(); assert_eq!(bytes.len(), 200); assert_eq!(bytes.iter().filter(|b| **b == b'a').count(), 100); } #[tokio::test] async fn generated_snapshots() { let counter = Arc::new(AtomicUsize::new(0)); let c = counter.clone(); let mut g = Generated::default(); g.insert("/nested/data", move || { Ok(c.fetch_add(1, Ordering::SeqCst).to_string().into_bytes()) }); let a = g.open("/nested/data", Open::default()).await.unwrap(); let b = g.open("/nested/data", Open::default()).await.unwrap(); assert_eq!(a.read(0, 99).await.unwrap(), b"0"); assert_eq!(b.read(0, 99).await.unwrap(), b"1"); assert!(a.read(1, 99).await.unwrap().is_empty()); assert_eq!(g.list("/").await.unwrap()[0].name, "nested"); assert_eq!( g.remove_file("/nested/data").await.unwrap_err().kind(), ErrorKind::ReadOnlyFilesystem ); } #[tokio::test] async fn mounts_resolve_and_protect() { let mut mounts = Mounts::default(); mounts.mount("/", Arc::new(Memory::default()), false); mounts.mount("/proc", Arc::new(Memory::default()), true); mounts.mount("/run", Arc::new(Memory::default()), false); assert_eq!( mounts .list("/") .await .unwrap() .iter() .map(|e| e.name.as_str()) .collect::>(), ["proc", "run"] ); mounts.open("/proc/../a", rw()).await.unwrap(); assert_eq!( mounts.open("/proc/a", rw()).await.err().unwrap().kind(), ErrorKind::ReadOnlyFilesystem ); assert_eq!( mounts.rename("/a", "/run/a").await.unwrap_err().kind(), ErrorKind::CrossesDevices ); } #[tokio::test] async fn aborted_operations_keep_guard() { let p = Process::default(); let q = p.clone(); let (started_tx, started_rx) = tokio::sync::oneshot::channel(); let (release_tx, release_rx) = std::sync::mpsc::channel(); let operation = tokio::spawn(async move { q.blocking(move || { started_tx.send(()).unwrap(); release_rx.recv().unwrap(); Ok(()) }) .await }); started_rx.await.unwrap(); operation.abort(); let mut idle = Box::pin(p.idle()); assert!(matches!( std::future::poll_fn(|cx| std::task::Poll::Ready(idle.as_mut().poll(cx))).await, std::task::Poll::Pending )); release_tx.send(()).unwrap(); idle.await; } proptest! { #[test] fn memory_model(ops in prop::collection::vec((0u8..3,0usize..64,prop::collection::vec(any::(),0..20)),0..80)) { tokio::runtime::Runtime::new().unwrap().block_on(async { let m = Memory::default(); let f = m.open("/f",rw()).await.unwrap(); let mut model = Vec::new(); for (op,offset,bytes) in ops { match op { 0 => { f.write(offset as u64,bytes.clone()).await.unwrap(); if !bytes.is_empty() { model.resize(model.len().max(offset+bytes.len()),0); model[offset..offset+bytes.len()].copy_from_slice(&bytes); } }, 1 => { f.append(bytes.clone()).await.unwrap(); model.extend(bytes); }, _ => { f.set_size(offset as u64).await.unwrap(); model.resize(offset,0); } } prop_assert_eq!(f.read(0,1000).await.unwrap(),model.clone()); } Ok(()) })?; } #[test] fn host_differential(ops in prop::collection::vec((0u8..8,0u8..5,prop::collection::vec(any::(),0..30)),0..50)) { tokio::runtime::Runtime::new().unwrap().block_on(async { let a = tempfile::tempdir().unwrap(); let b = tempfile::tempdir().unwrap(); let host = Host::new(a.path(),Process::default()).unwrap(); for (op,index,data) in ops { let name = format!("f{index}"); let path = format!("/{name}"); let real = b.path().join(&name); let (actual,expected) = match op { 0 => { let actual = match host.open(&path,Open { truncate:true,..rw() }).await { Ok(f) => f.write(0,data.clone()).await.map(|_|()), Err(e) => Err(e) }; (actual,std::fs::write(real,&data)) }, 1 => (host.remove_file(&path).await,std::fs::remove_file(real)), 2 => (host.mkdir(&path).await,std::fs::create_dir(real)), 3 => (host.remove_dir(&path).await,std::fs::remove_dir(real)), 4 => (host.rename(&path,"/renamed").await,std::fs::rename(real,b.path().join("renamed"))), _ => { use std::io::{Seek, SeekFrom, Write}; let actual = match host.open(&path,Open { write:true,..Open::default() }).await { Ok(f) => match op { 5 => f.append(data.clone()).await.map(|_|()), 6 => f.set_size(data.len() as u64).await, _ => f.write(index as u64,data.clone()).await.map(|_|()) }, Err(e) => Err(e), }; let expected = (|| { let mut f = std::fs::OpenOptions::new().read(true).write(true).append(op == 5).open(real)?; match op { 5 => f.write_all(&data), 6 => f.set_len(data.len() as u64), _ => { f.seek(SeekFrom::Start(index as u64))?; f.write_all(&data) } } })(); (actual,expected) } }; prop_assert_eq!(actual.map_err(|e|e.kind()),expected.map_err(|e|e.kind())); let mut expected = std::fs::read_dir(b.path()).unwrap().map(|e| { let e=e.unwrap(); Entry { name:e.file_name().to_str().unwrap().into(),directory:e.file_type().unwrap().is_dir() } }).collect::>(); expected.sort_by(|a,b|a.name.cmp(&b.name)); for entry in &expected { let p = format!("/{}", entry.name); let metadata = host.stat(&p).await.unwrap(); prop_assert_eq!(metadata.directory, entry.directory); if !entry.directory { let bytes = std::fs::read(b.path().join(&entry.name)).unwrap(); prop_assert_eq!(metadata.size, bytes.len() as u64); prop_assert_eq!(host.open(&p, Open::default()).await.unwrap().read(0, 1024).await.unwrap(), bytes); } } prop_assert_eq!(host.list("/").await.unwrap(),expected); } Ok(()) })?; } }