diff options
Diffstat (limited to 'src/server/tests.rs')
-rw-r--r-- | src/server/tests.rs | 1351 |
1 files changed, 1351 insertions, 0 deletions
diff --git a/src/server/tests.rs b/src/server/tests.rs new file mode 100644 index 0000000..0c0e73d --- /dev/null +++ b/src/server/tests.rs @@ -0,0 +1,1351 @@ +// Copyright 2019 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::borrow::Cow; +use std::collections::HashSet; +use std::collections::VecDeque; +use std::env; +use std::ffi::CString; +use std::ffi::OsString; +use std::fs; +use std::fs::File; +use std::io; +use std::io::Cursor; +use std::mem; +use std::ops::Deref; +use std::os::unix::ffi::OsStringExt; +use std::os::unix::fs::symlink; +use std::os::unix::fs::MetadataExt; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use std::u32; + +use super::*; + +// Used to indicate that there is no fid associated with this message. +const P9_NOFID: u32 = u32::MAX; + +// The fid associated with the root directory of the server. +const ROOT_FID: u32 = 1; + +// The pid of the server process, cannot be 1 since that's the kernel init +const SERVER_PID: u32 = 5; + +// How big we want the default buffer to be when running tests. +const DEFAULT_BUFFER_SIZE: u32 = 4096; + +// How big we want to make randomly generated files +const LOCAL_FILE_LEN: u64 = 200; + +// Joins `path` to `buf`. If `path` is '..', removes the last component from `buf` +// only if `buf` != `root` but does nothing if `buf` == `root`. Pushes `path` onto +// `buf` if it is a normal path component. +// +// Returns an error if `path` is absolute, has more than one component, or contains +// a '.' component. +fn join_path<P: AsRef<Path>, R: AsRef<Path>>( + mut buf: PathBuf, + path: P, + root: R, +) -> io::Result<PathBuf> { + let path = path.as_ref(); + let root = root.as_ref(); + debug_assert!(buf.starts_with(root)); + + if path.components().count() > 1 { + return Err(io::Error::from_raw_os_error(libc::EINVAL)); + } + + for component in path.components() { + match component { + // Prefix should only appear on windows systems. + Component::Prefix(_) => return Err(io::Error::from_raw_os_error(libc::EINVAL)), + // Absolute paths are not allowed. + Component::RootDir => return Err(io::Error::from_raw_os_error(libc::EINVAL)), + // '.' elements are not allowed. + Component::CurDir => return Err(io::Error::from_raw_os_error(libc::EINVAL)), + Component::ParentDir => { + // We only remove the parent path if we are not already at the root of the + // file system. + if buf != root { + buf.pop(); + } + } + Component::Normal(element) => buf.push(element), + } + } + + Ok(buf) +} + +// Automatically deletes the path it contains when it goes out of scope. +struct ScopedPath<P: AsRef<Path>>(P); + +impl<P: AsRef<Path>> AsRef<Path> for ScopedPath<P> { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl<P: AsRef<Path>> Deref for ScopedPath<P> { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl<P: AsRef<Path>> Drop for ScopedPath<P> { + fn drop(&mut self) { + if let Err(e) = fs::remove_dir_all(&**self) { + println!("Failed to remove {}: {}", self.display(), e); + } + } +} + +enum DirEntry<'a> { + File { + name: &'a str, + content: &'a [u8], + }, + Directory { + name: &'a str, + entries: &'a [DirEntry<'a>], + }, + Symlink { + name: &'a str, + target: &'a str, + }, +} + +impl<'a> DirEntry<'a> { + // Creates `self` in the path given by `dir`. + // TODO(b/228627457): clippy is warning about the `Cow` below, but it is necessary + #[allow(clippy::ptr_arg)] + fn create(&self, dir: &mut Cow<Path>) { + match *self { + DirEntry::File { name, content } => { + let mut f = File::create(dir.join(name)).expect("failed to create file"); + f.write_all(content).expect("failed to write file content"); + } + DirEntry::Directory { name, entries } => { + dir.to_mut().push(name); + + fs::create_dir_all(&**dir).expect("failed to create directory"); + for e in entries { + e.create(dir); + } + + assert!(dir.to_mut().pop()); + } + DirEntry::Symlink { name, target } => { + symlink(target, dir.join(name)).expect("failed to create symlink"); + } + } + } +} + +// Creates a file with `name` in `dir` and fills it with random +// content. +fn create_local_file<P: AsRef<Path>>(dir: P, name: &str) -> Vec<u8> { + let mut content = Vec::new(); + File::open("/dev/urandom") + .and_then(|f| f.take(LOCAL_FILE_LEN).read_to_end(&mut content)) + .expect("failed to read from /dev/urandom"); + + let f = DirEntry::File { + name, + content: &content, + }; + f.create(&mut Cow::from(dir.as_ref())); + + content +} + +// Create a symlink named `name` that links to `target`. +fn create_local_symlink<P: AsRef<Path>>(dir: P, name: &str, target: &str) { + let f = DirEntry::Symlink { name, target }; + f.create(&mut Cow::from(dir.as_ref())); +} + +fn check_qid(qid: &Qid, md: &fs::Metadata) { + let ty = if md.is_dir() { + P9_QTDIR + } else if md.is_file() { + P9_QTFILE + } else if md.file_type().is_symlink() { + P9_QTSYMLINK + } else { + panic!("unknown file type: {:?}", md.file_type()); + }; + assert_eq!(qid.ty, ty); + assert_eq!(qid.version, md.mtime() as u32); + assert_eq!(qid.path, md.ino()); +} + +fn check_attr(server: &mut Server, fid: u32, md: &fs::Metadata) { + let tgetattr = Tgetattr { + fid, + request_mask: P9_GETATTR_BASIC, + }; + + let rgetattr = server.get_attr(&tgetattr).expect("failed to call get_attr"); + + let ty = if md.is_dir() { + P9_QTDIR + } else if md.is_file() { + P9_QTFILE + } else if md.file_type().is_symlink() { + P9_QTSYMLINK + } else { + panic!("unknown file type: {:?}", md.file_type()); + }; + assert_eq!(rgetattr.valid, P9_GETATTR_BASIC); + assert_eq!(rgetattr.qid.ty, ty); + assert_eq!(rgetattr.qid.version, md.mtime() as u32); + assert_eq!(rgetattr.qid.path, md.ino()); + assert_eq!(rgetattr.mode, md.mode()); + assert_eq!(rgetattr.uid, md.uid()); + assert_eq!(rgetattr.gid, md.gid()); + assert_eq!(rgetattr.nlink, md.nlink()); + assert_eq!(rgetattr.rdev, md.rdev()); + assert_eq!(rgetattr.size, md.size()); + assert_eq!(rgetattr.atime_sec, md.atime() as u64); + assert_eq!(rgetattr.atime_nsec, md.atime_nsec() as u64); + assert_eq!(rgetattr.mtime_sec, md.mtime() as u64); + assert_eq!(rgetattr.mtime_nsec, md.mtime_nsec() as u64); + assert_eq!(rgetattr.ctime_sec, md.ctime() as u64); + assert_eq!(rgetattr.ctime_nsec, md.ctime_nsec() as u64); + assert_eq!(rgetattr.btime_sec, 0); + assert_eq!(rgetattr.btime_nsec, 0); + assert_eq!(rgetattr.gen, 0); + assert_eq!(rgetattr.data_version, 0); +} + +fn check_content(server: &mut Server, content: &[u8], fid: u32) { + for offset in 0..content.len() { + let tread = Tread { + fid, + offset: offset as u64, + count: DEFAULT_BUFFER_SIZE, + }; + + let rread = server.read(&tread).expect("failed to read file"); + assert_eq!(content[offset..], rread.data[..]); + } +} + +fn walk<P: Into<PathBuf>>( + server: &mut Server, + start: P, + fid: u32, + newfid: u32, + names: Vec<String>, +) { + let mut mds = Vec::with_capacity(names.len()); + let mut buf = start.into(); + for name in &names { + buf.push(name); + mds.push( + buf.symlink_metadata() + .expect("failed to get metadata for path"), + ); + } + + let twalk = Twalk { + fid, + newfid, + wnames: names, + }; + + let rwalk = server.walk(twalk).expect("failed to walk directoy"); + assert_eq!(mds.len(), rwalk.wqids.len()); + for (md, qid) in mds.iter().zip(rwalk.wqids.iter()) { + check_qid(qid, md); + } +} + +fn open<P: Into<PathBuf>>( + server: &mut Server, + dir: P, + dir_fid: u32, + name: &str, + fid: u32, + flags: u32, +) -> io::Result<Rlopen> { + let wnames = if name.is_empty() { + vec![] + } else { + vec![String::from(name)] + }; + walk(server, dir, dir_fid, fid, wnames); + + let tlopen = Tlopen { fid, flags }; + + server.lopen(&tlopen) +} + +fn write<P: AsRef<Path>>(server: &mut Server, dir: P, name: &str, fid: u32, flags: u32) { + let file_path = dir.as_ref().join(name); + let file_len = if file_path.exists() { + fs::symlink_metadata(&file_path) + .expect("unable to get metadata for file") + .len() as usize + } else { + 0usize + }; + let mut new_content = Vec::new(); + File::open("/dev/urandom") + .and_then(|f| f.take(200).read_to_end(&mut new_content)) + .expect("failed to read from /dev/urandom"); + + let twrite = Twrite { + fid, + offset: 0, + data: Data(new_content), + }; + + let rwrite = server.write(&twrite).expect("failed to write file"); + assert_eq!(rwrite.count, twrite.data.len() as u32); + + let tfsync = Tfsync { fid, datasync: 0 }; + server.fsync(&tfsync).expect("failed to sync file contents"); + + let actual_content = fs::read(file_path).expect("failed to read back content from file"); + + // If the file was opened append-only, then the content should have been + // written to the end even though the offset was 0. + let idx = if flags & P9_APPEND == 0 { 0 } else { file_len }; + assert_eq!(actual_content[idx..], twrite.data[..]); +} + +fn create<P: Into<PathBuf>>( + server: &mut Server, + dir: P, + dir_fid: u32, + fid: u32, + name: &str, + flags: u32, + mode: u32, +) -> io::Result<Rlcreate> { + // The `fid` in the lcreate call initially points to the directory + // but is supposed to point to the newly created file after the call + // completes. Duplicate the fid so that we don't end up consuming the + // directory fid. + walk(server, dir, dir_fid, fid, Vec::new()); + + let tlcreate = Tlcreate { + fid, + name: String::from(name), + flags, + mode, + gid: 0, + }; + + server.lcreate(tlcreate) +} + +struct Readdir<'a> { + server: &'a mut Server, + fid: u32, + offset: u64, + cursor: Cursor<Vec<u8>>, +} + +impl<'a> Iterator for Readdir<'a> { + type Item = Dirent; + + fn next(&mut self) -> Option<Self::Item> { + if self.cursor.position() >= self.cursor.get_ref().len() as u64 { + let treaddir = Treaddir { + fid: self.fid, + offset: self.offset, + count: DEFAULT_BUFFER_SIZE, + }; + + let Rreaddir { data } = self + .server + .readdir(&treaddir) + .expect("failed to read directory"); + if data.is_empty() { + // No more entries. + return None; + } + + mem::drop(mem::replace(&mut self.cursor, Cursor::new(data.0))); + } + + let dirent: Dirent = WireFormat::decode(&mut self.cursor).expect("failed to decode dirent"); + self.offset = dirent.offset; + + Some(dirent) + } +} + +fn readdir(server: &mut Server, fid: u32) -> Readdir { + Readdir { + server, + fid, + offset: 0, + cursor: Cursor::new(Vec::new()), + } +} + +// Sets up the server to start handling messages. Creates a new temporary +// directory to act as the server root and sends an initial Tattach message. +// At the end of setup, fid 1 points to the root of the server. +fn setup<P: AsRef<Path>>(name: P) -> (ScopedPath<OsString>, Server) { + let mut test_dir = env::var_os("T") + .map(PathBuf::from) + .unwrap_or_else(env::temp_dir); + test_dir.push(name); + + let mut os_str = OsString::from(test_dir); + os_str.push(".XXXXXX"); + + // Create a c string and release ownership. This seems like the only way + // to get a *mut c_char. + let buf = CString::new(os_str.into_vec()) + .expect("failed to create CString") + .into_raw(); + + // Safe because this will only modify the contents of `buf`. + let ret = unsafe { libc::mkdtemp(buf) }; + + // Take ownership of the buffer back before checking the result. Safe because + // this was created by a call to into_raw() above and mkdtemp will not overwrite + // the trailing '\0'. + let buf = unsafe { CString::from_raw(buf) }; + + assert!(!ret.is_null()); + + let test_dir = ScopedPath(OsString::from_vec(buf.into_bytes())); + + // Create a basic file system hierarchy. + let entries = [ + DirEntry::Directory { + name: "subdir", + entries: &[ + DirEntry::File { + name: "b", + content: b"hello, world!", + }, + DirEntry::Directory { + name: "nested", + entries: &[DirEntry::File { + name: "Огонь по готовности!", + content: &[ + 0xe9u8, 0xbeu8, 0x8du8, 0xe3u8, 0x81u8, 0x8cu8, 0xe6u8, 0x88u8, 0x91u8, + 0xe3u8, 0x81u8, 0x8cu8, 0xe6u8, 0x95u8, 0xb5u8, 0xe3u8, 0x82u8, 0x92u8, + 0xe5u8, 0x96u8, 0xb0u8, 0xe3u8, 0x82u8, 0x89u8, 0xe3u8, 0x81u8, 0x86u8, + 0x21u8, + ], + }], + }, + ], + }, + DirEntry::File { + name: "世界.txt", + content: &[ + 0xe3u8, 0x81u8, 0x93u8, 0xe3u8, 0x82u8, 0x93u8, 0xe3u8, 0x81u8, 0xabu8, 0xe3u8, + 0x81u8, 0xa1u8, 0xe3u8, 0x81u8, 0xafu8, + ], + }, + ]; + + for e in &entries { + e.create(&mut Cow::from(&*test_dir)); + } + + let md = test_dir + .symlink_metadata() + .expect("failed to get metadata for root dir"); + + let mut server = Server::new(&*test_dir, Default::default(), Default::default()) + .expect("Failed to create server"); + + let tversion = Tversion { + msize: DEFAULT_BUFFER_SIZE, + version: String::from("9P2000.L"), + }; + + let rversion = server + .version(&tversion) + .expect("failed to get version from server"); + assert_eq!(rversion.msize, DEFAULT_BUFFER_SIZE); + assert_eq!(rversion.version, "9P2000.L"); + + let tattach = Tattach { + fid: ROOT_FID, + afid: P9_NOFID, + uname: String::from("unittest"), + aname: String::from(""), + n_uname: 1000, + }; + + let rattach = server.attach(&tattach).expect("failed to attach to server"); + check_qid(&rattach.qid, &md); + + (test_dir, server) +} + +#[test] +fn path_joins() { + let root = PathBuf::from("/a/b/c"); + let path = PathBuf::from("/a/b/c/d/e/f"); + + assert_eq!( + &join_path(path.clone(), "nested", &root).expect("normal"), + Path::new("/a/b/c/d/e/f/nested") + ); + + let p1 = join_path(path, "..", &root).expect("parent 1"); + assert_eq!(&p1, Path::new("/a/b/c/d/e/")); + + let p2 = join_path(p1, "..", &root).expect("parent 2"); + assert_eq!(&p2, Path::new("/a/b/c/d/")); + + let p3 = join_path(p2, "..", &root).expect("parent 3"); + assert_eq!(&p3, Path::new("/a/b/c/")); + + let p4 = join_path(p3, "..", &root).expect("parent of root"); + assert_eq!(&p4, Path::new("/a/b/c/")); +} + +#[test] +fn invalid_joins() { + let root = PathBuf::from("/a"); + let path = PathBuf::from("/a/b"); + + join_path(path.clone(), ".", &root).expect_err("current directory"); + join_path(path.clone(), "c/d/e", &root).expect_err("too many components"); + join_path(path, "/c/d/e", &root).expect_err("absolute path"); +} + +#[test] +fn clunk() { + let (_test_dir, mut server) = setup("clunk"); + + let tclunk = Tclunk { fid: ROOT_FID }; + server.clunk(&tclunk).expect("failed to clunk root fid"); +} + +#[test] +fn get_attr() { + let (test_dir, mut server) = setup("get_attr"); + + let md = test_dir + .symlink_metadata() + .expect("failed to get metadata for test dir"); + + check_attr(&mut server, ROOT_FID, &md); +} + +#[test] +fn tree_walk() { + let (test_dir, mut server) = setup("readdir"); + + let mut next_fid = ROOT_FID + 1; + + let mut dirs = VecDeque::new(); + dirs.push_back(test_dir.to_path_buf()); + + while let Some(dir) = dirs.pop_front() { + let dfid = next_fid; + next_fid += 1; + + let wnames: Vec<String> = dir + .strip_prefix(&test_dir) + .expect("test directory is not prefix of subdir") + .components() + .map(|c| Path::new(&c).to_string_lossy().to_string()) + .collect(); + walk(&mut server, &*test_dir, ROOT_FID, dfid, wnames); + + let md = dir.symlink_metadata().expect("failed to get metadata"); + + check_attr(&mut server, dfid, &md); + + let fid = next_fid; + next_fid += 1; + open(&mut server, &dir, dfid, "", fid, P9_DIRECTORY).expect("Failed to open directory"); + for dirent in readdir(&mut server, fid) { + if dirent.name == "." || dirent.name == ".." { + continue; + } + + let entry_path = dir.join(&dirent.name); + assert!( + entry_path.exists(), + "directory entry \"{}\" does not exist", + entry_path.display() + ); + let md = fs::symlink_metadata(&entry_path).expect("failed to get metadata for entry"); + + let ty = if md.is_dir() { + dirs.push_back(dir.join(dirent.name)); + libc::DT_DIR + } else if md.is_file() { + libc::DT_REG + } else if md.file_type().is_symlink() { + libc::DT_LNK + } else { + panic!("unknown file type: {:?}", md.file_type()); + }; + + assert_eq!(dirent.ty, ty); + check_qid(&dirent.qid, &md); + } + + let tclunk = Tclunk { fid }; + server.clunk(&tclunk).expect("failed to clunk fid"); + } +} + +#[test] +fn create_existing_file() { + let (test_dir, mut server) = setup("create_existing"); + + let name = "existing"; + create_local_file(&test_dir, name); + + let fid = ROOT_FID + 1; + create( + &mut server, + &*test_dir, + ROOT_FID, + fid, + name, + P9_APPEND, + 0o644, + ) + .expect_err("successfully created existing file"); +} + +enum SetAttrKind { + File, + Directory, +} + +fn set_attr_test<F>(kind: SetAttrKind, set_fields: F) -> io::Result<fs::Metadata> +where + F: FnOnce(&mut Tsetattr), +{ + let (test_dir, mut server) = setup("set_attr"); + + let name = "existing"; + match kind { + SetAttrKind::File => { + create_local_file(&test_dir, name); + } + SetAttrKind::Directory => { + let tmkdir = Tmkdir { + dfid: ROOT_FID, + name: String::from(name), + mode: 0o755, + gid: 0, + }; + + let rmkdir = server.mkdir(tmkdir).expect("failed to create directory"); + let md = fs::symlink_metadata(test_dir.join(name)) + .expect("failed to get metadata for directory"); + + assert!(md.is_dir()); + check_qid(&rmkdir.qid, &md); + } + }; + + let fid = ROOT_FID + 1; + walk( + &mut server, + &*test_dir, + ROOT_FID, + fid, + vec![String::from(name)], + ); + + let mut tsetattr = Tsetattr { + fid, + valid: 0, + mode: 0, + uid: 0, + gid: 0, + size: 0, + atime_sec: 0, + atime_nsec: 0, + mtime_sec: 0, + mtime_nsec: 0, + }; + + set_fields(&mut tsetattr); + server.set_attr(&tsetattr)?; + + fs::symlink_metadata(test_dir.join(name)) +} + +#[test] +fn set_len() { + let len = 661; + let md = set_attr_test(SetAttrKind::File, |tsetattr| { + tsetattr.valid = P9_SETATTR_SIZE; + tsetattr.size = len; + }) + .expect("failed to run set length of file"); + + assert_eq!(md.size(), len); +} + +#[test] +fn set_file_mode() { + let mode = 0o640; + let md = set_attr_test(SetAttrKind::File, |tsetattr| { + tsetattr.valid = P9_SETATTR_MODE; + tsetattr.mode = mode; + }) + .expect("failed to set mode"); + + assert_eq!(md.mode() & 0o777, mode); +} + +#[test] +fn set_file_mtime() { + let (secs, nanos) = (1245247825, 524617); + let md = set_attr_test(SetAttrKind::File, |tsetattr| { + tsetattr.valid = P9_SETATTR_MTIME | P9_SETATTR_MTIME_SET; + tsetattr.mtime_sec = secs; + tsetattr.mtime_nsec = nanos; + }) + .expect("failed to set mtime"); + + assert_eq!(md.mtime() as u64, secs); + assert_eq!(md.mtime_nsec() as u64, nanos); +} + +#[test] +fn set_file_atime() { + let (secs, nanos) = (9247605, 4016); + let md = set_attr_test(SetAttrKind::File, |tsetattr| { + tsetattr.valid = P9_SETATTR_ATIME | P9_SETATTR_ATIME_SET; + tsetattr.atime_sec = secs; + tsetattr.atime_nsec = nanos; + }) + .expect("failed to set atime"); + + assert_eq!(md.atime() as u64, secs); + assert_eq!(md.atime_nsec() as u64, nanos); +} + +#[test] +fn set_dir_mode() { + let mode = 0o640; + let md = set_attr_test(SetAttrKind::Directory, |tsetattr| { + tsetattr.valid = P9_SETATTR_MODE; + tsetattr.mode = mode; + }) + .expect("failed to set mode"); + + assert_eq!(md.mode() & 0o777, mode); +} + +#[test] +fn set_dir_mtime() { + let (secs, nanos) = (1245247825, 524617); + let md = set_attr_test(SetAttrKind::Directory, |tsetattr| { + tsetattr.valid = P9_SETATTR_MTIME | P9_SETATTR_MTIME_SET; + tsetattr.mtime_sec = secs; + tsetattr.mtime_nsec = nanos; + }) + .expect("failed to set mtime"); + + assert_eq!(md.mtime() as u64, secs); + assert_eq!(md.mtime_nsec() as u64, nanos); +} + +#[test] +fn set_dir_atime() { + let (secs, nanos) = (9247605, 4016); + let md = set_attr_test(SetAttrKind::Directory, |tsetattr| { + tsetattr.valid = P9_SETATTR_ATIME | P9_SETATTR_ATIME_SET; + tsetattr.atime_sec = secs; + tsetattr.atime_nsec = nanos; + }) + .expect("failed to set atime"); + + assert_eq!(md.atime() as u64, secs); + assert_eq!(md.atime_nsec() as u64, nanos); +} + +#[test] +fn huge_directory() { + let (test_dir, mut server) = setup("huge_directory"); + + let name = "newdir"; + let newdir = test_dir.join(name); + fs::create_dir(&newdir).expect("failed to create directory"); + + let dfid = ROOT_FID + 1; + walk( + &mut server, + &*test_dir, + ROOT_FID, + dfid, + vec![String::from(name)], + ); + + // Create ~4K files in the directory and then attempt to read them all. + let mut filenames = HashSet::with_capacity(4096); + for i in 0..4096 { + let name = format!("file_{}", i); + create_local_file(&newdir, &name); + assert!(filenames.insert(name)); + } + + let fid = dfid + 1; + open(&mut server, &newdir, dfid, "", fid, P9_DIRECTORY).expect("Failed to open directory"); + for f in readdir(&mut server, fid) { + let path = newdir.join(&f.name); + + let md = fs::symlink_metadata(path).expect("failed to get metadata for path"); + check_qid(&f.qid, &md); + + if f.name == "." || f.name == ".." { + assert_eq!(f.ty, libc::DT_DIR); + } else { + assert_eq!(f.ty, libc::DT_REG); + assert!(filenames.remove(&f.name)); + } + } + + assert!(filenames.is_empty()); +} + +#[test] +fn mkdir() { + let (test_dir, mut server) = setup("mkdir"); + + let name = "conan"; + let tmkdir = Tmkdir { + dfid: ROOT_FID, + name: String::from(name), + mode: 0o755, + gid: 0, + }; + + let rmkdir = server.mkdir(tmkdir).expect("failed to create directory"); + let md = + fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for directory"); + + assert!(md.is_dir()); + check_qid(&rmkdir.qid, &md); +} + +#[test] +fn unlink_all() { + let (test_dir, mut server) = setup("readdir"); + + let mut next_fid = ROOT_FID + 1; + + let mut dirs = VecDeque::new(); + dirs.push_back((ROOT_FID, test_dir.to_path_buf())); + + // First iterate over the whole directory. + let mut unlinks = VecDeque::new(); + while let Some((dfid, dir)) = dirs.pop_front() { + let mut names = VecDeque::new(); + for entry in fs::read_dir(dir).expect("failed to read directory") { + let entry = entry.expect("unable to iterate over directory"); + let ft = entry + .file_type() + .expect("failed to get file type for entry"); + if ft.is_dir() { + let fid = next_fid; + next_fid += 1; + + let wnames: Vec<String> = entry + .path() + .strip_prefix(&test_dir) + .expect("test directory is not prefix of subdir") + .components() + .map(|c| Path::new(&c).to_string_lossy().to_string()) + .collect(); + walk(&mut server, &*test_dir, ROOT_FID, fid, wnames); + dirs.push_back((fid, entry.path())); + } + + names.push_back(( + entry + .file_name() + .into_string() + .expect("failed to convert entry name to string"), + if ft.is_dir() { + libc::AT_REMOVEDIR as u32 + } else { + 0 + }, + )); + } + + unlinks.push_back((dfid, names)); + } + + // Now remove everything in reverse order. + while let Some((dfid, names)) = unlinks.pop_back() { + for (name, flags) in names { + let tunlinkat = Tunlinkat { + dirfd: dfid, + name, + flags, + }; + + server.unlink_at(tunlinkat).expect("failed to unlink path"); + } + } +} + +#[test] +fn rename_at() { + let (test_dir, mut server) = setup("rename"); + + let name = "oldfile"; + let content = create_local_file(&test_dir, name); + + let newname = "newfile"; + let trename = Trenameat { + olddirfid: ROOT_FID, + oldname: String::from(name), + newdirfid: ROOT_FID, + newname: String::from(newname), + }; + + server.rename_at(trename).expect("failed to rename file"); + + assert!(!test_dir.join(name).exists()); + + let mut newcontent = Vec::with_capacity(content.len()); + let size = File::open(test_dir.join(newname)) + .expect("failed to open file") + .read_to_end(&mut newcontent) + .expect("failed to read new file content"); + assert_eq!(size, content.len()); + assert_eq!(newcontent, content); +} + +fn setlk_tlock(fid: u32, len: u64, start: u64, type_: i32) -> Tlock { + Tlock { + fid, + type_: type_ as u8, + flags: 0, + start, + length: len, + proc_id: SERVER_PID, + client_id: String::from("test-server"), + } +} + +fn getlk_tgetlock(fid: u32, type_: i32) -> Tgetlock { + Tgetlock { + fid, + type_: type_ as u8, + start: 0, + length: 0, + proc_id: SERVER_PID, + client_id: String::from("test-server"), + } +} + +fn setup_simple_lock_no_open() -> Server { + let (test_dir, server) = setup("simple lock"); + + let filename = "file"; + create_local_file(&test_dir, filename); + + server +} + +fn setup_simple_lock(flags: u32) -> Server { + let (test_dir, mut server) = setup("simple lock"); + + let filename = "file"; + create_local_file(&test_dir, filename); + + open( + &mut server, + &*test_dir, + ROOT_FID, + filename, + ROOT_FID + 1, + flags, + ) + .expect("failed to open file"); + + server +} + +#[test] +fn lock_rdlck_no_open_file() { + let mut server = setup_simple_lock_no_open(); + + let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_RDLCK); + + server.lock(&tlock).expect_err("Bad file descriptor"); +} + +#[test] +fn lock_rdlck() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_RDLCK); + + server.lock(&tlock).expect("failed to lock file"); +} +#[test] +fn lock_wrlck_no_open_file() { + let mut server = setup_simple_lock_no_open(); + + let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_WRLCK); + + server.lock(&tlock).expect_err("Bad file descriptor"); +} +#[test] +fn lock_wrlck() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, 8, 0, libc::F_WRLCK); + + server.lock(&tlock).expect("failed to lock file"); +} + +#[test] +fn lock_unlck_no_lock() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK); + + server.lock(&tlock).expect("failed to lock file"); +} + +#[test] +fn lock_unlck() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK); + + server.lock(&tlock).expect("failed to lock file"); + + let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK); + + server.lock(&tlock).expect("failed to lock file"); +} + +#[test] +fn lock_unlck_relock() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK); + + server.lock(&tlock).expect("failed to lock file"); + + let tlock = setlk_tlock(ROOT_FID + 1, 0, 0, libc::F_UNLCK); + + server.lock(&tlock).expect("failed to lock file"); + + let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK); + + server.lock(&tlock).expect("failed to lock file"); +} + +#[test] +fn getlock_rdlck_nolock() { + let mut server = setup_simple_lock(P9_RDWR); + + let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_RDLCK); + + server + .get_lock(&tgetlock) + .expect("failed to get lock on file"); +} + +#[test] +fn getlock_wrlck() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_WRLCK); + + server.lock(&tlock).expect("failed to lock file"); + + let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_WRLCK); + + server + .get_lock(&tgetlock) + .expect("failed to get lock on file"); +} + +#[test] +fn getlock_rdlck() { + let mut server = setup_simple_lock(P9_RDWR); + + let tlock = setlk_tlock(ROOT_FID + 1, LOCAL_FILE_LEN / 2, 0, libc::F_RDLCK); + + server.lock(&tlock).expect("failed to lock file"); + + let tgetlock = getlk_tgetlock(ROOT_FID + 1, libc::F_RDLCK); + + server + .get_lock(&tgetlock) + .expect("failed to get lock on file"); +} + +macro_rules! open_test { + ($name:ident, $flags:expr) => { + #[test] + fn $name() { + let (test_dir, mut server) = setup("open"); + + let fid = ROOT_FID + 1; + let name = "test.txt"; + let content = create_local_file(&test_dir, name); + + let rlopen = open(&mut server, &*test_dir, ROOT_FID, name, fid, $flags as u32) + .expect("failed to open file"); + + let md = + fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for file"); + check_qid(&rlopen.qid, &md); + assert_eq!(rlopen.iounit, 0); + + check_attr(&mut server, fid, &md); + + // Check that the file has the proper contents as long as we didn't + // truncate it first. + if $flags & P9_TRUNC == 0 && $flags & P9_WRONLY == 0 { + check_content(&mut server, &content, fid); + } + + // Check that we can write to the file. + if $flags & P9_RDWR != 0 || $flags & P9_WRONLY != 0 { + write(&mut server, &test_dir, name, fid, $flags); + } + + let tclunk = Tclunk { fid }; + server.clunk(&tclunk).expect("Unable to clunk file"); + } + }; + ($name:ident, $flags:expr, $expected_err:expr) => { + #[test] + fn $name() { + let (test_dir, mut server) = setup("open_fail"); + + let fid = ROOT_FID + 1; + let name = "test.txt"; + create_local_file(&test_dir, name); + + let err = open(&mut server, &*test_dir, ROOT_FID, name, fid, $flags as u32) + .expect_err("successfully opened file"); + assert_eq!(err.kind(), $expected_err); + + let tclunk = Tclunk { fid }; + server.clunk(&tclunk).expect("Unable to clunk file"); + } + }; +} + +open_test!(read_only_file_open, P9_RDONLY); +open_test!(read_write_file_open, P9_RDWR); +open_test!(write_only_file_open, P9_WRONLY); + +open_test!(create_read_only_file_open, P9_CREATE | P9_RDONLY); +open_test!(create_read_write_file_open, P9_CREATE | P9_RDWR); +open_test!(create_write_only_file_open, P9_CREATE | P9_WRONLY); + +open_test!(append_read_only_file_open, P9_APPEND | P9_RDONLY); +open_test!(append_read_write_file_open, P9_APPEND | P9_RDWR); +open_test!(append_write_only_file_open, P9_APPEND | P9_WRONLY); + +open_test!(trunc_read_only_file_open, P9_TRUNC | P9_RDONLY); +open_test!(trunc_read_write_file_open, P9_TRUNC | P9_RDWR); +open_test!(trunc_write_only_file_open, P9_TRUNC | P9_WRONLY); + +open_test!( + create_append_read_only_file_open, + P9_CREATE | P9_APPEND | P9_RDONLY +); +open_test!( + create_append_read_write_file_open, + P9_CREATE | P9_APPEND | P9_RDWR +); +open_test!( + create_append_wronly_file_open, + P9_CREATE | P9_APPEND | P9_WRONLY +); + +open_test!( + create_trunc_read_only_file_open, + P9_CREATE | P9_TRUNC | P9_RDONLY +); +open_test!( + create_trunc_read_write_file_open, + P9_CREATE | P9_TRUNC | P9_RDWR +); +open_test!( + create_trunc_wronly_file_open, + P9_CREATE | P9_TRUNC | P9_WRONLY +); + +open_test!( + append_trunc_read_only_file_open, + P9_APPEND | P9_TRUNC | P9_RDONLY +); +open_test!( + append_trunc_read_write_file_open, + P9_APPEND | P9_TRUNC | P9_RDWR +); +open_test!( + append_trunc_wronly_file_open, + P9_APPEND | P9_TRUNC | P9_WRONLY +); + +open_test!( + create_append_trunc_read_only_file_open, + P9_CREATE | P9_APPEND | P9_TRUNC | P9_RDONLY +); +open_test!( + create_append_trunc_read_write_file_open, + P9_CREATE | P9_APPEND | P9_TRUNC | P9_RDWR +); +open_test!( + create_append_trunc_wronly_file_open, + P9_CREATE | P9_APPEND | P9_TRUNC | P9_WRONLY +); + +open_test!( + create_excl_read_only_file_open, + P9_CREATE | P9_EXCL | P9_RDONLY, + io::ErrorKind::AlreadyExists +); +open_test!( + create_excl_read_write_file_open, + P9_CREATE | P9_EXCL | P9_RDWR, + io::ErrorKind::AlreadyExists +); +open_test!( + create_excl_wronly_file_open, + P9_CREATE | P9_EXCL | P9_WRONLY, + io::ErrorKind::AlreadyExists +); + +macro_rules! create_test { + ($name:ident, $flags:expr, $mode:expr) => { + #[test] + fn $name() { + let (test_dir, mut server) = setup("create"); + + let name = "foo.txt"; + let fid = ROOT_FID + 1; + let rlcreate = create(&mut server, &*test_dir, ROOT_FID, fid, name, $flags, $mode) + .expect("failed to create file"); + + let md = + fs::symlink_metadata(test_dir.join(name)).expect("failed to get metadata for file"); + assert_eq!(rlcreate.iounit, 0); + check_qid(&rlcreate.qid, &md); + check_attr(&mut server, fid, &md); + + // Check that we can write to the file. + if $flags & P9_RDWR != 0 || $flags & P9_WRONLY != 0 { + write(&mut server, &test_dir, name, fid, $flags); + } + + let tclunk = Tclunk { fid }; + server.clunk(&tclunk).expect("Unable to clunk file"); + } + }; + ($name:ident, $flags:expr, $mode:expr, $expected_err:expr) => { + #[test] + fn $name() { + let (test_dir, mut server) = setup("create_fail"); + + let name = "foo.txt"; + // The `fid` in the lcreate call initially points to the directory + // but is supposed to point to the newly created file after the call + // completes. Duplicate the fid so that we don't end up consuming the + // root fid. + let fid = ROOT_FID + 1; + let err = create(&mut server, &*test_dir, ROOT_FID, fid, name, $flags, $mode) + .expect_err("successfully created file"); + assert_eq!(err.kind(), $expected_err); + } + }; +} + +create_test!(read_only_file_create, P9_RDONLY, 0o600u32); +create_test!(read_write_file_create, P9_RDWR, 0o600u32); +create_test!(write_only_file_create, P9_WRONLY, 0o600u32); + +create_test!( + append_read_only_file_create, + P9_APPEND | P9_RDONLY, + 0o600u32 +); +create_test!(append_read_write_file_create, P9_APPEND | P9_RDWR, 0o600u32); +create_test!(append_wronly_file_create, P9_APPEND | P9_WRONLY, 0o600u32); + +#[test] +fn lcreate_set_len() { + let (test_dir, mut server) = setup("lcreate_set_len"); + + let name = "foo.txt"; + let fid = ROOT_FID + 1; + create( + &mut server, + &*test_dir, + ROOT_FID, + fid, + name, + P9_RDWR, + 0o600u32, + ) + .expect("failed to create file"); + + let tsetattr = Tsetattr { + fid, + valid: 0x8, // P9_SETATTR_SIZE + size: 100, + // The other fields are not used because the relevant flags aren't set in `valid`. + mode: 0, + uid: 0, + gid: 0, + atime_sec: 0, + atime_nsec: 0, + mtime_sec: 0, + mtime_nsec: 0, + }; + server + .set_attr(&tsetattr) + .expect("failed to set file length after lcreate"); + + let tclunk = Tclunk { fid }; + server.clunk(&tclunk).expect("Unable to clunk file"); +} + +#[test] +fn readlink() { + let (test_dir, mut server) = setup("readlink"); + create_local_symlink(&test_dir, "symlink", "target/of/symlink"); + + let fid = ROOT_FID + 1; + walk( + &mut server, + &*test_dir, + ROOT_FID, + fid, + vec!["symlink".into()], + ); + + let treadlink = Treadlink { fid }; + + let rreadlink = server.readlink(&treadlink).expect("failed to readlink"); + + assert_eq!(rreadlink.target, "target/of/symlink"); +} |