Skip to content

Instantly share code, notes, and snippets.

@derekschrock
Created January 28, 2020 01:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derekschrock/2775cb850f96c981d9e55580132781da to your computer and use it in GitHub Desktop.
Save derekschrock/2775cb850f96c981d9e55580132781da to your computer and use it in GitHub Desktop.
/**
* xrdp: A Remote Desktop Protocol server.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file implements the interface in chansrv_fuse_fs.h
*/
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#if defined(HAVE_CONFIG_H)
#include <config_ac.h>
#endif
#include "os_calls.h"
#include "chansrv_xfs.h"
/*
* Skip this module if FUSE is not supported. A standards-compliant C
* translation unit must contain at least one declaration (C99:6.9), and we've
* fulfilled that requirement by this stage.
*/
#ifdef XRDP_FUSE
#define INODE_TABLE_ALLOCATION_INITIAL 4096
#define INODE_TABLE_ALLOCATION_GRANULARITY 100
/* inum of the delete pending directory */
#define DELETE_PENDING_ID 2
/*
* A double-linked list of inodes, sorted by inum
*
* The elements in the list are sorted in increasing inum order, as this
* allows a directory enumeration to be easily resumed if elements
* are removed or added. See xfs_readdir() for details on this.
*/
typedef struct xfs_inode_all XFS_INODE_ALL;
typedef struct xfs_list
{
XFS_INODE_ALL *begin;
XFS_INODE_ALL *end;
} XFS_LIST;
/*
* A complete inode, including the private elements used by the
* implementation
*/
typedef struct xfs_inode_all
{
XFS_INODE pub; /* Public elements */
/*
* Directory linkage elements
*
* Because we don't support hard-linking, elements can be stored in
* one and only one directory:-
*/
struct xfs_inode_all *parent; /* Parent inode */
struct xfs_inode_all *next; /* Next entry in parent */
struct xfs_inode_all *previous; /* Previous entry in parent */
XFS_LIST dir; /* Directory only - children */
/*
* Other private elements
*/
unsigned int open_count; /* Regular files only */
} XFS_INODE_ALL;
/* the xrdp file system in memory
*
* inode_table allows for O(1) access to any file based on the inum.
* Index 0 is unused, so we can use an inode of zero for
* an invalid inode, and avoid off-by-one errors index
* 1 is our '.' directory.
* 2 is the delete pending directory, where we can place
* inodes with a positive open count which are
* deleted.
* free_list List of free inode numbers. Allows for O(1) access to
* a free node, provided the free list is not empty.
*/
struct xfs_fs
{
XFS_INODE_ALL **inode_table; /* a table of entries; can grow. */
fuse_ino_t *free_list; /* Free inodes */
unsigned int inode_count; /* Current number of inodes */
unsigned int free_count; /* Size of free_list */
unsigned int generation; /* Changes when an inode is deleted */
};
/* A directory handle
*
* inum inum of the directory being scanned
* generation Generation of the inum we opened
*/
struct xfs_dir_handle
{
fuse_ino_t inum;
tui32 generation;
};
/* module based logging */
#define LOG_ERROR 0
#define LOG_INFO 1
#define LOG_DEBUG 2
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_ERROR
#endif
#define log_error(_params...) \
{ \
g_write("[%10.10u]: XFS %s: %d : ERROR: ", \
g_time3(), __func__, __LINE__); \
g_writeln (_params); \
}
#define log_always(_params...) \
{ \
g_write("[%10.10u]: XFS %s: %d : ALWAYS: ", \
g_time3(), __func__, __LINE__); \
g_writeln (_params); \
}
#define log_info(_params...) \
{ \
if (LOG_INFO <= LOG_LEVEL) \
{ \
g_write("[%10.10u]: XFS %s: %d : ", \
g_time3(), __func__, __LINE__); \
g_writeln (_params); \
} \
}
#define log_debug(_params...) \
{ \
if (LOG_DEBUG <= LOG_LEVEL) \
{ \
g_write("[%10.10u]: XFS %s: %d : ", \
g_time3(), __func__, __LINE__); \
g_writeln (_params); \
} \
}
/* ------------------------------------------------------------------------ */
static int
grow_xfs(struct xfs_fs *xfs, unsigned int extra_inodes)
{
int result = 0;
unsigned int new_count = xfs->inode_count + extra_inodes;
XFS_INODE_ALL **new_table;
fuse_ino_t *new_free_list;
new_table = (XFS_INODE_ALL **)
realloc(xfs->inode_table, new_count * sizeof(new_table[0]));
if (new_table != NULL)
{
unsigned int i;
for (i = xfs->inode_count ; i < new_count ; ++i)
{
new_table[i] = NULL;
}
xfs->inode_table = new_table;
new_free_list = (fuse_ino_t *)
realloc(xfs->free_list,
new_count * sizeof(new_free_list[0]));
if (new_free_list)
{
/* Add the new inodes in to the new_free_list, so the lowest
* number is allocated first
*/
i = new_count;
while (i > xfs->inode_count)
{
new_free_list[xfs->free_count++] = --i;
}
xfs->free_list = new_free_list;
xfs->inode_count = new_count;
result = 1;
}
}
return result;
}
/* ------------------------------------------------------------------------ */
static void
add_inode_to_list(XFS_LIST *list, XFS_INODE_ALL *xino)
{
fuse_ino_t inum = xino->pub.inum;
/* Find the element we need to insert after */
XFS_INODE_ALL *predecessor = list->end;
while (predecessor != NULL && predecessor->pub.inum > inum)
{
predecessor = predecessor->previous;
}
if (predecessor == NULL)
{
/* Inserting at the beginning */
/* Set up links in node */
xino->next = list->begin;
xino->previous = NULL;
/* Set up back-link to node */
if (list->begin == NULL)
{
/* We are the last node */
list->end = xino;
}
else
{
list->begin->previous = xino;
}
/* Set up forward-link to node */
list->begin = xino;
}
else
{
/* Set up links in node */
xino->next = predecessor->next;
xino->previous = predecessor;
/* Set up back-link to node */
if (predecessor->next == NULL)
{
list->end = xino;
}
else
{
predecessor->next->previous = xino;
}
/* Set up forward-link to node */
predecessor->next = xino;
}
}
/* ------------------------------------------------------------------------ */
static void
remove_inode_from_list(XFS_LIST *list, XFS_INODE_ALL *xino)
{
if (xino->previous == NULL)
{
/* First element */
list->begin = xino->next;
}
else
{
xino->previous->next = xino->next;
}
if (xino->next == NULL)
{
/* Last element */
list->end = xino->previous;
}
else
{
xino->next->previous = xino->previous;
}
}
/* ------------------------------------------------------------------------ */
static void
link_inode_into_directory_node(XFS_INODE_ALL *dinode, XFS_INODE_ALL *xino)
{
xino->parent = dinode;
add_inode_to_list(&dinode->dir, xino);
}
/* ------------------------------------------------------------------------ */
static void
unlink_inode_from_parent(XFS_INODE_ALL *xino)
{
remove_inode_from_list(&xino->parent->dir, xino);
xino->next = NULL;
xino->previous = NULL;
xino->parent = NULL;
}
/* ------------------------------------------------------------------------ */
struct xfs_fs *
xfs_create_xfs_fs(mode_t umask, uid_t uid, gid_t gid)
{
struct xfs_fs *xfs = g_new0(struct xfs_fs, 1);
XFS_INODE_ALL *xino1 = NULL;
XFS_INODE_ALL *xino2 = NULL;
if (xfs != NULL)
{
xfs->inode_count = 0;
xfs->free_count = 0;
xfs->inode_table = NULL;
xfs->free_list = NULL;
xfs->generation = 1;
if (!grow_xfs(xfs, INODE_TABLE_ALLOCATION_INITIAL) ||
(xino1 = g_new0(XFS_INODE_ALL, 1)) == NULL ||
(xino2 = g_new0(XFS_INODE_ALL, 1)) == NULL)
{
free(xino1);
free(xino2);
xfs_delete_xfs_fs(xfs);
xfs = NULL;
}
else
{
/*
* The use of grow_xfs to allocate the inode table will make
* inodes 0, 1 (FUSE_ROOT_ID) and 2 (DELETE_PENDING_ID) the first
* available free inodes. We can ignore these */
xfs->free_count -= 3;
xfs->inode_table[0] = NULL;
xfs->inode_table[FUSE_ROOT_ID] = xino1;
xfs->inode_table[DELETE_PENDING_ID] = xino2;
xino1->pub.inum = FUSE_ROOT_ID;
xino1->pub.mode = (S_IFDIR | 0777) & ~umask;
xino1->pub.uid = uid;
xino1->pub.gid = gid;
xino1->pub.size = 0;
xino1->pub.atime = time(0);
xino1->pub.mtime = xino1->pub.atime;
xino1->pub.ctime = xino1->pub.atime;
strcpy(xino1->pub.name, ".");
xino1->pub.generation = xfs->generation;
xino1->pub.device_id = 0;
/*
* FUSE_ROOT_ID has no parent rather than being a parent
* of itself. This is intentional */
xino1->parent = NULL;
xino1->next = NULL;
xino1->previous = NULL;
xino1->dir.begin = NULL;
xino1->dir.end = NULL;
xino2->pub.inum = DELETE_PENDING_ID;
xino2->pub.mode = (S_IFDIR | 0777) & ~umask;
xino2->pub.uid = uid;
xino2->pub.gid = gid;
xino2->pub.size = 0;
xino2->pub.atime = time(0);
xino2->pub.mtime = xino2->pub.atime;
xino2->pub.ctime = xino2->pub.atime;
strcpy(xino2->pub.name, ".delete-pending");
xino2->pub.generation = xfs->generation;
xino2->pub.device_id = 0;
xino2->parent = NULL;
xino2->next = NULL;
xino2->previous = NULL;
xino2->dir.begin = NULL;
xino2->dir.end = NULL;
/*
* Uncomment this line to make the .delete-pending
* directory visible to the user in the root
*/
/* link_inode_into_directory_node(xino1, xino2); */
}
}
return xfs;
}
/* ------------------------------------------------------------------------ */
void
xfs_delete_xfs_fs(struct xfs_fs *xfs)
{
if (xfs != NULL)
{
if (xfs->inode_table != NULL)
{
size_t i;
for (i = 0 ; i < xfs->inode_count; ++i)
{
free(xfs->jnode_table[i]);
}
free(xfs->inode_table);
}
free(xfs->free_list);
free(xfs);
}
}
/* ------------------------------------------------------------------------ */
XFS_INODE *
xfs_add_entry(struct xfs_fs *xfs, fuse_ino_t parent_inum,
const char *name, mode_t mode)
{
XFS_INODE *result = NULL;
XFS_INODE_ALL *parent = NULL;
/* Checks:-
* 1) the parent exists (and is a directory)
* 2) the caller is not inserting into the .delete-pending directory,
* 3) Name's not too long
* 4) Entry does not already exist
*/
if (parent_inum < xfs->inode_count &&
((parent = xfs->inode_table[parent_inum]) != NULL) &&
(parent->pub.mode & S_IFDIR) != 0 &&
parent_inum != DELETE_PENDING_ID &&
strlen(name) <= XFS_MAXFILENAMELEN &&
!xfs_lookup_in_dir(xfs, parent_inum, name))
{
/* Sanitise the mode so one-and-only-one of S_IFDIR and
* S_IFREG is set */
if ((mode & S_IFDIR) != 0)
{
mode = (mode & 0777) | S_IFDIR;
}
else
{
mode = (mode & 0777) | S_IFREG;
}
/* Space for a new entry? */
if (xfs->free_count > 0 ||
grow_xfs(xfs, INODE_TABLE_ALLOCATION_GRANULARITY))
{
XFS_INODE_ALL *xino = NULL;
if ((xino = g_new0(XFS_INODE_ALL, 1)) != NULL)
{
fuse_ino_t inum = xfs->free_list[--xfs->free_count];
if (xfs->inode_table[inum] != NULL)
{
log_error("Unexpected non-NULL value in inode table "
"entry %ld", inum);
}
xfs->inode_table[inum] = xino;
xino->pub.inum = inum;
xino->pub.mode = mode;
xino->pub.uid = parent->pub.uid;
xino->pub.gid = parent->pub.gid;
if (mode & S_IFDIR)
{
xino->pub.size = 4096;
}
else
{
xino->pub.size = 0;
}
xino->pub.atime = time(0);
xino->pub.mtime = xino->pub.atime;
xino->pub.ctime = xino->pub.atime;
strcpy(xino->pub.name, name);
xino->pub.generation = xfs->generation;
xino->pub.device_id = parent->pub.device_id;
xino->pub.lindex = 0;
xino->parent = NULL;
xino->next = NULL;
xino->previous = NULL;
link_inode_into_directory_node(parent, xino);
result = &xino->pub;
}
}
}
return result;
}
/* ------------------------------------------------------------------------ */
void
xfs_remove_directory_contents(struct xfs_fs *xfs, fuse_ino_t inum)
{
XFS_INODE_ALL *xino = NULL;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
((xino->pub.mode & S_IFDIR) != 0))
{
XFS_INODE_ALL *e;
while ((e = xino->dir.end) != NULL)
{
xfs_remove_entry(xfs, e->pub.inum);
}
}
}
/* ------------------------------------------------------------------------ */
void
xfs_remove_entry(struct xfs_fs *xfs, fuse_ino_t inum)
{
XFS_INODE_ALL *xino = NULL;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL))
{
if ((xino->pub.mode & S_IFDIR) != 0)
{
xfs_remove_directory_contents(xfs, inum);
}
unlink_inode_from_parent(xino);
if ((xino->pub.mode & S_IFREG) != 0 && xino->open_count > 0)
{
link_inode_into_directory_node(
xfs->inode_table[DELETE_PENDING_ID], xino);
}
else
{
xfs->free_list[xfs->free_count++] = inum;
xfs->inode_table[inum] = NULL;
/*
* Bump the generation when we return an inum to the free list,
* so that the caller can distinguish re-uses of the same inum.
*/
++xfs->generation;
free(xino);
}
}
}
/* ------------------------------------------------------------------------ */
XFS_INODE *
xfs_get(struct xfs_fs *xfs, fuse_ino_t inum)
{
return (inum < xfs->inode_count) ? &xfs->inode_table[inum]->pub : NULL;
}
/* ------------------------------------------------------------------------ */
char *
xfs_get_full_path(struct xfs_fs *xfs, fuse_ino_t inum)
{
char *result = NULL;
XFS_INODE_ALL *xino = NULL;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL))
{
if (xino->pub.inum == FUSE_ROOT_ID)
{
return strdup("/");
}
else
{
/*
* Add up the lengths of all the names up to the root,
* allowing one extra char for a '/' prefix for each element
*/
size_t len = 0;
XFS_INODE_ALL *p;
for (p = xino ; p->pub.inum != FUSE_ROOT_ID ; p = p->parent)
{
len += strlen(p->pub.name);
++len; /* Allow for '/' prefix */
}
result = (char *) malloc(len + 1);
if (result != NULL)
{
/* Construct the path from the end */
char *end = result + len;
*end = '\0';
for (p = xino ; p->pub.inum != FUSE_ROOT_ID ; p = p->parent)
{
len = strlen(p->pub.name);
end -= (len + 1);
*end = '/';
memcpy(end + 1, p->pub.name, len);
}
}
}
}
return result;
}
/* ------------------------------------------------------------------------ */
XFS_INODE *
xfs_lookup_in_dir(struct xfs_fs *xfs, fuse_ino_t inum, const char *name)
{
XFS_INODE_ALL *xino;
XFS_INODE *result = NULL;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
(xino->pub.mode & S_IFDIR) != 0)
{
XFS_INODE_ALL *p;
for (p = xino->dir.begin ; p != NULL; p = p->next)
{
if (strcmp(p->pub.name, name) == 0)
{
result = &p->pub;
break;
}
}
}
return result;
}
/* ------------------------------------------------------------------------ */
int
xfs_is_dir_empty(struct xfs_fs *xfs, fuse_ino_t inum)
{
XFS_INODE_ALL *xino = NULL;
int result = 0;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
(xino->pub.mode & S_IFDIR) != 0)
{
result = (xino->dir.begin == NULL);
}
return result;
}
/* ------------------------------------------------------------------------ */
unsigned int
xfs_is_under(struct xfs_fs *xfs, fuse_ino_t dir, fuse_ino_t entry)
{
unsigned int result = 0;
XFS_INODE_ALL *dxino = NULL;
XFS_INODE_ALL *exino = NULL;
if (dir < xfs->inode_count &&
((dxino = xfs->inode_table[dir]) != NULL) &&
(dxino->pub.mode & S_IFDIR) != 0 &&
entry < xfs->inode_count &&
((exino = xfs->inode_table[entry]) != NULL))
{
unsigned int count = 0;
while (exino != NULL && exino != dxino)
{
++count;
exino = exino->parent;
}
if (exino != NULL)
{
result = count;
}
}
return result;
}
/* ------------------------------------------------------------------------ */
struct xfs_dir_handle *
xfs_opendir(struct xfs_fs *xfs, fuse_ino_t dir)
{
XFS_INODE_ALL *xino = NULL;
struct xfs_dir_handle *result = NULL;
if (dir < xfs->inode_count &&
((xino = xfs->inode_table[dir]) != NULL) &&
(xino->pub.mode & S_IFDIR) != 0)
{
result = g_new0(struct xfs_dir_handle, 1);
if (result)
{
result->inum = xino->pub.inum;
result->generation = xino->pub.generation;
}
}
return result;
}
/* ------------------------------------------------------------------------ */
XFS_INODE *
xfs_readdir(struct xfs_fs *xfs, struct xfs_dir_handle *handle, off_t *off)
{
XFS_INODE_ALL *result = NULL;
XFS_INODE_ALL *dxino = NULL;
XFS_INODE_ALL *xino = NULL;
/* Check the direcory is still valid */
if (handle->inum < xfs->inode_count &&
((dxino = xfs->inode_table[handle->inum]) != NULL) &&
(dxino->pub.mode & S_IFDIR) != 0 &&
handle->generation == dxino->pub.generation)
{
fuse_ino_t inum;
if (*off == (off_t) -1)
{
/* We're at the end already */
}
else if ((inum = *off) == 0)
{
/* First call */
result = dxino->dir.begin;
}
else if (inum < xfs->inode_count &&
(xino = xfs->inode_table[inum]) != 0 &&
xino->parent == dxino)
{
/* The node we're pointing to is still valid */
result = xino;
}
else
{
/*
* The file we wanted has been pulled out from under us.
* We will look forward in the inode table to try to
* discover the next inode in the directory. Because
* files are stored in inode order, this guarantees
* we'll meet POSIX requirements.
*/
for (inum = inum + 1 ; inum < xfs->inode_count ; ++inum)
{
if ((xino = xfs->inode_table[inum]) != 0 &&
xino->parent == dxino)
{
result = xino;
break;
}
}
}
}
/* Update the offset */
if (result == NULL || result->next == NULL)
{
/* We're done */
*off = (off_t) -1;
}
else
{
*off = (off_t)result->next->pub.inum;
}
/* Caller only sees public interface to the result */
return (result) ? &result->pub : NULL;
}
/* ------------------------------------------------------------------------ */
void
xfs_closedir(struct xfs_fs *xfs, struct xfs_dir_handle *handle)
{
free(handle);
}
/* ------------------------------------------------------------------------ */
void
xfs_increment_file_open_count(struct xfs_fs *xfs, fuse_ino_t inum)
{
XFS_INODE_ALL *xino;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
(xino->pub.mode & S_IFREG) != 0)
{
++xino->open_count;
}
}
/* ------------------------------------------------------------------------ */
void
xfs_decrement_file_open_count(struct xfs_fs *xfs, fuse_ino_t inum)
{
XFS_INODE_ALL *xino;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
(xino->pub.mode & S_IFREG) != 0)
{
if (xino->open_count > 0)
{
--xino->open_count;
}
if (xino->open_count == 0 &&
xino->parent == xfs->inode_table[DELETE_PENDING_ID])
{
/* We can get rid of this one now */
xfs_remove_entry(xfs, inum);
}
}
}
/* ------------------------------------------------------------------------ */
unsigned int
xfs_get_file_open_count(struct xfs_fs *xfs, fuse_ino_t inum)
{
unsigned int result = 0;
XFS_INODE_ALL *xino;
if (inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
(xino->pub.mode & S_IFREG) != 0)
{
result = xino->open_count;
}
return result;
}
/* ------------------------------------------------------------------------ */
void
xfs_delete_entries_with_device_id(struct xfs_fs *xfs, tui32 device_id)
{
fuse_ino_t inum;
XFS_INODE_ALL *xino;
if (device_id != 0)
{
/* Using xfs_remove_entry() is convenient, but it recurses
* in to directories. To make sure all entries are removed, set the
* open_count of all affected files to 0 first
*/
for (inum = FUSE_ROOT_ID; inum < xfs->inode_count; ++inum)
{
if ((xino = xfs->inode_table[inum]) != NULL &&
xino->pub.device_id == device_id &&
(xino->pub.mode & S_IFREG) != 0)
{
xino->open_count = 0;
}
}
/* Now we can be sure everything will be deleted correctly */
for (inum = FUSE_ROOT_ID; inum < xfs->inode_count; ++inum)
{
if ((xino = xfs->inode_table[inum]) != NULL &&
xino->pub.device_id == device_id)
{
xfs_remove_entry(xfs, xino->pub.inum);
}
}
}
}
/* ------------------------------------------------------------------------ */
int
xfs_check_move_entry(struct xfs_fs *xfs, fuse_ino_t inum,
fuse_ino_t new_parent_inum, const char *name)
{
XFS_INODE_ALL *xino;
XFS_INODE_ALL *parent;
return
(strlen(name) <= XFS_MAXFILENAMELEN &&
inum < xfs->inode_count &&
((xino = xfs->inode_table[inum]) != NULL) &&
new_parent_inum != DELETE_PENDING_ID &&
new_parent_inum < xfs->inode_count &&
((parent = xfs->inode_table[new_parent_inum]) != NULL) &&
(parent->pub.mode & S_IFDIR) != 0 &&
xfs_is_under(xfs, inum, new_parent_inum) == 0);
}
/* ------------------------------------------------------------------------ */
int
xfs_move_entry(struct xfs_fs *xfs, fuse_ino_t inum,
fuse_ino_t new_parent_inum, const char *name)
{
int result = EINVAL;
XFS_INODE_ALL *xino;
XFS_INODE_ALL *parent;
XFS_INODE *dest;
if (xfs_check_move_entry(xfs, inum, new_parent_inum, name))
{
xino = xfs->inode_table[inum];
parent = xfs->inode_table[new_parent_inum];
if (xino->parent != parent)
{
/* We're moving between directories */
/* Does the target name already exist in the destination? */
if ((dest = xfs_lookup_in_dir(xfs, new_parent_inum, name)) != NULL)
{
xfs_remove_entry(xfs, dest->inum);
}
unlink_inode_from_parent(xino);
link_inode_into_directory_node(parent, xino);
strcpy(xino->pub.name, name);
}
else if (strcmp(xino->pub.name, name) != 0)
{
/* Same directory, but name has changed */
if ((dest = xfs_lookup_in_dir(xfs, new_parent_inum, name)) != NULL)
{
xfs_remove_entry(xfs, dest->inum);
}
strcpy(xino->pub.name, name);
}
result = 0;
}
return result;
}
#endif /* XRDP_FUSE */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment