490e0de548
Import libpuffs and our port of libpuffs. The port was done as part of GSoC 2011 FUSE project, done by Evgeniy Ivanov. The librefuse import did not require any porting efforts. Libpuffs has been modified to understand our VFS-FS protocol and translate between that and PUFFS. As an example that it works, fuse-ntfs-3g from pkgsrc can be compiled and used to mount ntfs partitions: mount -t ntfs-3g <device> <mountpoint> FUSE only works with the asynchronous version of VFS. See <docs/UPDATING> on how to run AVFS. This patch further includes some changes to mount(1) and mount(2) so it's possible to use file systems provided by pkgsrc (note: manual modifications to /etc/system.conf are still needed. There has been made an exception for fuse-ntfs-3g, so it already as an entry).
560 lines
16 KiB
C
560 lines
16 KiB
C
#include "fs.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <assert.h>
|
|
|
|
#include "puffs.h"
|
|
#include "puffs_priv.h"
|
|
|
|
#define SAME 1000
|
|
|
|
|
|
/*===========================================================================*
|
|
* fs_ftrunc *
|
|
*===========================================================================*/
|
|
PUBLIC int fs_ftrunc(void)
|
|
{
|
|
int r;
|
|
struct puffs_node *pn;
|
|
off_t start, end;
|
|
PUFFS_MAKECRED(pcr, &global_kcred);
|
|
|
|
if ((pn = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_INODE_NR)) == NULL)
|
|
return(EINVAL);
|
|
|
|
start = fs_m_in.REQ_TRC_START_LO;
|
|
end = fs_m_in.REQ_TRC_END_LO;
|
|
|
|
if (end == 0) {
|
|
struct vattr va;
|
|
|
|
if (pn->pn_va.va_size == start)
|
|
return(OK);
|
|
|
|
if (global_pu->pu_ops.puffs_node_setattr == NULL)
|
|
return(EINVAL);
|
|
|
|
puffs_vattr_null(&va);
|
|
va.va_size = start;
|
|
|
|
r = global_pu->pu_ops.puffs_node_setattr(global_pu, pn, &va, pcr);
|
|
if (r) return(EINVAL);
|
|
} else {
|
|
/* XXX zerofill the given region. Can we make a hole? */
|
|
off_t bytes_left = end - start;
|
|
char* rw_buf;
|
|
|
|
if (global_pu->pu_ops.puffs_node_write == NULL)
|
|
return(EINVAL);
|
|
|
|
/* XXX split into chunks? */
|
|
rw_buf = malloc(bytes_left);
|
|
if (!rw_buf)
|
|
panic("fs_ftrunc: failed to allocated memory\n");
|
|
memset(rw_buf, 0, bytes_left);
|
|
|
|
r = global_pu->pu_ops.puffs_node_write(global_pu, pn, (uint8_t *)rw_buf,
|
|
start, (size_t *) &bytes_left, pcr, 0);
|
|
free(rw_buf);
|
|
if (r) return(EINVAL);
|
|
}
|
|
|
|
update_times(pn, CTIME | MTIME, 0);
|
|
|
|
return(r);
|
|
}
|
|
|
|
|
|
/*===========================================================================*
|
|
* fs_link *
|
|
*===========================================================================*/
|
|
PUBLIC int fs_link()
|
|
{
|
|
/* Perform the link(name1, name2) system call. */
|
|
|
|
register int r;
|
|
char string[NAME_MAX + 1];
|
|
phys_bytes len;
|
|
struct puffs_node *pn, *pn_dir, *new_pn;
|
|
time_t cur_time;
|
|
struct puffs_kcn pkcnp;
|
|
PUFFS_MAKECRED(pcr, &global_kcred);
|
|
struct puffs_cn pcn = {&pkcnp, (struct puffs_cred *)pcr, {0}};
|
|
|
|
if (global_pu->pu_ops.puffs_node_link == NULL)
|
|
return(OK);
|
|
|
|
/* Copy the link name's last component */
|
|
len = fs_m_in.REQ_PATH_LEN;
|
|
if (len > NAME_MAX + 1)
|
|
return(ENAMETOOLONG);
|
|
|
|
r = sys_safecopyfrom(VFS_PROC_NR, (cp_grant_id_t) fs_m_in.REQ_GRANT, 0,
|
|
(vir_bytes) string, (size_t) len, D);
|
|
if (r != OK) return(r);
|
|
NUL(string, len, sizeof(string));
|
|
|
|
if ((pn = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_INODE_NR)) == NULL)
|
|
return(EINVAL);
|
|
|
|
/* Check to see if the file has maximum number of links already. */
|
|
if (pn->pn_va.va_nlink >= LINK_MAX)
|
|
return(EMLINK);
|
|
|
|
/* Only super_user may link to directories. */
|
|
if ((pn->pn_va.va_mode & I_TYPE) == I_DIRECTORY && caller_uid != SU_UID)
|
|
return(EPERM);
|
|
|
|
if ((pn_dir = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_DIR_INO)) == NULL)
|
|
return(EINVAL);
|
|
|
|
if (pn_dir->pn_va.va_nlink == NO_LINK) {
|
|
/* Dir does not actually exist */
|
|
return(ENOENT);
|
|
}
|
|
|
|
/* If 'name2' exists in full (even if no space) set 'r' to error. */
|
|
if ((new_pn = advance(pn_dir, string, IGN_PERM)) == NULL) {
|
|
r = err_code;
|
|
if (r == ENOENT) r = OK;
|
|
} else {
|
|
r = EEXIST;
|
|
}
|
|
|
|
if (r != OK) return(r);
|
|
|
|
/* Try to link. */
|
|
pcn.pcn_namelen = strlen(string);
|
|
assert(pcn.pcn_namelen <= MAXPATHLEN);
|
|
strcpy(pcn.pcn_name, string);
|
|
|
|
if (buildpath) {
|
|
if (puffs_path_pcnbuild(global_pu, &pcn, pn_dir) != 0)
|
|
return(EINVAL);
|
|
}
|
|
|
|
if (global_pu->pu_ops.puffs_node_link(global_pu, pn_dir, pn, &pcn) != 0)
|
|
r = EINVAL;
|
|
|
|
if (buildpath)
|
|
global_pu->pu_pathfree(global_pu, &pcn.pcn_po_full);
|
|
|
|
if (r != OK) return(EINVAL);
|
|
|
|
cur_time = clock_time();
|
|
update_times(pn, CTIME, cur_time);
|
|
update_times(pn_dir, MTIME | CTIME, cur_time);
|
|
|
|
return(OK);
|
|
}
|
|
|
|
|
|
/*===========================================================================*
|
|
* fs_rdlink *
|
|
*===========================================================================*/
|
|
PUBLIC int fs_rdlink()
|
|
{
|
|
register int r; /* return value */
|
|
size_t copylen;
|
|
struct puffs_node *pn;
|
|
char user_path[PATH_MAX];
|
|
PUFFS_MAKECRED(pcr, &global_kcred);
|
|
|
|
copylen = fs_m_in.REQ_MEM_SIZE < UMAX_FILE_POS ?
|
|
fs_m_in.REQ_MEM_SIZE : UMAX_FILE_POS;
|
|
|
|
assert(copylen <= PATH_MAX);
|
|
|
|
if ((pn = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_INODE_NR)) == NULL)
|
|
return(EINVAL);
|
|
|
|
if (!S_ISLNK(pn->pn_va.va_mode))
|
|
return(EACCES);
|
|
|
|
if (global_pu->pu_ops.puffs_node_readlink == NULL)
|
|
return(EINVAL);
|
|
|
|
r = global_pu->pu_ops.puffs_node_readlink(global_pu, pn, pcr, user_path,
|
|
©len);
|
|
if (r != OK) {
|
|
if (r > 0) r = -r;
|
|
return(r);
|
|
}
|
|
|
|
r = sys_safecopyto(VFS_PROC_NR, (cp_grant_id_t) fs_m_in.REQ_GRANT,
|
|
(vir_bytes) 0, (vir_bytes) user_path, (size_t) copylen, D);
|
|
if (r == OK)
|
|
fs_m_out.RES_NBYTES = copylen;
|
|
|
|
return(r);
|
|
}
|
|
|
|
|
|
FORWARD _PROTOTYPE( void release_node, (struct puffs_usermount *pu,
|
|
struct puffs_node *pn ));
|
|
|
|
/*===========================================================================*
|
|
* fs_rename *
|
|
*===========================================================================*/
|
|
PUBLIC int fs_rename()
|
|
{
|
|
/* Perform the rename(name1, name2) system call. */
|
|
struct puffs_node *old_dirp, *old_ip; /* ptrs to old dir, file pnodes */
|
|
struct puffs_node *new_dirp, *new_ip; /* ptrs to new dir, file pnodes */
|
|
struct puffs_kcn pkcnp_src;
|
|
PUFFS_MAKECRED(pcr_src, &global_kcred);
|
|
struct puffs_cn pcn_src = {&pkcnp_src, (struct puffs_cred *) pcr_src, {0}};
|
|
struct puffs_kcn pkcnp_dest;
|
|
PUFFS_MAKECRED(pcr_dest, &global_kcred);
|
|
struct puffs_cn pcn_targ = {&pkcnp_dest, (struct puffs_cred *) pcr_dest, {0}};
|
|
int r = OK; /* error flag; initially no error */
|
|
int odir, ndir; /* TRUE iff {old|new} file is dir */
|
|
int same_pdir; /* TRUE iff parent dirs are the same */
|
|
phys_bytes len;
|
|
time_t cur_time;
|
|
|
|
if (global_pu->pu_ops.puffs_node_rename == NULL)
|
|
return(EINVAL);
|
|
|
|
/* Copy the last component of the old name */
|
|
len = fs_m_in.REQ_REN_LEN_OLD; /* including trailing '\0' */
|
|
if (len > NAME_MAX + 1)
|
|
return(ENAMETOOLONG);
|
|
|
|
r = sys_safecopyfrom(VFS_PROC_NR, (cp_grant_id_t) fs_m_in.REQ_REN_GRANT_OLD,
|
|
(vir_bytes) 0, (vir_bytes) pcn_src.pcn_name, (size_t) len, D);
|
|
if (r != OK) return(r);
|
|
NUL(pcn_src.pcn_name, len, sizeof(pcn_src.pcn_name));
|
|
pcn_src.pcn_namelen = len - 1;
|
|
|
|
/* Copy the last component of the new name */
|
|
len = fs_m_in.REQ_REN_LEN_NEW; /* including trailing '\0' */
|
|
if (len > NAME_MAX + 1)
|
|
return(ENAMETOOLONG);
|
|
|
|
r = sys_safecopyfrom(VFS_PROC_NR, (cp_grant_id_t) fs_m_in.REQ_REN_GRANT_NEW,
|
|
(vir_bytes) 0, (vir_bytes) pcn_targ.pcn_name, (size_t) len, D);
|
|
if (r != OK) return(r);
|
|
NUL(pcn_targ.pcn_name, len, sizeof(pcn_targ.pcn_name));
|
|
pcn_targ.pcn_namelen = len - 1;
|
|
|
|
/* Get old dir pnode */
|
|
if ((old_dirp = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_REN_OLD_DIR))
|
|
== NULL)
|
|
return(ENOENT);
|
|
|
|
old_ip = advance(old_dirp, pcn_src.pcn_name, IGN_PERM);
|
|
if (!old_ip) {
|
|
return(ENOENT);
|
|
}
|
|
r = err_code;
|
|
|
|
if (r == EENTERMOUNT || r == ELEAVEMOUNT) {
|
|
old_ip = NULL;
|
|
if (r == EENTERMOUNT) r = EXDEV; /* should this fail at all? */
|
|
else if (r == ELEAVEMOUNT) r = EINVAL; /* rename on dot-dot */
|
|
}
|
|
|
|
/* Get new dir pnode */
|
|
if ((new_dirp = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_REN_NEW_DIR))
|
|
== NULL) {
|
|
r = ENOENT;
|
|
} else {
|
|
if (new_dirp->pn_va.va_nlink == NO_LINK) {
|
|
/* Dir does not actually exist */
|
|
return(ENOENT);
|
|
}
|
|
}
|
|
|
|
/* not required to exist */
|
|
new_ip = advance(new_dirp, pcn_targ.pcn_name, IGN_PERM);
|
|
|
|
/* However, if the check failed because the file does exist, don't continue.
|
|
* Note that ELEAVEMOUNT is covered by the dot-dot check later. */
|
|
if (err_code == EENTERMOUNT) {
|
|
new_ip = NULL;
|
|
r = EBUSY;
|
|
}
|
|
|
|
if (old_ip != NULL) {
|
|
/* TRUE iff dir */
|
|
odir = ((old_ip->pn_va.va_mode & I_TYPE) == I_DIRECTORY);
|
|
} else {
|
|
odir = FALSE;
|
|
}
|
|
|
|
if (r != OK) return(r);
|
|
|
|
/* Check for a variety of possible errors. */
|
|
same_pdir = (old_dirp == new_dirp);
|
|
|
|
/* The old or new name must not be . or .. */
|
|
if (strcmp(pcn_src.pcn_name, ".") == 0 ||
|
|
strcmp(pcn_src.pcn_name, "..") == 0 ||
|
|
strcmp(pcn_targ.pcn_name, ".") == 0 ||
|
|
strcmp(pcn_targ.pcn_name, "..") == 0) {
|
|
r = EINVAL;
|
|
}
|
|
|
|
/* Some tests apply only if the new path exists. */
|
|
if (new_ip == NULL) {
|
|
if (odir && (new_dirp->pn_va.va_nlink >= SHRT_MAX ||
|
|
new_dirp->pn_va.va_nlink >= LINK_MAX) &&
|
|
!same_pdir && r == OK) {
|
|
r = EMLINK;
|
|
}
|
|
} else {
|
|
if (old_ip == new_ip) r = SAME; /* old=new */
|
|
|
|
/* dir ? */
|
|
ndir = ((new_ip->pn_va.va_mode & I_TYPE) == I_DIRECTORY);
|
|
if (odir == TRUE && ndir == FALSE) r = ENOTDIR;
|
|
if (odir == FALSE && ndir == TRUE) r = EISDIR;
|
|
}
|
|
|
|
if (r == SAME) {
|
|
r = OK;
|
|
goto rename_out;
|
|
}
|
|
|
|
if (r != OK) return(r);
|
|
|
|
/* If a process has another root directory than the system root, we might
|
|
* "accidently" be moving it's working directory to a place where it's
|
|
* root directory isn't a super directory of it anymore. This can make
|
|
* the function chroot useless. If chroot will be used often we should
|
|
* probably check for it here. */
|
|
|
|
/* The rename will probably work. Only two things can go wrong now:
|
|
* 1. being unable to remove the new file. (when new file already exists)
|
|
* 2. being unable to make the new directory entry. (new file doesn't exists)
|
|
* [directory has to grow by one block and cannot because the disk
|
|
* is completely full].
|
|
* 3. Something (doubtfully) else depending on the FS.
|
|
*/
|
|
|
|
if (buildpath) {
|
|
pcn_src.pcn_po_full = old_ip->pn_po;
|
|
|
|
if (puffs_path_pcnbuild(global_pu, &pcn_targ, new_dirp) != 0)
|
|
return(EINVAL);
|
|
}
|
|
|
|
r = global_pu->pu_ops.puffs_node_rename(global_pu, old_dirp, old_ip, &pcn_src,
|
|
new_dirp, new_ip, &pcn_targ);
|
|
if (r > 0) r = -r;
|
|
|
|
if (buildpath) {
|
|
if (r) {
|
|
global_pu->pu_pathfree(global_pu, &pcn_targ.pcn_po_full);
|
|
} else {
|
|
struct puffs_pathinfo pi;
|
|
struct puffs_pathobj po_old;
|
|
|
|
/* handle this node */
|
|
po_old = old_ip->pn_po;
|
|
old_ip->pn_po = pcn_targ.pcn_po_full;
|
|
|
|
if (old_ip->pn_va.va_type != VDIR) {
|
|
global_pu->pu_pathfree(global_pu, &po_old);
|
|
return(OK);
|
|
}
|
|
|
|
/* handle all child nodes for DIRs */
|
|
pi.pi_old = &pcn_src.pcn_po_full;
|
|
pi.pi_new = &pcn_targ.pcn_po_full;
|
|
|
|
PU_LOCK();
|
|
if (puffs_pn_nodewalk(global_pu, puffs_path_prefixadj, &pi)
|
|
!= NULL) {
|
|
/* Actually nomem */
|
|
return(EINVAL);
|
|
}
|
|
PU_UNLOCK();
|
|
global_pu->pu_pathfree(global_pu, &po_old);
|
|
}
|
|
}
|
|
|
|
rename_out:
|
|
cur_time = clock_time();
|
|
update_times(old_dirp, MTIME | CTIME, cur_time);
|
|
update_times(new_dirp, MTIME | CTIME, cur_time);
|
|
|
|
/* XXX see release_node comment in fs_unlink */
|
|
if (new_ip && new_ip->pn_count == 0) {
|
|
release_node(global_pu, new_ip);
|
|
}
|
|
|
|
return(r);
|
|
}
|
|
|
|
FORWARD _PROTOTYPE( int remove_dir, (struct puffs_node *pn_dir,
|
|
struct puffs_node *pn, struct puffs_cn *pcn));
|
|
FORWARD _PROTOTYPE( int unlink_file, (struct puffs_node *dirp,
|
|
struct puffs_node *pn, struct puffs_cn *pcn));
|
|
|
|
/*===========================================================================*
|
|
* fs_unlink *
|
|
*===========================================================================*/
|
|
PUBLIC int fs_unlink()
|
|
{
|
|
/* Perform the unlink(name) or rmdir(name) system call. The code for these two
|
|
* is almost the same. They differ only in some condition testing. Unlink()
|
|
* may be used by the superuser to do dangerous things; rmdir() may not.
|
|
*/
|
|
int r;
|
|
struct puffs_node *pn, *pn_dir;
|
|
time_t cur_time;
|
|
struct puffs_kcn pkcnp;
|
|
struct puffs_cn pcn = {&pkcnp, 0, {0}};
|
|
PUFFS_KCREDTOCRED(pcn.pcn_cred, &global_kcred);
|
|
int len;
|
|
|
|
/* Copy the last component */
|
|
len = fs_m_in.REQ_PATH_LEN;
|
|
pcn.pcn_namelen = len - 1;
|
|
if (pcn.pcn_namelen > NAME_MAX)
|
|
return(ENAMETOOLONG);
|
|
|
|
r = sys_safecopyfrom(VFS_PROC_NR, (cp_grant_id_t) fs_m_in.REQ_GRANT,
|
|
(vir_bytes) 0, (vir_bytes) pcn.pcn_name,
|
|
(size_t) len, D);
|
|
if (r != OK) return (r);
|
|
NUL(pcn.pcn_name, len, sizeof(pcn.pcn_name));
|
|
|
|
if ((pn_dir = puffs_pn_nodewalk(global_pu, 0, &fs_m_in.REQ_INODE_NR)) == NULL)
|
|
return(EINVAL);
|
|
|
|
/* The last directory exists. Does the file also exist? */
|
|
pn = advance(pn_dir, pcn.pcn_name, IGN_PERM);
|
|
r = err_code;
|
|
|
|
/* If error, return pnode. */
|
|
if (r != OK) {
|
|
/* Mount point? */
|
|
if (r == EENTERMOUNT || r == ELEAVEMOUNT) {
|
|
r = EBUSY;
|
|
}
|
|
return(r);
|
|
}
|
|
|
|
/* Now test if the call is allowed, separately for unlink() and rmdir(). */
|
|
if (fs_m_in.m_type == REQ_UNLINK) {
|
|
/* Only the su may unlink directories, but the su can unlink any dir */
|
|
if ((pn->pn_va.va_mode & I_TYPE) == I_DIRECTORY)
|
|
r = EPERM;
|
|
if (r == OK)
|
|
r = unlink_file(pn_dir, pn, &pcn);
|
|
} else {
|
|
r = remove_dir(pn_dir, pn, &pcn); /* call is RMDIR */
|
|
}
|
|
|
|
if (pn->pn_va.va_nlink != 0) {
|
|
cur_time = clock_time();
|
|
update_times(pn, CTIME, cur_time);
|
|
update_times(pn_dir, MTIME | CTIME, cur_time);
|
|
}
|
|
|
|
/* XXX Ideally, we should check pn->pn_flags & PUFFS_NODE_REMOVED, but
|
|
* librefuse doesn't set it (neither manually or via puffs_pn_remove() ).
|
|
* Thus we just check that "pn_count == 0". Otherwise release_node()
|
|
* will be called in fs_put().
|
|
*/
|
|
if (pn->pn_count == 0)
|
|
release_node(global_pu, pn);
|
|
|
|
return(r);
|
|
}
|
|
|
|
|
|
/*===========================================================================*
|
|
* remove_dir *
|
|
*===========================================================================*/
|
|
PRIVATE int remove_dir(pn_dir, pn, pcn)
|
|
struct puffs_node *pn_dir; /* parent directory */
|
|
struct puffs_node *pn; /* directory to be removed */
|
|
struct puffs_cn *pcn; /* Name, creads of directory */
|
|
{
|
|
/* A directory file has to be removed. Five conditions have to met:
|
|
* - The file must be a directory
|
|
* - The directory must be empty (except for . and ..)
|
|
* - The final component of the path must not be . or ..
|
|
* - The directory must not be the root of a mounted file system (VFS)
|
|
* - The directory must not be anybody's root/working directory (VFS)
|
|
*/
|
|
|
|
/* "." and ".." dentries can be stored in 28 bytes */
|
|
#define EMPTY_DIR_DENTRIES_SIZE 28
|
|
int r;
|
|
char remove_dir_buf[EMPTY_DIR_DENTRIES_SIZE];
|
|
struct dirent *dent = (struct dirent*) remove_dir_buf;
|
|
int buf_left = EMPTY_DIR_DENTRIES_SIZE;
|
|
off_t pos = 0;
|
|
int eofflag = 0;
|
|
|
|
if (global_pu->pu_ops.puffs_node_rmdir == NULL)
|
|
return(EINVAL);
|
|
|
|
if (!S_ISDIR(pn->pn_va.va_mode))
|
|
return(ENOTDIR);
|
|
|
|
/* Check if directory is empty */
|
|
r = global_pu->pu_ops.puffs_node_readdir(global_pu, pn, dent, &pos,
|
|
(size_t *)&buf_left, pcn->pcn_cred, &eofflag, 0, 0);
|
|
if (r) return(EINVAL);
|
|
if (!eofflag) return(ENOTEMPTY);
|
|
|
|
if (strcmp(pcn->pcn_name, ".") == 0 || strcmp(pcn->pcn_name, "..") == 0)
|
|
return(EINVAL);
|
|
|
|
if (pn->pn_va.va_fileid == global_pu->pu_pn_root->pn_va.va_fileid)
|
|
return(EBUSY); /* can't remove 'root' */
|
|
|
|
if (buildpath) {
|
|
r = puffs_path_pcnbuild(global_pu, pcn, pn_dir);
|
|
if (r) return(EINVAL);
|
|
}
|
|
|
|
r = global_pu->pu_ops.puffs_node_rmdir(global_pu, pn_dir, pn, pcn);
|
|
|
|
global_pu->pu_pathfree(global_pu, &pcn->pcn_po_full);
|
|
|
|
if (r) return(EINVAL);
|
|
|
|
return(OK);
|
|
}
|
|
|
|
|
|
/*===========================================================================*
|
|
* unlink_file *
|
|
*===========================================================================*/
|
|
PRIVATE int unlink_file(dirp, pn, pcn)
|
|
struct puffs_node *dirp; /* parent directory of file */
|
|
struct puffs_node *pn; /* pnode of file, may be NULL too. */
|
|
struct puffs_cn *pcn; /* Name, creads of file */
|
|
{
|
|
/* Unlink 'file_name'; pn must be the pnode of 'file_name' */
|
|
int r;
|
|
|
|
assert(pn != NULL);
|
|
|
|
if (global_pu->pu_ops.puffs_node_remove == NULL)
|
|
return(EINVAL);
|
|
|
|
if (S_ISDIR(pn->pn_va.va_mode))
|
|
return(EINVAL);
|
|
|
|
if (buildpath) {
|
|
r = puffs_path_pcnbuild(global_pu, pcn, dirp);
|
|
if (r)
|
|
return(EINVAL);
|
|
}
|
|
|
|
r = global_pu->pu_ops.puffs_node_remove(global_pu, dirp, pn, pcn);
|
|
|
|
global_pu->pu_pathfree(global_pu, &pcn->pcn_po_full);
|
|
|
|
if (r) return(EINVAL);
|
|
|
|
return(OK);
|
|
}
|