Merge branch 'unlinkat' into 'master'
Introduce SchemeRoot and integrate unlink and rmdir into unlinkat. See merge request redox-os/redoxfs!104
This commit is contained in:
Generated
+7
-7
@@ -347,9 +347,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -494,9 +494,9 @@ checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717"
|
||||
|
||||
[[package]]
|
||||
name = "redox-scheme"
|
||||
version = "0.8.3"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cea2668a5932f878a4298a1d7f8950249bbbb77120fb263da252c589152f5ea"
|
||||
checksum = "5eb0cee524a5c6e4db180250f699e101ee25f3f79ad2618527e539e028cb6722"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"redox_syscall",
|
||||
@@ -504,9 +504,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
|
||||
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -519,7 +519,7 @@ checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
|
||||
|
||||
[[package]]
|
||||
name = "redoxfs"
|
||||
version = "0.8.6"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"argon2",
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
name = "redoxfs"
|
||||
description = "The Redox Filesystem"
|
||||
repository = "https://gitlab.redox-os.org/redox-os/redoxfs"
|
||||
version = "0.8.6"
|
||||
version = "0.9.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
authors = ["Jeremy Soller <jackpot51@gmail.com>"]
|
||||
@@ -56,7 +56,7 @@ log = { version = "0.4.14", default-features = false, optional = true }
|
||||
lz4_flex = { version = "0.11", default-features = false, features = ["checked-decode"] }
|
||||
parse-size = { version = "1", optional = true }
|
||||
range-tree = { version = "0.1", optional = true }
|
||||
redox_syscall = "0.6.0"
|
||||
redox_syscall = "0.7.0"
|
||||
seahash = { version = "4.1.0", default-features = false }
|
||||
termion = { version = "4", optional = true }
|
||||
uuid = { version = "1.4", default-features = false }
|
||||
@@ -85,9 +85,9 @@ std = [
|
||||
fuser = { version = "0.16", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
libredox = { version = "0.1.11", features = ["call"], optional = true }
|
||||
libredox = { version = "0.1.12", features = ["call"], optional = true }
|
||||
redox-path = "0.3.0"
|
||||
redox-scheme = { version = "0.8.3", optional = true }
|
||||
redox-scheme = { version = "0.9.0", optional = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
+11
-3
@@ -20,13 +20,21 @@ where
|
||||
F: FnOnce(&Path) -> T,
|
||||
{
|
||||
let mountpoint = mountpoint.as_ref();
|
||||
let scheme_name = format!("{}", mountpoint.display());
|
||||
let socket = Socket::create(&scheme_name)?;
|
||||
let socket = Socket::create()?;
|
||||
|
||||
let scheme_name = format!("{}", mountpoint.display());
|
||||
let mounted_path = format!("/scheme/{}", mountpoint.display());
|
||||
|
||||
let mut scheme = FileScheme::new(scheme_name, mounted_path.clone(), filesystem, &socket);
|
||||
|
||||
redox_scheme::scheme::register_sync_scheme(
|
||||
&socket,
|
||||
&format!("{}", mountpoint.display()),
|
||||
&mut scheme,
|
||||
)?;
|
||||
|
||||
let res = callback(Path::new(&mounted_path));
|
||||
|
||||
let mut scheme = FileScheme::new(scheme_name, mounted_path, filesystem, &socket);
|
||||
while IS_UMT.load(Ordering::SeqCst) == 0 {
|
||||
let req = match socket.next_request(SignalBehavior::Restart)? {
|
||||
None => break,
|
||||
|
||||
+71
-40
@@ -28,13 +28,18 @@ use crate::{Disk, FileSystem, Node, Transaction, TreeData, TreePtr, BLOCK_SIZE};
|
||||
|
||||
use super::resource::{DirResource, Entry, FileMmapInfo, FileResource, Resource};
|
||||
|
||||
enum Handle<D: Disk> {
|
||||
Resource(Box<dyn Resource<D>>),
|
||||
SchemeRoot,
|
||||
}
|
||||
|
||||
pub struct FileScheme<'sock, D: Disk> {
|
||||
scheme_name: String,
|
||||
mounted_path: String,
|
||||
pub(crate) fs: FileSystem<D>,
|
||||
socket: &'sock Socket,
|
||||
next_id: AtomicUsize,
|
||||
files: BTreeMap<usize, Box<dyn Resource<D>>>,
|
||||
handles: BTreeMap<usize, Handle<D>>,
|
||||
fmap: super::resource::Fmaps,
|
||||
|
||||
// Map of file id to other scheme's file descriptor.
|
||||
@@ -54,7 +59,7 @@ impl<'sock, D: Disk> FileScheme<'sock, D> {
|
||||
fs,
|
||||
socket,
|
||||
next_id: AtomicUsize::new(1),
|
||||
files: BTreeMap::new(),
|
||||
handles: BTreeMap::new(),
|
||||
fmap: BTreeMap::new(),
|
||||
other_scheme_fd_map: BTreeMap::new(),
|
||||
}
|
||||
@@ -126,7 +131,9 @@ impl<'sock, D: Disk> FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, id: usize, payload: &mut [u8]) -> Result<usize> {
|
||||
let resource = self.files.get(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(resource)) = self.handles.get(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
let inode_id = resource.node_ptr().id();
|
||||
let target_fd = self
|
||||
.other_scheme_fd_map
|
||||
@@ -136,6 +143,10 @@ impl<'sock, D: Disk> FileScheme<'sock, D> {
|
||||
return Ok(len);
|
||||
}
|
||||
|
||||
fn open(&mut self, url: &str, flags: usize, ctx: &CallerCtx) -> Result<OpenResult> {
|
||||
self.open_internal(TreePtr::root(), url, flags, ctx)
|
||||
}
|
||||
|
||||
fn open_internal(
|
||||
&mut self,
|
||||
start_ptr: TreePtr<Node>,
|
||||
@@ -347,7 +358,7 @@ impl<'sock, D: Disk> FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.files.insert(id, resource);
|
||||
self.handles.insert(id, Handle::Resource(resource));
|
||||
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: id,
|
||||
@@ -418,8 +429,10 @@ fn dirname(path: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
fn open(&mut self, url: &str, flags: usize, ctx: &CallerCtx) -> Result<OpenResult> {
|
||||
self.open_internal(TreePtr::root(), url, flags, ctx)
|
||||
fn scheme_root(&mut self) -> Result<usize> {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.handles.insert(id, Handle::SchemeRoot);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn openat(
|
||||
@@ -430,13 +443,14 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
_fcntl_flags: u32,
|
||||
ctx: &CallerCtx,
|
||||
) -> Result<OpenResult> {
|
||||
// If pathname is absolute, then dirfd is ignored.
|
||||
if path.starts_with('/') {
|
||||
return self.open_internal(TreePtr::root(), path, flags, ctx);
|
||||
}
|
||||
let dir_resource = self.files.get(&dirfd).ok_or(Error::new(EBADF))?;
|
||||
// Only allow DirResource as base for openat
|
||||
let dir_node_ptr = dir_resource.node_ptr();
|
||||
let dir_node_ptr = match self.handles.get(&dirfd).ok_or(Error::new(EBADF))? {
|
||||
// If pathname is absolute, then dirfd is ignored.
|
||||
Handle::Resource(dir_resource) if !path.starts_with('/') => {
|
||||
// only allow dirresource as base for openat
|
||||
dir_resource.node_ptr()
|
||||
}
|
||||
_ => TreePtr::root(),
|
||||
};
|
||||
self.open_internal(dir_node_ptr, path, flags, ctx)
|
||||
}
|
||||
|
||||
@@ -445,8 +459,12 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
let uid = ctx.uid;
|
||||
let gid = ctx.gid;
|
||||
|
||||
let start_ptr = self.files.get(&dirfd).ok_or(Error::new(EBADF))?.node_ptr();
|
||||
// println!("Unlinkat '{}' flags: {:X}", path, flags);+
|
||||
let start_ptr = match self.handles.get(&dirfd).ok_or(Error::new(EBADF))? {
|
||||
Handle::Resource(dir_resource) => dir_resource.node_ptr(),
|
||||
Handle::SchemeRoot => TreePtr::root(),
|
||||
};
|
||||
|
||||
// println!("Unlinkat '{}' flags: {:X}", path, flags);
|
||||
|
||||
let scheme_name = &self.scheme_name;
|
||||
|
||||
@@ -505,9 +523,11 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
let Some(node_id) = unlink_result? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(fd) = self.other_scheme_fd_map.remove(&node_id) {
|
||||
let _ = syscall::close(fd);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -521,7 +541,9 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
// println!("Read {}, {:X} {}", id, buf.as_ptr() as usize, buf.len());
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
self.fs.tx(|tx| file.read(buf, offset, tx))
|
||||
}
|
||||
|
||||
@@ -534,18 +556,22 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
// println!("Write {}, {:X} {}", id, buf.as_ptr() as usize, buf.len());
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
self.fs.tx(|tx| file.write(buf, offset, tx))
|
||||
}
|
||||
|
||||
fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result<u64> {
|
||||
// println!("Seek {}, {} {}", id, pos, whence);
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
self.fs.tx(|tx| file.fsize(tx))
|
||||
}
|
||||
|
||||
fn fchmod(&mut self, id: usize, mode: u16, _ctx: &CallerCtx) -> Result<()> {
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
self.fs.tx(|tx| file.fchmod(mode, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -553,7 +579,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
fn fchown(&mut self, id: usize, new_uid: u32, new_gid: u32, _ctx: &CallerCtx) -> Result<()> {
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
self.fs.tx(|tx| file.fchown(new_uid, new_gid, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -561,7 +587,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, id: usize, cmd: usize, arg: usize, _ctx: &CallerCtx) -> Result<usize> {
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
file.fcntl(cmd, arg)
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -569,8 +595,8 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
|
||||
if let Some(_file) = self.files.get(&id) {
|
||||
// EPERM is returned for files that are always readable or writable
|
||||
if let Some(Handle::Resource(_file)) = self.handles.get(&id) {
|
||||
// EPERM is returned for handles that are always readable or writable
|
||||
Err(Error::new(EPERM))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -579,7 +605,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
// println!("Fpath {}, {:X} {}", id, buf.as_ptr() as usize, buf.len());
|
||||
if let Some(file) = self.files.get(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get(&id) {
|
||||
let mounted_path = self.mounted_path.as_bytes();
|
||||
|
||||
let mut i = 0;
|
||||
@@ -614,7 +640,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
// println!("Flink {}, {} from {}, {}", id, new_path, uid, gid);
|
||||
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
//TODO: Check for EINVAL
|
||||
// The new pathname contained a path prefix of the old, or, more generally,
|
||||
// an attempt was made to make a directory a subdirectory of itself.
|
||||
@@ -721,7 +747,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
// println!("Frename {}, {} from {}, {}", id, new_path, uid, gid);
|
||||
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
//TODO: Check for EINVAL
|
||||
// The new pathname contained a path prefix of the old, or, more generally,
|
||||
// an attempt was made to make a directory a subdirectory of itself.
|
||||
@@ -822,7 +848,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> {
|
||||
// println!("Fstat {}, {:X}", id, stat as *mut Stat as usize);
|
||||
if let Some(file) = self.files.get(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get(&id) {
|
||||
self.fs.tx(|tx| file.stat(stat, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -830,7 +856,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
fn fstatvfs(&mut self, id: usize, stat: &mut StatVfs, _ctx: &CallerCtx) -> Result<()> {
|
||||
if let Some(_file) = self.files.get(&id) {
|
||||
if let Some(Handle::Resource(_file)) = self.handles.get(&id) {
|
||||
stat.f_bsize = BLOCK_SIZE as u32;
|
||||
stat.f_blocks = self.fs.header.size() / (stat.f_bsize as u64);
|
||||
stat.f_bfree = self.fs.allocator().free();
|
||||
@@ -844,7 +870,9 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
|
||||
// println!("Fsync {}", id);
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
let fmaps = &mut self.fmap;
|
||||
|
||||
self.fs.tx(|tx| file.sync(fmaps, tx))
|
||||
@@ -852,7 +880,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn ftruncate(&mut self, id: usize, len: u64, _ctx: &CallerCtx) -> Result<()> {
|
||||
// println!("Ftruncate {}, {}", id, len);
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
self.fs.tx(|tx| file.truncate(len, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -861,7 +889,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn futimens(&mut self, id: usize, times: &[TimeSpec], _ctx: &CallerCtx) -> Result<()> {
|
||||
// println!("Futimens {}, {}", id, times.len());
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
self.fs.tx(|tx| file.utimens(times, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -874,7 +902,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
buf: DirentBuf<&'buf mut [u8]>,
|
||||
opaque_offset: u64,
|
||||
) -> Result<DirentBuf<&'buf mut [u8]>> {
|
||||
if let Some(file) = self.files.get_mut(&id) {
|
||||
if let Some(Handle::Resource(file)) = self.handles.get_mut(&id) {
|
||||
self.fs.tx(|tx| file.getdents(buf, opaque_offset, tx))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
@@ -889,7 +917,9 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
flags: MapFlags,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
let fmaps = &mut self.fmap;
|
||||
|
||||
self.fs.tx(|tx| file.fmap(fmaps, flags, size, offset, tx))
|
||||
@@ -903,7 +933,9 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
flags: MunmapFlags,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<()> {
|
||||
let file = self.files.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(file)) = self.handles.get_mut(&id) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
let fmaps = &mut self.fmap;
|
||||
|
||||
self.fs.tx(|tx| file.funmap(fmaps, offset, size, tx))
|
||||
@@ -911,7 +943,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
// println!("Close {}", id);
|
||||
let Some(file) = self.files.remove(&id) else {
|
||||
let Some(Handle::Resource(file)) = self.handles.remove(&id) else {
|
||||
return;
|
||||
};
|
||||
let node_ptr = file.node_ptr();
|
||||
@@ -943,10 +975,9 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
let uid = ctx.uid;
|
||||
let gid = ctx.gid;
|
||||
|
||||
let parent_resource = self
|
||||
.files
|
||||
.get(&sendfd_request.id())
|
||||
.ok_or(Error::new(EBADF))?;
|
||||
let Some(Handle::Resource(parent_resource)) = self.handles.get(&sendfd_request.id()) else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
|
||||
let mut new_fd = usize::MAX;
|
||||
if let Err(e) = sendfd_request.obtain_fd(
|
||||
@@ -1042,7 +1073,7 @@ impl<'sock, D: Disk> SchemeSync for FileScheme<'sock, D> {
|
||||
}
|
||||
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.files.insert(id, resource);
|
||||
self.handles.insert(id, Handle::Resource(resource));
|
||||
self.other_scheme_fd_map.insert(node_id, new_fd);
|
||||
Ok(new_fd)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user