nfs4: Add Request class for better handling of NFS4 requests
This commit is contained in:
@@ -11,8 +11,7 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "ReplyInterpreter.h"
|
||||
#include "RequestBuilder.h"
|
||||
#include "Request.h"
|
||||
|
||||
#include "Inode.h"
|
||||
|
||||
@@ -33,7 +32,9 @@ status_t
|
||||
Filesystem::Mount(Filesystem** pfs, RPC::Server* serv, const char* fsPath,
|
||||
dev_t id)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(serv);
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutRootFH();
|
||||
|
||||
// Better way of doing this will be needed
|
||||
@@ -62,11 +63,11 @@ Filesystem::Mount(Filesystem** pfs, RPC::Server* serv, const char* fsPath,
|
||||
Attribute attr[] = { FATTR4_FH_EXPIRE_TYPE };
|
||||
req.GetAttr(attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
serv->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
status_t result;
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
result = reply.PutRootFH();
|
||||
if (result != B_OK)
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
#include <dirent.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ReplyInterpreter.h"
|
||||
#include "RequestBuilder.h"
|
||||
#include "Request.h"
|
||||
|
||||
|
||||
// Creating Inode object from Filehandle probably is not a good idea when
|
||||
@@ -25,17 +24,20 @@ Inode::Inode(Filesystem* fs, const FileInfo &fi)
|
||||
fParentFH(fi.fParent),
|
||||
fName(strdup(fi.fName))
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
|
||||
Attribute attr[] = { FATTR4_TYPE, FATTR4_FILEID };
|
||||
req.GetAttr(attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fs->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return;
|
||||
@@ -79,7 +81,9 @@ Inode::LookUp(const char* name, ino_t* id)
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
|
||||
if (!strcmp(name, ".."))
|
||||
@@ -92,11 +96,13 @@ Inode::LookUp(const char* name, ino_t* id)
|
||||
Attribute attr[] = { FATTR4_FILEID };
|
||||
req.GetAttr(attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
status_t result = reply.PutFH();
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
@@ -135,15 +141,18 @@ Inode::LookUp(const char* name, ino_t* id)
|
||||
status_t
|
||||
Inode::Access(int mode)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
req.Access();
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -176,17 +185,20 @@ Inode::Access(int mode)
|
||||
status_t
|
||||
Inode::Stat(struct stat* st)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
|
||||
Attribute attr[] = { FATTR4_SIZE, FATTR4_MODE, FATTR4_NUMLINKS };
|
||||
req.GetAttr(attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -231,34 +243,41 @@ Inode::Open(int mode, OpenFileCookie* cookie)
|
||||
{
|
||||
cookie->fSeq = 0;
|
||||
|
||||
RequestBuilder req_setid(ProcCompound);
|
||||
req_setid.SetClientID(fFilesystem->Server());
|
||||
Request request(fFilesystem->Server());
|
||||
request.Builder().SetClientID(fFilesystem->Server());
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req_setid.Request(), &rpl);
|
||||
ReplyInterpreter setid(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
uint64 id, ver;
|
||||
status_t result = setid.SetClientID(&id, &ver);
|
||||
result = request.Reply().SetClientID(&id, &ver);
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
RequestBuilder req_conf(ProcCompound);
|
||||
req_conf.SetClientIDConfirm(id, ver);
|
||||
request.Reset();
|
||||
request.Builder().SetClientIDConfirm(id, ver);
|
||||
|
||||
fFilesystem->Server()->SendCall(req_conf.Request(), &rpl);
|
||||
ReplyInterpreter conf(rpl);
|
||||
|
||||
result = conf.SetClientIDConfirm();
|
||||
result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
RequestBuilder req(ProcCompound);
|
||||
result = request.Reply().SetClientIDConfirm();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
request.Reset();
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fParentFH);
|
||||
req.Open(cookie->fSeq, OPEN4_SHARE_ACCESS_READ, id, OPEN4_NOCREATE, fName);
|
||||
cookie->fSeq++;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
|
||||
result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
@@ -270,12 +289,14 @@ Inode::Open(int mode, OpenFileCookie* cookie)
|
||||
return result;
|
||||
|
||||
if (confirm) {
|
||||
RequestBuilder req(ProcCompound);
|
||||
request.Reset();
|
||||
req.PutFH(fHandle);
|
||||
req.OpenConfirm(cookie->fSeq, cookie->fStateId, cookie->fStateSeq);
|
||||
cookie->fSeq++;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
|
||||
result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
@@ -293,15 +314,18 @@ Inode::Open(int mode, OpenFileCookie* cookie)
|
||||
status_t
|
||||
Inode::Close(OpenFileCookie* cookie)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
req.Close(cookie->fSeq, cookie->fStateId, cookie->fStateSeq);
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -322,16 +346,19 @@ Inode::Read(OpenFileCookie* cookie, off_t pos, void* buffer, size_t* _length)
|
||||
uint32 len = 0;
|
||||
|
||||
while (size < *_length && !eof) {
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
req.Read(cookie->fStateId, cookie->fStateSeq, pos + size,
|
||||
*_length - size);
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -355,15 +382,18 @@ Inode::OpenDir(uint64* cookie)
|
||||
if (fType != NF4DIR)
|
||||
return B_NOT_A_DIRECTORY;
|
||||
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
req.Access();
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -387,17 +417,20 @@ status_t
|
||||
Inode::_ReadDirOnce(DirEntry** dirents, uint32* count, uint64* cookie,
|
||||
bool* eof)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
|
||||
Attribute attr[] = { FATTR4_FILEID };
|
||||
req.ReadDir(*count, cookie, attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
@@ -431,18 +464,21 @@ Inode::_FillDirEntry(struct dirent* de, ino_t id, const char* name, uint32 pos,
|
||||
status_t
|
||||
Inode::_ReadDirUp(struct dirent* de, uint32 pos, uint32 size)
|
||||
{
|
||||
RequestBuilder req(ProcCompound);
|
||||
Request request(fFilesystem->Server());
|
||||
RequestBuilder& req = request.Builder();
|
||||
|
||||
req.PutFH(fHandle);
|
||||
req.LookUpUp();
|
||||
req.GetFH();
|
||||
Attribute attr[] = { FATTR4_FILEID };
|
||||
req.GetAttr(attr, sizeof(attr) / sizeof(Attribute));
|
||||
|
||||
RPC::Reply *rpl;
|
||||
fFilesystem->Server()->SendCall(req.Request(), &rpl);
|
||||
ReplyInterpreter reply(rpl);
|
||||
status_t result = request.Send();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
ReplyInterpreter& reply = request.Reply();
|
||||
|
||||
status_t result;
|
||||
result = reply.PutFH();
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
@@ -8,6 +8,7 @@ KernelAddon nfs4 :
|
||||
Inode.cpp
|
||||
kernel_interface.cpp
|
||||
ReplyInterpreter.cpp
|
||||
Request.cpp
|
||||
RequestBuilder.cpp
|
||||
RPCAuth.cpp
|
||||
RPCCall.cpp
|
||||
|
||||
@@ -34,9 +34,8 @@ ReplyInterpreter::ReplyInterpreter(RPC::Reply* reply)
|
||||
:
|
||||
fReply(reply)
|
||||
{
|
||||
fReply->Stream().GetUInt();
|
||||
fReply->Stream().GetOpaque(NULL);
|
||||
fReply->Stream().GetUInt();
|
||||
if (reply != NULL)
|
||||
_ParseHeader();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +45,15 @@ ReplyInterpreter::~ReplyInterpreter()
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ReplyInterpreter::_ParseHeader()
|
||||
{
|
||||
fReply->Stream().GetUInt();
|
||||
fReply->Stream().GetOpaque(NULL);
|
||||
fReply->Stream().GetUInt();
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
ReplyInterpreter::Access(uint32* supported, uint32* allowed)
|
||||
{
|
||||
@@ -330,6 +338,9 @@ ReplyInterpreter::_DecodeAttrs(XDR::ReadStream& str, AttrValue** attrs,
|
||||
status_t
|
||||
ReplyInterpreter::_OperationError(Opcode op)
|
||||
{
|
||||
if (fReply == NULL)
|
||||
return B_NOT_INITIALIZED;
|
||||
|
||||
if (fReply->Error() != B_OK || fReply->Stream().IsEOF())
|
||||
return fReply->Error();
|
||||
|
||||
|
||||
@@ -35,9 +35,12 @@ struct DirEntry {
|
||||
|
||||
class ReplyInterpreter {
|
||||
public:
|
||||
ReplyInterpreter(RPC::Reply* reply);
|
||||
ReplyInterpreter(RPC::Reply* reply = NULL);
|
||||
~ReplyInterpreter();
|
||||
|
||||
inline status_t SetTo(RPC::Reply* reply);
|
||||
inline void Reset();
|
||||
|
||||
status_t Access(uint32* supported, uint32* allowed);
|
||||
status_t Close();
|
||||
status_t GetAttr(AttrValue** attrs, uint32* count);
|
||||
@@ -55,6 +58,8 @@ public:
|
||||
inline status_t SetClientIDConfirm();
|
||||
|
||||
private:
|
||||
void _ParseHeader();
|
||||
|
||||
status_t _DecodeAttrs(XDR::ReadStream& stream, AttrValue** attrs,
|
||||
uint32* count);
|
||||
status_t _OperationError(Opcode op);
|
||||
@@ -65,6 +70,29 @@ private:
|
||||
};
|
||||
|
||||
|
||||
inline status_t
|
||||
ReplyInterpreter::SetTo(RPC::Reply* _reply)
|
||||
{
|
||||
if (fReply != NULL)
|
||||
return B_DONT_DO_THAT;
|
||||
|
||||
fReply = _reply;
|
||||
|
||||
if (fReply != NULL)
|
||||
_ParseHeader();
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
|
||||
inline void
|
||||
ReplyInterpreter::Reset()
|
||||
{
|
||||
delete fReply;
|
||||
fReply = NULL;
|
||||
}
|
||||
|
||||
|
||||
inline status_t
|
||||
ReplyInterpreter::LookUp()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012 Haiku, Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Paweł Dziepak, [email protected]
|
||||
*/
|
||||
|
||||
|
||||
#include "Request.h"
|
||||
|
||||
|
||||
status_t
|
||||
Request::Send()
|
||||
{
|
||||
RPC::Reply *rpl;
|
||||
status_t result = fServer->SendCall(fBuilder.Request(), &rpl);
|
||||
if (result != B_OK)
|
||||
return result;
|
||||
|
||||
return fReply.SetTo(rpl);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Request::Reset()
|
||||
{
|
||||
fBuilder.Reset();
|
||||
fReply.Reset();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012 Haiku, Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Paweł Dziepak, [email protected]
|
||||
*/
|
||||
#ifndef REQUEST_H
|
||||
#define REQUEST_H
|
||||
|
||||
|
||||
#include "ReplyInterpreter.h"
|
||||
#include "RequestBuilder.h"
|
||||
#include "RPCServer.h"
|
||||
|
||||
|
||||
class Request {
|
||||
public:
|
||||
inline Request(RPC::Server* serv);
|
||||
|
||||
inline RequestBuilder& Builder();
|
||||
inline ReplyInterpreter& Reply();
|
||||
|
||||
status_t Send();
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
RPC::Server* fServer;
|
||||
|
||||
RequestBuilder fBuilder;
|
||||
ReplyInterpreter fReply;
|
||||
};
|
||||
|
||||
|
||||
inline
|
||||
Request::Request(RPC::Server* serv)
|
||||
:
|
||||
fServer(serv)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
inline RequestBuilder&
|
||||
Request::Builder()
|
||||
{
|
||||
return fBuilder;
|
||||
}
|
||||
|
||||
|
||||
inline ReplyInterpreter&
|
||||
Request::Reply()
|
||||
{
|
||||
return fReply;
|
||||
}
|
||||
|
||||
|
||||
#endif // REQUEST_H
|
||||
|
||||
@@ -16,9 +16,27 @@ RequestBuilder::RequestBuilder(Procedure proc)
|
||||
:
|
||||
fOpCount(0),
|
||||
fProcedure(proc),
|
||||
fRequest(RPC::Call::Create(proc, RPC::Auth::CreateSys(),
|
||||
RPC::Auth::CreateNone()))
|
||||
fRequest(NULL)
|
||||
{
|
||||
_InitHeader();
|
||||
}
|
||||
|
||||
|
||||
RequestBuilder::~RequestBuilder()
|
||||
{
|
||||
delete fRequest;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RequestBuilder::_InitHeader()
|
||||
{
|
||||
fRequest = RPC::Call::Create(fProcedure, RPC::Auth::CreateSys(),
|
||||
RPC::Auth::CreateNone());
|
||||
|
||||
if (fRequest == NULL)
|
||||
return;
|
||||
|
||||
if (fProcedure == ProcCompound) {
|
||||
fRequest->Stream().AddOpaque(NULL, 0);
|
||||
fRequest->Stream().AddUInt(0);
|
||||
@@ -29,12 +47,6 @@ RequestBuilder::RequestBuilder(Procedure proc)
|
||||
}
|
||||
|
||||
|
||||
RequestBuilder::~RequestBuilder()
|
||||
{
|
||||
delete fRequest;
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
RequestBuilder::Access()
|
||||
{
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
|
||||
class RequestBuilder {
|
||||
public:
|
||||
RequestBuilder(Procedure proc);
|
||||
RequestBuilder(Procedure p = ProcCompound);
|
||||
~RequestBuilder();
|
||||
|
||||
inline void Reset(Procedure proc = ProcCompound);
|
||||
|
||||
status_t Access();
|
||||
status_t Close(uint32 seq, const uint32* id,
|
||||
uint32 stateSeq);
|
||||
@@ -45,6 +47,8 @@ public:
|
||||
RPC::Call* Request();
|
||||
|
||||
private:
|
||||
void _InitHeader();
|
||||
|
||||
void _AttrBitmap(XDR::WriteStream& stream,
|
||||
Attribute* attrs, uint32 count);
|
||||
|
||||
@@ -57,5 +61,17 @@ private:
|
||||
};
|
||||
|
||||
|
||||
inline void
|
||||
RequestBuilder::Reset(Procedure proc)
|
||||
{
|
||||
fRequest->Stream().Clear();
|
||||
fOpCount = 0;
|
||||
fProcedure = proc;
|
||||
delete fRequest;
|
||||
|
||||
_InitHeader();
|
||||
}
|
||||
|
||||
|
||||
#endif // REQUESTBUILDER_H
|
||||
|
||||
|
||||
@@ -190,6 +190,17 @@ WriteStream::~WriteStream()
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WriteStream::Clear()
|
||||
{
|
||||
free(fBuffer);
|
||||
fSize = kInitialSize;
|
||||
fBuffer = reinterpret_cast<uint32*>(malloc(fSize));
|
||||
fError = B_OK;
|
||||
fPosition = 0;
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
WriteStream::InsertUInt(Stream::Position pos, uint32 x)
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
virtual ~WriteStream();
|
||||
|
||||
inline int Size() const;
|
||||
void Clear();
|
||||
|
||||
status_t InsertUInt(Stream::Position pos, uint32 x);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user