⛏️ index : haiku.git

/*
 * Copyright 2012-2020 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Paweł Dziepak, pdziepak@quarnos.org
 */


#include <AutoDeleter.h>
#include <fs_cache.h>
#include <fs_interface.h>
#include <fs_volume.h>

#include "Connection.h"
#include "FileSystem.h"
#include "IdMap.h"
#include "Inode.h"
#include "NFS4Defs.h"
#include "RequestBuilder.h"
#include "ReplyInterpreter.h"
#include "RootInode.h"
#include "RPCCallbackServer.h"
#include "RPCServer.h"
#include "VnodeToInode.h"
#include "WorkQueue.h"


extern fs_volume_ops gNFSv4VolumeOps;
extern fs_vnode_ops gNFSv4VnodeOps;


RPC::ServerManager* gRPCServerManager;


RPC::ProgramData*
CreateNFS4Server(RPC::Server* serv)
{
	return new NFS4Server(serv);
}


// Format: ip{4,6}_address:path options
// Available options:
//	hard		- retry requests until success
//	soft		- retry requests no more than retrans times (default)
//  timeo=X		- request time limit before next retransmission (default: 60s)
//	retrans=X	- retry requests X times (default: 5)
//	ac			- use metadata cache (default)
//	noac		- do not use metadata cache
//	xattr-emu	- emulate named attributes
//	noxattr-emu	- do not emulate named attributes (default)
//	port=X		- connect to port X (default: 2049)
//	proto=X		- user transport protocol X (default: tcp)
//	dirtime=X	- attempt revalidate directory cache not more often than each X
//				  seconds
static status_t
ParseArguments(const char* _args, AddressResolver** address, char** _server,
	char** _path, MountConfiguration* conf)
{
	if (_args == NULL)
		return B_BAD_VALUE;

	char* args = strdup(_args);
	if (args == NULL)
		return B_NO_MEMORY;
	MemoryDeleter argsDeleter(args);

	char* options = strchr(args, ' ');
	if (options != NULL)
		*options++ = '\0';

	char* path = strrchr(args, ':');
	if (path == NULL)
		return B_MISMATCHED_VALUES;
	*path++ = '\0';

	*_server = strdup(args);
	if (*_server == NULL)
		return B_NO_MEMORY;
	*address = new AddressResolver(args);
	if (*address == NULL) {
		free(*_server);
		return B_NO_MEMORY;
	}

	*_path = strdup(path);
	if (*_path == NULL) {
		free(*_server);
		delete *address;
		return B_NO_MEMORY;
	}

	conf->fHard = false;
	conf->fRetryLimit = 5;
	conf->fRequestTimeout = sSecToBigTime(60);
	conf->fEmulateNamedAttrs = false;
	conf->fCacheMetadata = true;
	conf->fDirectoryCacheTime = sSecToBigTime(5);

	char* optionsEnd = NULL;
	if (options != NULL)
		optionsEnd = strchr(options, ' ');
	while (options != NULL && *options != '\0') {
		if (optionsEnd != NULL)
			*optionsEnd++ = '\0';

		if (strcmp(options, "hard") == 0)
			conf->fHard = true;
		else if (strncmp(options, "retrans=", 8) == 0) {
			options += strlen("retrans=");
			conf->fRetryLimit = atoi(options);
		} else if (strncmp(options, "timeo=", 6) == 0) {
			options += strlen("timeo=");
			conf->fRequestTimeout = atoi(options);
		} else if (strcmp(options, "noac") == 0)
			conf->fCacheMetadata = false;
		else if (strcmp(options, "xattr-emu") == 0)
			conf->fEmulateNamedAttrs = true;
		else if (strncmp(options, "port=", 5) == 0) {
			options += strlen("port=");
			(*address)->ForcePort(atoi(options));
		} else if (strncmp(options, "proto=", 6) == 0) {
			options += strlen("proto=");
			(*address)->ForceProtocol(options);
		} else if (strncmp(options, "dirtime=", 8) == 0) {
			options += strlen("dirtime=");
			conf->fDirectoryCacheTime = sSecToBigTime(atoi(options));
		}

		options = optionsEnd;
		if (options != NULL)
			optionsEnd = strchr(options, ' ');
	}

	return B_OK;
}


static status_t
nfs4_mount(fs_volume* volume, const char* device, uint32 flags,
	const char* args, ino_t* _rootVnodeID)
{
	TRACE("volume = %p, device = %s, flags = %" B_PRIu32 ", args = %s\n",
		volume, device, flags, args);

	status_t result;

	/* prepare idmapper server */
	MutexLocker locker(gIdMapperLock);
	if (gIdMapper == NULL) {
		gIdMapper = new(std::nothrow) IdMap;
		if (gIdMapper == NULL)
			return B_NO_MEMORY;

		result = gIdMapper->InitStatus();
		if (result != B_OK) {
			delete gIdMapper;
			gIdMapper = NULL;
			return result;
		}
	}
	locker.Unlock();

	MountConfiguration config;
	config.fReadOnly = (flags & B_MOUNT_READ_ONLY) != 0;

	AddressResolver* resolver;
	char* path;
	char* serverName;
	result = ParseArguments(args, &resolver, &serverName, &path, &config);
	if (result != B_OK) {
		ERROR("Unable to parse mount arguments!\n");
		return result;
	}

	MemoryDeleter pathDeleter(path);
	MemoryDeleter serverNameDeleter(serverName);

	RPC::Server* server;
	result = gRPCServerManager->Acquire(&server, resolver, CreateNFS4Server);
	delete resolver;
	if (result != B_OK) {
		ERROR("Unable to Acquire RPCServerManager!\n");
		return result;
	}

	FileSystem* fs;
	result = FileSystem::Mount(&fs, server, serverName, path, volume, config);
	if (result != B_OK) {
		ERROR("Error mounting filesystem: %s\n", strerror(result));
		gRPCServerManager->Release(server);
		return result;
	}

	Inode* inode = fs->Root();
	if (inode == NULL) {
		delete fs;
		gRPCServerManager->Release(server);
		ERROR("Unable to locate root inode!\n");
		return B_IO_ERROR;
	}

	volume->private_volume = fs;
	volume->ops = &gNFSv4VolumeOps;

	VnodeToInode* vti = new VnodeToInode(inode->ID(), fs);
	if (vti == NULL) {
		delete fs;
		gRPCServerManager->Release(server);
		ERROR("Unable to translate vnode to inode!\n");
		return B_NO_MEMORY;
	}

	vti->Replace(inode);
	result = publish_vnode(volume, inode->ID(), vti, &gNFSv4VnodeOps,
							inode->Type(), 0);
	if (result != B_OK)
		return result;

	*_rootVnodeID = inode->ID();

	TRACE("*_rootVnodeID = %" B_PRIi64 "\n", inode->ID());

	return B_OK;
}


static status_t
nfs4_get_vnode(fs_volume* volume, ino_t id, fs_vnode* vnode, int* _type,
	uint32* _flags, bool reenter)
{
	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	TRACE("volume = %p, id = %" B_PRIi64 "\n", volume, id);

	VnodeToInode* vnodeToInode = new VnodeToInode(id, fs);
	if (vnodeToInode == NULL)
		return B_NO_MEMORY;

	Inode* inode;	
	status_t result = fs->GetInode(id, &inode);
	if (result != B_OK) {
		delete vnodeToInode;
		return result;
	}

	vnodeToInode->Replace(inode);
	vnode->ops = &gNFSv4VnodeOps;
	vnode->private_node = vnodeToInode;

	*_type = inode->Type();
	*_flags = 0;

	return B_OK;
}


static status_t
nfs4_unmount(fs_volume* volume)
{
	TRACE("volume = %p\n", volume);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	RPC::Server* server = fs->Server();

	put_vnode(volume, fs->Root()->ID());

	delete fs;
	gRPCServerManager->Release(server);

	return B_OK;
}


static status_t
nfs4_read_fs_info(fs_volume* volume, struct fs_info* info)
{
	TRACE("volume = %p\n", volume);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	RootInode* inode = reinterpret_cast<RootInode*>(fs->Root());
	return inode->ReadInfo(info);
}


static status_t
nfs4_lookup(fs_volume* volume, fs_vnode* dir, const char* name, ino_t* _id)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(dir->private_node);

	if (!strcmp(name, ".")) {
		*_id = vti->ID();
		void* ptr;
		return get_vnode(volume, *_id, &ptr);
	}

	VnodeToInodeLocker locker(vti);

	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	TRACE("volume = %p, dir = %" B_PRIi64 ", name = %s\n", volume, vti->ID(),
		name);

	status_t result = inode->LookUp(name, _id);
	if (result != B_OK)
		return result;
	locker.Unlock();

	TRACE("*_id = %" B_PRIi64 "\n", *_id);

	// If VTI holds an outdated Inode next operation performed on it will
	// return either ERR_STALE or ERR_FHEXPIRED. Both of these error codes
	// will cause FileInfo data to be updated (the former will also cause Inode
	// object to be recreated). We are taking an optimistic (an lazy) approach
	// here. The following code just ensures VTI won't be removed too soon.
	void* ptr;
	result = get_vnode(volume, *_id, &ptr);
	if (result == B_OK)
		unremove_vnode(volume, *_id);

	return result;
}


static status_t
nfs4_put_vnode(fs_volume* volume, fs_vnode* vnode, bool reenter)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	delete vti;
	return B_OK;
}


static status_t
nfs4_remove_vnode(fs_volume* volume, fs_vnode* vnode, bool reenter)
{
	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	Inode* node = vti->GetPointer();

	if (node == fs->Root())
		return B_OK;

	// Unless the file was deleted by someone else, verify that all known names have been
	// unlinked.
	if (node != NULL && node->IsStale() == false) {
		FileInfo fileInfo;
		ASSERT(fs->InoIdMap()->GetFileInfo(&fileInfo, vti->ID()) == B_ENTRY_NOT_FOUND);
	}

	delete vti;

	return B_OK;
}


static status_t
nfs4_read_pages(fs_volume* _volume, fs_vnode* vnode, void* _cookie, off_t pos,
	const iovec* vecs, size_t count, size_t* _numBytes)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, pos = %" B_PRIi64 \
		", count = %lu, numBytes = %lu\n", _volume, vti->ID(), _cookie, pos,
		count, *_numBytes);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	status_t result;
	size_t totalRead = 0;
	bool eof = false;
	for (size_t i = 0; i < count && !eof; i++) {
		size_t bytesLeft = vecs[i].iov_len;
		char* buffer = reinterpret_cast<char*>(vecs[i].iov_base);

		do {
			size_t bytesRead = bytesLeft;
			result = inode->ReadDirect(cookie, pos, buffer, &bytesRead, &eof);
			if (result != B_OK)
				return result;

			totalRead += bytesRead;
			pos += bytesRead;
			buffer += bytesRead;
			bytesLeft -= bytesRead;
		} while (bytesLeft > 0 && !eof);
	}

	*_numBytes = totalRead;

	TRACE("*numBytes = %lu\n", totalRead);

	return B_OK;
}


static status_t
nfs4_write_pages(fs_volume* _volume, fs_vnode* vnode, void* _cookie, off_t pos,
	const iovec* vecs, size_t count, size_t* _numBytes)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, pos = %" B_PRIi64 \
		", count = %lu, numBytes = %lu\n", _volume, vti->ID(), _cookie, pos,
		count, *_numBytes);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	status_t result;
	for (size_t i = 0; i < count; i++) {
		uint64 bytesLeft = vecs[i].iov_len;
		if (pos + bytesLeft > inode->MaxFileSize())
			bytesLeft = inode->MaxFileSize() - pos;

		char* buffer = reinterpret_cast<char*>(vecs[i].iov_base);

		do {
			size_t bytesWritten = bytesLeft;

			result = inode->WriteDirect(cookie, pos, buffer, &bytesWritten);
			if (result != B_OK)
				return result;

			bytesLeft -= bytesWritten;
			pos += bytesWritten;
			buffer += bytesWritten;
		} while (bytesLeft > 0);
	}

	return B_OK;
}


static status_t
nfs4_io(fs_volume* volume, fs_vnode* vnode, void* cookie, io_request* request)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		vti->ID(), cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	IORequestArgs* args = new(std::nothrow) IORequestArgs;
	if (args == NULL) {
		notify_io_request(request, B_NO_MEMORY);
		return B_NO_MEMORY;
	}
	args->fRequest = request;
	args->fInode = inode;

	status_t result = gWorkQueue->EnqueueJob(IORequest, args);
	if (result != B_OK)
		notify_io_request(request, result);

	return result;
}


static status_t
nfs4_get_file_map(fs_volume* volume, fs_vnode* vnode, off_t _offset,
	size_t size, struct file_io_vec* vecs, size_t* _count)
{
	return B_ERROR;
}


static status_t
nfs4_set_flags(fs_volume* volume, fs_vnode* vnode, void* _cookie, int flags)
{
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, flags = %d\n",
		volume,	reinterpret_cast<VnodeToInode*>(vnode->private_node)->ID(),
		_cookie, flags);

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);
	cookie->fMode = (cookie->fMode & ~(O_APPEND | O_NONBLOCK)) | flags;
	return B_OK;
}


static status_t
nfs4_fsync(fs_volume* volume, fs_vnode* vnode, bool dataOnly)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->SyncAndCommit();
}


static status_t
nfs4_read_symlink(fs_volume* volume, fs_vnode* link, char* buffer,
	size_t* _bufferSize)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(link->private_node);
	TRACE("volume = %p, link = %" B_PRIi64 "\n", volume, vti->ID());

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->ReadLink(buffer, _bufferSize);
}


static status_t
nfs4_create_symlink(fs_volume* volume, fs_vnode* dir, const char* name,
	const char* path, int mode)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(dir->private_node);
	TRACE("volume = %p, dir = %" B_PRIi64 ", name = %s, path = %s, mode = %d\n",
		volume, vti->ID(), name, path, mode);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	ino_t id;
	status_t result = inode->CreateLink(name, path, mode, &id);
	if (result != B_OK)
		return result;

	result = get_vnode(volume, id, reinterpret_cast<void**>(&vti));
	if (result == B_OK) {
		unremove_vnode(volume, id);
		vti->Clear();
		put_vnode(volume, id);
	}

	return B_OK;
}


static status_t
nfs4_link(fs_volume* volume, fs_vnode* dir, const char* name, fs_vnode* vnode)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	VnodeToInode* dirVti = reinterpret_cast<VnodeToInode*>(dir->private_node);
	TRACE("volume = %p, dir = %" B_PRIi64 ", name = %s, vnode = %" B_PRIi64
		"\n", volume, dirVti->ID(), name, vti->ID());

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _dir(dirVti);
	Inode* dirInode = dirVti->Get();
	if (dirInode == NULL)
		return B_ENTRY_NOT_FOUND;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->Link(dirInode, name);
}


static status_t
nfs4_unlink(fs_volume* volume, fs_vnode* dir, const char* name)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(dir->private_node);

	TRACE("volume = %p, dir = %" B_PRIi64 ", name = %s\n", volume, vti->ID(),
		name);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker locker(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	ino_t id;
	status_t result = inode->LookUp(name, &id);
	if (result != B_OK)
		return B_ENTRY_NOT_FOUND;

	VnodeToInode* childVti = NULL;
	result = get_vnode(volume, id, reinterpret_cast<void**>(&childVti));
	if (result == B_OK) {
		childVti->Get();
			// Needed to ensure childVti::fInode is non-NULL prior to VnodeToInode::Unlink.
		ino_t removedId;
		status_t result = inode->Remove(name, NF4REG, &removedId);
		if (result != B_OK)
			return result;
		ASSERT(removedId == id);
		locker.Unlock();

		if (childVti->Unlink(inode->fInfo.fNames, name))
			remove_vnode(volume, id);

		put_vnode(volume, id);
	}

	return result;
}


static status_t
nfs4_rename(fs_volume* volume, fs_vnode* fromDir, const char* fromName,
	fs_vnode* toDir, const char* toName)
{
	VnodeToInode* fromVti
		= reinterpret_cast<VnodeToInode*>(fromDir->private_node);
	VnodeToInode* toVti = reinterpret_cast<VnodeToInode*>(toDir->private_node);
	TRACE("volume = %p, fromDir = %" B_PRIi64 ", toDir = %" B_PRIi64 ","
		" fromName = %s, toName = %s\n", volume, fromVti->ID(), toVti->ID(),
		fromName, toName);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _from(fromVti);
	Inode* fromInode = fromVti->Get();
	if (fromInode == NULL)
		return B_ENTRY_NOT_FOUND;

	VnodeToInodeLocker _to(toVti);
	Inode* toInode = toVti->Get();
	if (toInode == NULL)
		return B_ENTRY_NOT_FOUND;

	ino_t id;
	ino_t oldID;
	status_t result = Inode::Rename(fromInode, toInode, fromName, toName, false,
		&id, &oldID);
	if (result != B_OK)
		return result;

	VnodeToInode* vti;

	if (oldID != 0) {
		// we have overriden an inode
		result = get_vnode(volume, oldID, reinterpret_cast<void**>(&vti));
		if (result == B_OK) {
			if (vti->Unlink(toInode->fInfo.fNames, toName))
				remove_vnode(volume, oldID);
			put_vnode(volume, oldID);
		}
	}

	result = get_vnode(volume, id, reinterpret_cast<void**>(&vti));
	if (result == B_OK) {
		Inode* child = vti->Get();
		if (child == NULL) {
			put_vnode(volume, id);
			return B_ENTRY_NOT_FOUND;
		}

		unremove_vnode(volume, id);
		child->fInfo.fNames->RemoveName(fromInode->fInfo.fNames, fromName);
		child->fInfo.fNames->AddName(toInode->fInfo.fNames, toName);
		put_vnode(volume, id);
	}

	return B_OK;
}


static status_t
nfs4_access(fs_volume* volume, fs_vnode* vnode, int mode)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", mode = %d\n", volume, vti->ID(),
		mode);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->Access(mode);
}


static status_t
nfs4_read_stat(fs_volume* volume, fs_vnode* vnode, struct stat* stat)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	status_t result = inode->Stat(stat);
	if (inode->GetOpenState() != NULL)
		stat->st_size = inode->MaxFileSize();
	return result;
}


static status_t
nfs4_write_stat(fs_volume* volume, fs_vnode* vnode, const struct stat* stat,
	uint32 statMask)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", statMask = %" B_PRIu32 "\n",
		volume,	vti->ID(), statMask);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->WriteStat(stat, statMask);
}


static status_t
nfs4_create(fs_volume* volume, fs_vnode* dir, const char* name, int openMode,
	int perms, void** _cookie, ino_t* _newVnodeID)
{
	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);

	OpenFileCookie* cookie = new OpenFileCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(dir->private_node);
	TRACE("volume = %p, dir = %" B_PRIi64 ", name = %s, openMode = %d,"	\
		" perms = %d\n", volume, vti->ID(), name, openMode, perms);

	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	MutexLocker createLocker(fs->CreateFileLock());

	OpenDelegationData data;
	status_t result = inode->Create(name, openMode, perms, cookie, &data,
		_newVnodeID);
	if (result != B_OK) {
		delete cookie;
		return result;
	}

	result = get_vnode(volume, *_newVnodeID, reinterpret_cast<void**>(&vti));
	if (result != B_OK) {
		delete cookie;
		return result;
	}

	VnodeToInodeLocker _child(vti);
	Inode* child = vti->Get();
	if (child == NULL) {
		delete cookie;
		put_vnode(volume, *_newVnodeID);
		return B_ENTRY_NOT_FOUND;
	}

	child->SetOpenState(cookie->fOpenState);

	if (data.fType != OPEN_DELEGATE_NONE) {
		Delegation* delegation
			= new(std::nothrow) Delegation(data, child,
				cookie->fOpenState->fClientID);
		if (delegation != NULL) {
			delegation->fInfo = cookie->fOpenState->fInfo;
			delegation->fFileSystem = child->GetFileSystem();
			child->SetDelegation(delegation);
		}
	}

	TRACE("*cookie = %p, *newVnodeID = %" B_PRIi64 "\n", *_cookie,
		*_newVnodeID);
	return result;
}


static status_t
nfs4_open(fs_volume* volume, fs_vnode* vnode, int openMode, void** _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", openMode = %d\n", volume,
		vti->ID(), openMode);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly && (openMode & O_RWMASK) != O_RDONLY)
		return B_READ_ONLY_DEVICE;

	if (inode->Type() == S_IFDIR && (openMode & O_RWMASK) != O_RDONLY)
		return B_IS_A_DIRECTORY;
	if ((openMode & O_DIRECTORY) != 0 && inode->Type() != S_IFDIR)
		return B_NOT_A_DIRECTORY;

	if (inode->Type() == S_IFDIR || inode->Type() == S_IFLNK) {
		*_cookie = NULL;
		return B_OK;
	}

	OpenFileCookie* cookie = new OpenFileCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	status_t result = inode->Open(openMode, cookie);
	if (result != B_OK)
		delete cookie;

	TRACE("*cookie = %p\n", *_cookie);

	return result;
}


static status_t
nfs4_close(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		vti->ID(), _cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	if (inode->Type() == S_IFDIR || inode->Type() == S_IFLNK)
		return B_OK;

	Cookie* cookie = reinterpret_cast<Cookie*>(_cookie);
	return cookie->CancelAll();
}


static status_t
nfs4_free_cookie(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		vti->ID(), _cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	if (inode->Type() == S_IFDIR || inode->Type() == S_IFLNK)
		return B_OK;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	inode->Close(cookie);
	delete cookie;

	return B_OK;
}


static status_t
nfs4_read(fs_volume* volume, fs_vnode* vnode, void* _cookie, off_t pos,
	void* buffer, size_t* length)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, pos = %" B_PRIi64 \
		", length = %lu\n", volume, vti->ID(), _cookie, pos, *length);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	if (inode->Type() == S_IFDIR)
		return B_IS_A_DIRECTORY;

	if (inode->Type() == S_IFLNK)
		return B_BAD_VALUE;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	return inode->Read(cookie, pos, buffer, length);;
}


static status_t
nfs4_write(fs_volume* volume, fs_vnode* vnode, void* _cookie, off_t pos,
	const void* _buffer, size_t* length)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, pos = %" B_PRIi64 \
		", length = %lu\n", volume, vti->ID(), _cookie, pos, *length);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	if (inode->Type() == S_IFDIR)
		return B_IS_A_DIRECTORY;

	if (inode->Type() == S_IFLNK)
		return B_BAD_VALUE;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	return inode->Write(cookie, pos, _buffer, length);
}


static status_t
nfs4_create_dir(fs_volume* volume, fs_vnode* parent, const char* name,
	int mode)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(parent->private_node);
	TRACE("volume = %p, parent = %" B_PRIi64 ", mode = %d\n", volume, vti->ID(),
		mode);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	ino_t id;
	status_t result = inode->CreateDir(name, mode, &id);
	if (result != B_OK)
		return result;

	result = get_vnode(volume, id, reinterpret_cast<void**>(&vti));
	if (result == B_OK) {
		unremove_vnode(volume, id);
		vti->Clear();
		put_vnode(volume, id);
	}

	return B_OK;
}


static status_t
nfs4_remove_dir(fs_volume* volume, fs_vnode* parent, const char* name)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(parent->private_node);
	TRACE("volume = %p, parent = %" B_PRIi64 ", name = %s\n", volume, vti->ID(),
		name);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	ino_t id;
	status_t result = inode->Remove(name, NF4DIR, &id);
	if (result != B_OK)
		return result;

	result = get_vnode(volume, id, reinterpret_cast<void**>(&vti));
	if (result == B_OK) {
		if (vti->Unlink(inode->fInfo.fNames, name))
			remove_vnode(volume, id);
		put_vnode(volume, id);
	}

	return B_OK;
}


static status_t
nfs4_open_dir(fs_volume* volume, fs_vnode* vnode, void** _cookie)
{
	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	OpenDirCookie* cookie = new(std::nothrow) OpenDirCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	status_t result = inode->OpenDir(cookie);
	if (result != B_OK)
		delete cookie;

	TRACE("*cookie = %p\n", *_cookie);

	return result;
}


static status_t 
nfs4_close_dir(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		reinterpret_cast<VnodeToInode*>(vnode->private_node)->ID(), _cookie);

	Cookie* cookie = reinterpret_cast<Cookie*>(_cookie);
	return cookie->CancelAll();
}


static status_t
nfs4_free_dir_cookie(fs_volume* volume, fs_vnode* vnode, void* cookie)
{
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		reinterpret_cast<VnodeToInode*>(vnode->private_node)->ID(), cookie);

	delete reinterpret_cast<OpenDirCookie*>(cookie);
	return B_OK;
}


static status_t
nfs4_read_dir(fs_volume* volume, fs_vnode* vnode, void* _cookie,
	struct dirent* buffer, size_t bufferSize, uint32* _num)
{
	OpenDirCookie* cookie = reinterpret_cast<OpenDirCookie*>(_cookie);
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume, vti->ID(),
		_cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->ReadDir(buffer, bufferSize, _num, cookie);
}


static status_t
nfs4_rewind_dir(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p\n", volume,
		reinterpret_cast<VnodeToInode*>(vnode->private_node)->ID(), _cookie);

	OpenDirCookie* cookie = reinterpret_cast<OpenDirCookie*>(_cookie);
	cookie->fSpecial = 0;
	if (cookie->fSnapshot != NULL)
		cookie->fSnapshot->ReleaseReference();
	cookie->fSnapshot = NULL;
	cookie->fCurrent = NULL;
	cookie->fEOF = false;

	return B_OK;
}


static status_t
nfs4_open_attr_dir(fs_volume* volume, fs_vnode* vnode, void** _cookie)
{
	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	OpenDirCookie* cookie = new(std::nothrow) OpenDirCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 "\n", volume, vti->ID());

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	status_t result = inode->OpenAttrDir(cookie);
	if (result != B_OK)
		delete cookie;

	return result;
}


static status_t
nfs4_close_attr_dir(fs_volume* volume, fs_vnode* vnode, void* cookie)
{
	return nfs4_close_dir(volume, vnode, cookie);
}


static status_t
nfs4_free_attr_dir_cookie(fs_volume* volume, fs_vnode* vnode, void* cookie)
{
	return nfs4_free_dir_cookie(volume, vnode, cookie);
}


static status_t
nfs4_read_attr_dir(fs_volume* volume, fs_vnode* vnode, void* cookie,
	struct dirent* buffer, size_t bufferSize, uint32* _num)
{
	return nfs4_read_dir(volume, vnode, cookie, buffer, bufferSize, _num);
}


static status_t
nfs4_rewind_attr_dir(fs_volume* volume, fs_vnode* vnode, void* cookie)
{
	return nfs4_rewind_dir(volume, vnode, cookie);
}


static status_t
nfs4_create_attr(fs_volume* volume, fs_vnode* vnode, const char* name,
	uint32 type, int openMode, void** _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	OpenAttrCookie* cookie = new OpenAttrCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	status_t result = inode->OpenAttr(name, openMode, cookie, true, type);
	if (result != B_OK)
		delete cookie;

	return result;
}


static status_t
nfs4_open_attr(fs_volume* volume, fs_vnode* vnode, const char* name,
	int openMode, void** _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly && (openMode & O_RWMASK) != O_RDONLY)
		return B_READ_ONLY_DEVICE;

	OpenAttrCookie* cookie = new OpenAttrCookie(fs);
	if (cookie == NULL)
		return B_NO_MEMORY;
	*_cookie = cookie;

	status_t result = inode->OpenAttr(name, openMode, cookie, false);
	if (result != B_OK)
		delete cookie;

	return result;
}


static status_t
nfs4_close_attr(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	Cookie* cookie = reinterpret_cast<Cookie*>(_cookie);
	return cookie->CancelAll();
}


static status_t
nfs4_free_attr_cookie(fs_volume* volume, fs_vnode* vnode, void* _cookie)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	OpenAttrCookie* cookie = reinterpret_cast<OpenAttrCookie*>(_cookie);
	inode->CloseAttr(cookie);
	delete cookie;

	return B_OK;
}


static status_t
nfs4_read_attr(fs_volume* volume, fs_vnode* vnode, void* _cookie, off_t pos,
	void* buffer, size_t* length)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenAttrCookie* cookie = reinterpret_cast<OpenAttrCookie*>(_cookie);
	bool eof;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->ReadDirect(cookie, pos, buffer, length, &eof);
}


static status_t
nfs4_write_attr(fs_volume* volume, fs_vnode* vnode, void* _cookie, off_t pos,
	const void* buffer, size_t* length)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenAttrCookie* cookie = reinterpret_cast<OpenAttrCookie*>(_cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->WriteDirect(cookie, pos, buffer, length);
}


static status_t
nfs4_read_attr_stat(fs_volume* volume, fs_vnode* vnode, void* _cookie,
	struct stat* stat)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenAttrCookie* cookie = reinterpret_cast<OpenAttrCookie*>(_cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->Stat(stat, cookie);
}


static status_t
nfs4_write_attr_stat(fs_volume* volume, fs_vnode* vnode, void* _cookie,
	const struct stat* stat, int statMask)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenAttrCookie* cookie = reinterpret_cast<OpenAttrCookie*>(_cookie);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->WriteStat(stat, statMask, cookie);
}


static status_t
nfs4_rename_attr(fs_volume* volume, fs_vnode* fromVnode, const char* fromName,
	fs_vnode* toVnode, const char* toName)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(toVnode->private_node);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker to(vti);
	Inode* toInode = vti->Get();
	if (toInode == NULL)
		return B_ENTRY_NOT_FOUND;

	vti = reinterpret_cast<VnodeToInode*>(fromVnode->private_node);
	VnodeToInodeLocker from(vti);
	Inode* fromInode = vti->Get();
	if (fromInode == NULL)
		return B_ENTRY_NOT_FOUND;

	return Inode::Rename(fromInode, toInode, fromName, toName, true);
}


static status_t
nfs4_remove_attr(fs_volume* volume, fs_vnode* vnode, const char* name)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->Remove(name, NF4NAMEDATTR, NULL);
}


static status_t
nfs4_test_lock(fs_volume* volume, fs_vnode* vnode, void* _cookie,
	struct flock* lock)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, lock = %p\n",
		volume, vti->ID(), _cookie, lock);

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	return inode->TestLock(cookie, lock);
}


static status_t
nfs4_acquire_lock(fs_volume* volume, fs_vnode* vnode, void* _cookie,
			const struct flock* lock, bool wait)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, lock = %p\n",
		volume, vti->ID(), _cookie, lock);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	inode->RevalidateFileCache();
	return inode->AcquireLock(cookie, lock, wait);
}


static status_t
nfs4_release_lock(fs_volume* volume, fs_vnode* vnode, void* _cookie,
	const struct flock* lock)
{
	VnodeToInode* vti = reinterpret_cast<VnodeToInode*>(vnode->private_node);
	TRACE("volume = %p, vnode = %" B_PRIi64 ", cookie = %p, lock = %p\n",
		volume, vti->ID(), _cookie, lock);

	FileSystem* fs = reinterpret_cast<FileSystem*>(volume->private_volume);
	if (fs->GetConfiguration().fReadOnly)
		return B_READ_ONLY_DEVICE;

	VnodeToInodeLocker _(vti);
	Inode* inode = vti->Get();
	if (inode == NULL)
		return B_ENTRY_NOT_FOUND;

	if (inode->Type() == S_IFDIR || inode->Type() == S_IFLNK)
		return B_OK;

	OpenFileCookie* cookie = reinterpret_cast<OpenFileCookie*>(_cookie);

	if (lock != NULL)
		return inode->ReleaseLock(cookie, lock);
	else
		return inode->ReleaseAllLocks(cookie);
}


status_t
nfs4_init()
{
	init_debugging();

	gRPCServerManager = new(std::nothrow) RPC::ServerManager;
	if (gRPCServerManager == NULL)
		return B_NO_MEMORY;

	mutex_init(&gIdMapperLock, "idmapper Init Lock");
	gIdMapper = NULL;

	gWorkQueue = new(std::nothrow) WorkQueue;
	if (gWorkQueue == NULL || gWorkQueue->InitStatus() != B_OK) {
		delete gWorkQueue;
		mutex_destroy(&gIdMapperLock);
		delete gRPCServerManager;
		return B_NO_MEMORY;
	}

#ifdef _KERNEL_MODE
	add_debugger_command("nfs4", kprintf_volume, "dump an nfs4 volume");
	add_debugger_command("nfs4_inode", kprintf_inode, "dump an nfs4 inode");
#endif // _KERNEL_MODE

	return B_OK;
}


status_t
nfs4_uninit()
{
	exit_debugging();

	RPC::CallbackServer::ShutdownAll();

	delete gIdMapper;
	delete gWorkQueue;
	delete gRPCServerManager;

	mutex_destroy(&gIdMapperLock);

#ifdef _KERNEL_MODE
	remove_debugger_command("nfs4", kprintf_volume);
	remove_debugger_command("nfs4_inode", kprintf_inode);
#endif // _KERNEL_MODE

	return B_OK;
}


static status_t
nfs4_std_ops(int32 op, ...)
{
	switch (op) {
		case B_MODULE_INIT:
			return nfs4_init();
		case B_MODULE_UNINIT:
			return nfs4_uninit();
		default:
			return B_ERROR;
	}
}


fs_volume_ops gNFSv4VolumeOps = {
	nfs4_unmount,
	nfs4_read_fs_info,
	NULL,
	NULL,
	nfs4_get_vnode,
};

fs_vnode_ops gNFSv4VnodeOps = {
	nfs4_lookup,
	NULL,	// get_vnode_name()
	nfs4_put_vnode,
	nfs4_remove_vnode,

	/* VM file access */
	NULL,	// can_page()
	nfs4_read_pages,
	nfs4_write_pages,

	nfs4_io,
	NULL,	// cancel_io()

	nfs4_get_file_map,

	NULL,	// ioctl()
	nfs4_set_flags,
	NULL,	// fs_select()
	NULL,	// fs_deselect()
	nfs4_fsync,

	nfs4_read_symlink,
	nfs4_create_symlink,

	nfs4_link,
	nfs4_unlink,
	nfs4_rename,

	nfs4_access,
	nfs4_read_stat,
	nfs4_write_stat,
	NULL,	// fs_preallocate()

	/* file operations */
	nfs4_create,
	nfs4_open,
	nfs4_close,
	nfs4_free_cookie,
	nfs4_read,
	nfs4_write,

	/* directory operations */
	nfs4_create_dir,
	nfs4_remove_dir,
	nfs4_open_dir,
	nfs4_close_dir,
	nfs4_free_dir_cookie,
	nfs4_read_dir,
	nfs4_rewind_dir,

	/* attribute directory operations */
	nfs4_open_attr_dir,
	nfs4_close_attr_dir,
	nfs4_free_attr_dir_cookie,
	nfs4_read_attr_dir,
	nfs4_rewind_attr_dir,

	/* attribute operations */
	nfs4_create_attr,
	nfs4_open_attr,
	nfs4_close_attr,
	nfs4_free_attr_cookie,
	nfs4_read_attr,
	nfs4_write_attr,

	nfs4_read_attr_stat,
	nfs4_write_attr_stat,
	nfs4_rename_attr,
	nfs4_remove_attr,

	/* support for node and FS layers */
	NULL,	// create_special_node
	NULL,	// get_super_vnode

	/* lock operations */
	nfs4_test_lock,
	nfs4_acquire_lock,
	nfs4_release_lock,
};

static file_system_module_info sNFSv4ModuleInfo = {
	{
		"file_systems/nfs4" B_CURRENT_FS_API_VERSION,
		0,
		nfs4_std_ops,
	},

	"nfs4",								// short_name
	"Network File System version 4",	// pretty_name

	// DDM flags
	0,

	// scanning
	NULL,	// identify_partition()
	NULL,	// scan_partition()
	NULL,	// free_identify_partition_cookie()
	NULL,	// free_partition_content_cookie()

	nfs4_mount,
};

module_info* modules[] = {
	(module_info*)&sNFSv4ModuleInfo,
	NULL,
};