From 6bb60661aa20e6aa4a6a6c2244a3fb7df6cf2c4d Mon Sep 17 00:00:00 2001 From: Guus Sliepen Date: Mon, 11 Nov 2019 22:49:05 +0100 Subject: [PATCH] Add support for black/whitelisting by name, and forgetting nodes. --- src/conf.c | 21 ++++++ src/conf.h | 1 + src/meshlink++.h | 55 ++++++++++++++ src/meshlink.c | 184 ++++++++++++++++++++++++++++++++++++++--------- src/meshlink.h | 53 ++++++++++++++ src/meshlink.sym | 3 + test/Makefile.am | 5 ++ test/blacklist.c | 180 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 test/blacklist.c diff --git a/src/conf.c b/src/conf.c index 9a138712..212b7c4a 100644 --- a/src/conf.c +++ b/src/conf.c @@ -736,6 +736,27 @@ bool config_write(meshlink_handle_t *mesh, const char *conf_subdir, const char * return true; } +/// Delete a host configuration file. +bool config_delete(meshlink_handle_t *mesh, const char *conf_subdir, const char *name) { + assert(conf_subdir); + assert(name); + + if(!mesh->confbase) { + return true; + } + + char path[PATH_MAX]; + make_host_path(mesh, conf_subdir, name, path, sizeof(path)); + + if(unlink(path) && errno != ENOENT) { + logger(mesh, MESHLINK_ERROR, "Failed to unlink `%s': %s", path, strerror(errno)); + meshlink_errno = MESHLINK_ESTORAGE; + return false; + } + + return true; +} + /// Read the main configuration file. bool main_config_read(meshlink_handle_t *mesh, const char *conf_subdir, config_t *config, void *key) { assert(conf_subdir); diff --git a/src/conf.h b/src/conf.h index f3d1e358..2f79834a 100644 --- a/src/conf.h +++ b/src/conf.h @@ -51,6 +51,7 @@ extern bool main_config_write(struct meshlink_handle *mesh, const char *conf_sub extern bool config_exists(struct meshlink_handle *mesh, const char *conf_subdir, const char *name) __attribute__((__warn_unused_result__)); extern bool config_read(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, struct config_t *, void *key) __attribute__((__warn_unused_result__)); extern bool config_write(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, const struct config_t *, void *key) __attribute__((__warn_unused_result__)); +extern bool config_delete(struct meshlink_handle *mesh, const char *conf_subdir, const char *name) __attribute__((__warn_unused_result__)); extern bool config_scan_all(struct meshlink_handle *mesh, const char *conf_subdir, const char *conf_type, config_scan_action_t action, void *arg) __attribute__((__warn_unused_result__)); extern bool invitation_read(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, struct config_t *, void *key) __attribute__((__warn_unused_result__)); diff --git a/src/meshlink++.h b/src/meshlink++.h index 980e5b12..74ec06e5 100644 --- a/src/meshlink++.h +++ b/src/meshlink++.h @@ -658,6 +658,29 @@ public: return meshlink_import(handle, data); } + /// Forget any information about a node. + /** This function allows the local node to forget any information it has about a node, + * and if possible will remove any data it has stored on disk about the node. + * + * Any open channels to this node must be closed before calling this function. + * + * After this call returns, the node handle is invalid and may no longer be used, regardless + * of the return value of this call. + * + * Note that this function does not prevent MeshLink from actually forgetting about a node, + * or re-learning information about a node at a later point in time. It is merely a hint that + * the application does not care about this node anymore and that any resources kept could be + * cleaned up. + * + * \memberof meshlink_node + * @param node A pointer to a struct meshlink_node describing the node to be forgotten. + * + * @return This function returns true if all currently known data about the node has been forgotten, false otherwise. + */ + bool forget_node(node *node) { + return meshlink_forget_node(handle, node); + } + /// Blacklist a node from the mesh. /** This function causes the local node to blacklist another node. * The local node will drop any existing connections to that node, @@ -671,6 +694,21 @@ public: return meshlink_blacklist(handle, node); } + /// Blacklist a node from the mesh by name. + /** This function causes the local node to blacklist another node by name. + * The local node will drop any existing connections to that node, + * and will not send data to it nor accept any data received from it any more. + * + * If no node by the given name is known, it is created. + * + * @param name The name of the node to blacklist. + * + * @return This function returns true if the node has been blacklisted, false otherwise. + */ + bool blacklist_by_name(const char *name) { + return meshlink_blacklist_by_name(handle, name); + } + /// Whitelist a node on the mesh. /** This function causes the local node to whitelist another node. * The local node will allow connections to and from that node, @@ -684,6 +722,23 @@ public: return meshlink_whitelist(handle, node); } + /// Whitelist a node on the mesh by name. + /** This function causes the local node to whitelist a node by name. + * The local node will allow connections to and from that node, + * and will send data to it and accept any data received from it. + * + * If no node by the given name is known, it is created. + * This is useful if new nodes are blacklisted by default. + * + * \memberof meshlink_node + * @param node A pointer to a struct meshlink_node describing the node to be whitelisted. + * + * @return This function returns true if the node has been whitelisted, false otherwise. + */ + bool whitelist_by_name(const char *name) { + return meshlink_whitelist_by_name(handle, name); + } + /// Set the poll callback. /** This functions sets the callback that is called whenever data can be sent to another node. * The callback is run in MeshLink's own thread. diff --git a/src/meshlink.c b/src/meshlink.c index 75458fe6..57589512 100644 --- a/src/meshlink.c +++ b/src/meshlink.c @@ -1904,17 +1904,17 @@ meshlink_node_t *meshlink_get_node(meshlink_handle_t *mesh, const char *name) { return NULL; } - meshlink_node_t *node = NULL; + node_t *n = NULL; pthread_mutex_lock(&mesh->mutex); - node = (meshlink_node_t *)lookup_node(mesh, (char *)name); // TODO: make lookup_node() use const + n = lookup_node(mesh, (char *)name); // TODO: make lookup_node() use const pthread_mutex_unlock(&mesh->mutex); - if(!node) { + if(!n) { meshlink_errno = MESHLINK_ENOENT; } - return node; + return (meshlink_node_t *)n; } meshlink_submesh_t *meshlink_get_submesh(meshlink_handle_t *mesh, const char *name) { @@ -1975,8 +1975,8 @@ static meshlink_node_t **meshlink_get_all_nodes_by_condition(meshlink_handle_t * *nmemb = 0; for splay_each(node_t, n, mesh->nodes) { - if(true == search_node(n, condition)) { - *nmemb = *nmemb + 1; + if(search_node(n, condition)) { + ++*nmemb; } } @@ -1992,7 +1992,7 @@ static meshlink_node_t **meshlink_get_all_nodes_by_condition(meshlink_handle_t * meshlink_node_t **p = result; for splay_each(node_t, n, mesh->nodes) { - if(true == search_node(n, condition)) { + if(search_node(n, condition)) { *p++ = (meshlink_node_t *)n; } } @@ -2837,27 +2837,15 @@ bool meshlink_import(meshlink_handle_t *mesh, const char *data) { return true; } -bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) { - if(!mesh || !node) { - meshlink_errno = MESHLINK_EINVAL; - return false; - } - - pthread_mutex_lock(&mesh->mutex); - - node_t *n; - n = (node_t *)node; - +static bool blacklist(meshlink_handle_t *mesh, node_t *n) { if(n == mesh->self) { - logger(mesh, MESHLINK_ERROR, "%s blacklisting itself?\n", node->name); + logger(mesh, MESHLINK_ERROR, "%s blacklisting itself?\n", n->name); meshlink_errno = MESHLINK_EINVAL; - pthread_mutex_unlock(&mesh->mutex); return false; } if(n->status.blacklisted) { - logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", node->name); - pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", n->name); return true; } @@ -2880,38 +2868,71 @@ bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) { n->mtuprobes = 0; n->status.udp_confirmed = false; - if(!node_write_config(mesh, n)) { - pthread_mutex_unlock(&mesh->mutex); + /* Graph updates will suppress status updates for blacklisted nodes, so we need to + * manually call the status callback if necessary. + */ + if(n->status.reachable && mesh->node_status_cb) { + mesh->node_status_cb(mesh, (meshlink_node_t *)n, false); + } + + return node_write_config(mesh, n) && config_sync(mesh, "current"); +} + +bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) { + if(!mesh || !node) { + meshlink_errno = MESHLINK_EINVAL; return false; } - logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name); + pthread_mutex_lock(&mesh->mutex); + + if(!blacklist(mesh, (node_t *)node)) { + pthread_mutex_unlock(&mesh->mutex); + return false; + } pthread_mutex_unlock(&mesh->mutex); - return config_sync(mesh, "current"); + logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name); + return true; } -bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) { - if(!mesh || !node) { +bool meshlink_blacklist_by_name(meshlink_handle_t *mesh, const char *name) { + if(!mesh || !name) { meshlink_errno = MESHLINK_EINVAL; return false; } pthread_mutex_lock(&mesh->mutex); - node_t *n = (node_t *)node; + node_t *n = lookup_node(mesh, (char *)name); + + if(!n) { + n = new_node(); + n->name = xstrdup(name); + node_add(mesh, n); + } + + if(!blacklist(mesh, (node_t *)n)) { + pthread_mutex_unlock(&mesh->mutex); + return false; + } + + pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", name); + return true; +} + +static bool whitelist(meshlink_handle_t *mesh, node_t *n) { if(n == mesh->self) { - logger(mesh, MESHLINK_ERROR, "%s whitelisting itself?\n", node->name); + logger(mesh, MESHLINK_ERROR, "%s whitelisting itself?\n", n->name); meshlink_errno = MESHLINK_EINVAL; - pthread_mutex_unlock(&mesh->mutex); return false; } if(!n->status.blacklisted) { - logger(mesh, MESHLINK_DEBUG, "Node %s was already whitelisted\n", node->name); - pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_DEBUG, "Node %s was already whitelisted\n", n->name); return true; } @@ -2921,22 +2942,115 @@ bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) { update_node_status(mesh, n); } - if(!node_write_config(mesh, n)) { + return node_write_config(mesh, n) && config_sync(mesh, "current"); +} + +bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) { + if(!mesh || !node) { + meshlink_errno = MESHLINK_EINVAL; + return false; + } + + pthread_mutex_lock(&mesh->mutex); + + if(!whitelist(mesh, (node_t *)node)) { pthread_mutex_unlock(&mesh->mutex); return false; } + pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", node->name); + return true; +} + +bool meshlink_whitelist_by_name(meshlink_handle_t *mesh, const char *name) { + if(!mesh || !name) { + meshlink_errno = MESHLINK_EINVAL; + return false; + } + + pthread_mutex_lock(&mesh->mutex); + + node_t *n = lookup_node(mesh, (char *)name); + + if(!n) { + n = new_node(); + n->name = xstrdup(name); + node_add(mesh, n); + } + + if(!whitelist(mesh, (node_t *)n)) { + pthread_mutex_unlock(&mesh->mutex); + return false; + } pthread_mutex_unlock(&mesh->mutex); - return config_sync(mesh, "current"); + logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", name); + return true; } void meshlink_set_default_blacklist(meshlink_handle_t *mesh, bool blacklist) { mesh->default_blacklist = blacklist; } +bool meshlink_forget_node(meshlink_handle_t *mesh, meshlink_node_t *node) { + if(!mesh || !node) { + meshlink_errno = MESHLINK_EINVAL; + return false; + } + + node_t *n = (node_t *)node; + + pthread_mutex_lock(&mesh->mutex); + + /* Check that the node is not reachable */ + if(n->status.reachable || n->connection) { + pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_WARNING, "Could not forget %s: still reachable", n->name); + return false; + } + + /* Check that we don't have any active UTCP connections */ + if(n->utcp && utcp_is_active(n->utcp)) { + pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_WARNING, "Could not forget %s: active UTCP connections", n->name); + return false; + } + + /* Check that we have no active connections to this node */ + for list_each(connection_t, c, mesh->connections) { + if(c->node == n) { + pthread_mutex_unlock(&mesh->mutex); + logger(mesh, MESHLINK_WARNING, "Could not forget %s: active connection", n->name); + return false; + } + } + + /* Remove any pending outgoings to this node */ + if(mesh->outgoings) { + for list_each(outgoing_t, outgoing, mesh->outgoings) { + if(outgoing->node == n) { + list_delete_node(mesh->outgoings, node); + } + } + } + + /* Delete the config file for this node */ + if(!config_delete(mesh, "current", n->name)) { + pthread_mutex_unlock(&mesh->mutex); + return false; + } + + /* Delete the node struct and any remaining edges referencing this node */ + node_del(mesh, n); + + pthread_mutex_unlock(&mesh->mutex); + + return config_sync(mesh, "current"); +} + /* Hint that a hostname may be found at an address * See header file for detailed comment. */ diff --git a/src/meshlink.h b/src/meshlink.h index dd08436d..3298e7c4 100644 --- a/src/meshlink.h +++ b/src/meshlink.h @@ -999,6 +999,28 @@ extern char *meshlink_export(struct meshlink_handle *mesh) __attribute__((__warn */ extern bool meshlink_import(struct meshlink_handle *mesh, const char *data) __attribute__((__warn_unused_result__)); +/// Forget any information about a node. +/** This function allows the local node to forget any information it has about a node, + * and if possible will remove any data it has stored on disk about the node. + * + * Any open channels to this node must be closed before calling this function. + * + * After this call returns, the node handle is invalid and may no longer be used, regardless + * of the return value of this call. + * + * Note that this function does not prevent MeshLink from actually forgetting about a node, + * or re-learning information about a node at a later point in time. It is merely a hint that + * the application does not care about this node anymore and that any resources kept could be + * cleaned up. + * + * \memberof meshlink_node + * @param mesh A handle which represents an instance of MeshLink. + * @param node A pointer to a struct meshlink_node describing the node to be forgotten. + * + * @return This function returns true if all currently known data about the node has been forgotten, false otherwise. + */ +extern bool meshlink_forget_node(struct meshlink_handle *mesh, struct meshlink_node *node); + /// Blacklist a node from the mesh. /** This function causes the local node to blacklist another node. * The local node will drop any existing connections to that node, @@ -1012,6 +1034,21 @@ extern bool meshlink_import(struct meshlink_handle *mesh, const char *data) __at */ extern bool meshlink_blacklist(struct meshlink_handle *mesh, struct meshlink_node *node) __attribute__((__warn_unused_result__)); +/// Blacklist a node from the mesh by name. +/** This function causes the local node to blacklist another node by name. + * The local node will drop any existing connections to that node, + * and will not send data to it nor accept any data received from it any more. + * + * If no node by the given name is known, it is created. + * + * \memberof meshlink_node + * @param mesh A handle which represents an instance of MeshLink. + * @param name The name of the node to blacklist. + * + * @return This function returns true if the node has been blacklisted, false otherwise. + */ +extern bool meshlink_blacklist_by_name(struct meshlink_handle *mesh, const char *name) __attribute__((__warn_unused_result__)); + /// Whitelist a node on the mesh. /** This function causes the local node to whitelist a previously blacklisted node. * The local node will allow connections to and from that node, @@ -1025,6 +1062,22 @@ extern bool meshlink_blacklist(struct meshlink_handle *mesh, struct meshlink_nod */ extern bool meshlink_whitelist(struct meshlink_handle *mesh, struct meshlink_node *node) __attribute__((__warn_unused_result__)); +/// Whitelist a node on the mesh by name. +/** This function causes the local node to whitelist a node by name. + * The local node will allow connections to and from that node, + * and will send data to it and accept any data received from it. + * + * If no node by the given name is known, it is created. + * This is useful if new nodes are blacklisted by default. + * + * \memberof meshlink_node + * @param mesh A handle which represents an instance of MeshLink. + * @param node A pointer to a struct meshlink_node describing the node to be whitelisted. + * + * @return This function returns true if the node has been whitelisted, false otherwise. + */ +extern bool meshlink_whitelist_by_name(struct meshlink_handle *mesh, const char *name) __attribute__((__warn_unused_result__)); + /// Set whether new nodes are blacklisted by default. /** This function sets the blacklist behaviour for newly discovered nodes. * If set to true, new nodes will be automatically blacklisted. diff --git a/src/meshlink.sym b/src/meshlink.sym index cf38e94a..da5f0bc4 100644 --- a/src/meshlink.sym +++ b/src/meshlink.sym @@ -9,6 +9,7 @@ devtool_trybind_probe meshlink_add_address meshlink_add_external_address meshlink_blacklist +meshlink_blacklist_by_name meshlink_channel_aio_fd_receive meshlink_channel_aio_fd_send meshlink_channel_aio_receive @@ -27,6 +28,7 @@ meshlink_enable_discovery meshlink_encrypted_key_rotate meshlink_errno meshlink_export +meshlink_forget_node meshlink_get_all_nodes meshlink_get_all_nodes_by_dev_class meshlink_get_all_nodes_by_submesh @@ -79,3 +81,4 @@ meshlink_strerror meshlink_submesh_open meshlink_verify meshlink_whitelist +meshlink_whitelist_by_name diff --git a/test/Makefile.am b/test/Makefile.am index c4e6466f..1a0f8710 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,6 +1,7 @@ TESTS = \ basic \ basicpp \ + blacklist \ channels \ channels-aio \ channels-aio-fd \ @@ -30,6 +31,7 @@ AM_LDFLAGS = $(PTHREAD_LIBS) check_PROGRAMS = \ basic \ basicpp \ + blacklist \ channels \ channels-aio \ channels-aio-fd \ @@ -58,6 +60,9 @@ basic_LDADD = ../src/libmeshlink.la basicpp_SOURCES = basicpp.cpp utils.c utils.h basicpp_LDADD = ../src/libmeshlink.la +blacklist_SOURCES = blacklist.c utils.c utils.h +blacklist_LDADD = ../src/libmeshlink.la + channels_SOURCES = channels.c utils.c utils.h channels_LDADD = ../src/libmeshlink.la diff --git a/test/blacklist.c b/test/blacklist.c new file mode 100644 index 00000000..d5044602 --- /dev/null +++ b/test/blacklist.c @@ -0,0 +1,180 @@ +#define _GNU_SOURCE + +#ifdef NDEBUG +#undef NDEBUG +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "meshlink.h" +#include "devtools.h" +#include "utils.h" + +static struct sync_flag bar_connected; +static struct sync_flag bar_disconnected; +static struct sync_flag baz_connected; + +static void foo_status_cb(meshlink_handle_t *mesh, meshlink_node_t *node, bool reachable) { + (void)mesh; + (void)reachable; + + if(!strcmp(node->name, "bar")) { + if(reachable) { + set_sync_flag(&bar_connected, true); + } else { + set_sync_flag(&bar_disconnected, true); + } + } +} + +static void baz_status_cb(meshlink_handle_t *mesh, meshlink_node_t *node, bool reachable) { + (void)mesh; + (void)reachable; + + if(!strcmp(node->name, "bar")) { + if(reachable) { + set_sync_flag(&baz_connected, true); + } + } +} + +int main() { + meshlink_set_log_cb(NULL, MESHLINK_DEBUG, log_cb); + + // Create three instances. + + const char *name[3] = {"foo", "bar", "baz"}; + meshlink_handle_t *mesh[3]; + char *data[3]; + + for(int i = 0; i < 3; i++) { + char *path = NULL; + assert(asprintf(&path, "blacklist_conf.%d", i) != -1 && path); + + assert(meshlink_destroy(path)); + mesh[i] = meshlink_open(path, name[i], "trio", DEV_CLASS_BACKBONE); + assert(mesh[i]); + free(path); + + assert(meshlink_add_address(mesh[i], "localhost")); + + data[i] = meshlink_export(mesh[i]); + assert(data[i]); + + // Enable default blacklist on all nodes. + meshlink_set_default_blacklist(mesh[i], true); + } + + // The first node knows the two other nodes. + + for(int i = 1; i < 3; i++) { + assert(meshlink_import(mesh[i], data[0])); + assert(meshlink_import(mesh[0], data[i])); + + assert(meshlink_get_node(mesh[i], name[0])); + assert(meshlink_get_node(mesh[0], name[i])); + + } + + for(int i = 0; i < 3; i++) { + free(data[i]); + } + + // Second and third node should not know each other yet. + + assert(!meshlink_get_node(mesh[1], name[2])); + assert(!meshlink_get_node(mesh[2], name[1])); + + // Whitelisting and blacklisting by name should work. + + assert(meshlink_whitelist_by_name(mesh[0], "quux")); + assert(meshlink_blacklist_by_name(mesh[0], "xyzzy")); + + // Since these nodes now exist we should be able to forget them. + + assert(meshlink_forget_node(mesh[0], meshlink_get_node(mesh[0], "quux"))); + + // Start the nodes. + + meshlink_set_node_status_cb(mesh[0], foo_status_cb); + meshlink_set_node_status_cb(mesh[2], baz_status_cb); + + for(int i = 0; i < 3; i++) { + assert(meshlink_start(mesh[i])); + } + + // Wait for them to connect. + + assert(wait_sync_flag(&bar_connected, 5)); + + // Blacklist bar + + set_sync_flag(&bar_disconnected, false); + assert(meshlink_blacklist(mesh[0], meshlink_get_node(mesh[0], name[1]))); + assert(wait_sync_flag(&bar_disconnected, 5)); + + // Whitelist bar + + set_sync_flag(&bar_connected, false); + assert(meshlink_whitelist(mesh[0], meshlink_get_node(mesh[0], name[1]))); + assert(wait_sync_flag(&bar_connected, 15)); + + // Bar should not connect to baz + + assert(wait_sync_flag(&baz_connected, 5) == false); + + // But it should know about baz by now + + meshlink_node_t *bar = meshlink_get_node(mesh[2], "bar"); + meshlink_node_t *baz = meshlink_get_node(mesh[1], "baz"); + assert(bar); + assert(baz); + + // Have bar and baz whitelist each other + + assert(meshlink_whitelist(mesh[1], baz)); + assert(meshlink_whitelist(mesh[2], bar)); + + // They should connect to each other + + assert(wait_sync_flag(&baz_connected, 15)); + + // Trying to forget an active node should fail. + + assert(!meshlink_forget_node(mesh[1], baz)); + + // We need to re-acquire the handle to baz + + baz = meshlink_get_node(mesh[1], "baz"); + assert(baz); + + // Stop the mesh. + + for(int i = 0; i < 3; i++) { + meshlink_stop(mesh[i]); + } + + // Forgetting a node should work now. + + assert(meshlink_forget_node(mesh[1], baz)); + + // Clean up. + + for(int i = 0; i < 3; i++) { + meshlink_close(mesh[i]); + } + + // Check that foo has a config file for xyzzy but not quux + assert(access("blacklist_conf.0/current/hosts/xyzzy", F_OK) == 0); + assert(access("blacklist_conf.0/current/hosts/quux", F_OK) != 0 && errno == ENOENT); + + // Check that bar has no config file for baz + assert(access("blacklist_conf.2/current/hosts/bar", F_OK) == 0); + assert(access("blacklist_conf.1/current/hosts/baz", F_OK) != 0 && errno == ENOENT); +} -- 2.39.5