diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c2369eb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "chck"] + path = chck + url = git://github.com/Cloudef/chck.git + branch = refactor +[submodule "lib/chck"] + path = lib/chck + url = git://github.com/Cloudef/chck.git + branch = refactor diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e276717 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +PROJECT(pi9) +set(PI9_NAME "pi9") +set(PI9_DESCRIPTION "9p server abstraction library") + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${pi9_SOURCE_DIR}/CMake) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) + +if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-variadic-macros -Wno-long-long") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-variadic-macros -Wno-long-long") +endif () + +if (UNIX AND CMAKE_COMPILER_IS_GNUCC) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + if (${CMAKE_VERSION} VERSION_LESS 2.8.9) + add_definitions(-fPIC) + endif () +endif () + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -D_DEFAULT_SOURCE") + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/lib/chck) + +add_subdirectory(lib) +add_subdirectory(src) +add_subdirectory(bin) + +file(COPY src/pi9.h src/pi9_string.h DESTINATION include/pi9) diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt new file mode 100644 index 0000000..bfe7da6 --- /dev/null +++ b/bin/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(server server.c) +target_link_libraries(server pi9 chck_pool chck_lut) diff --git a/bin/server.c b/bin/server.c new file mode 100644 index 0000000..4f373df --- /dev/null +++ b/bin/server.c @@ -0,0 +1,993 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pi9.h" +#include "chck/pool/pool.h" +#include "chck/lut/lut.h" + +#define M(m) (m & 3) + +#ifndef offsetof +# if __GNUC__ +# define offsetof(st, m) __builtin_offsetof(st, m) +# else +# define offsetof(st, m) ((size_t)(&((st *)0)->m)) +# endif +#endif + +static const size_t NONODE = (size_t)~0; + +struct fs { + struct chck_hash_table fids; + struct chck_pool nodes; + size_t root; +}; + +struct node { + struct pi9_stat stat; + struct chck_iter_pool childs; + size_t parent; + uint8_t omode; + bool open; + + struct node_procs { + bool (*read)(struct pi9 *pi9, struct node *node, uint64_t offset, uint32_t count); + bool (*write)(struct pi9 *pi9, struct node *node, uint64_t offset, uint32_t count, const void *data); + uint64_t (*size)(struct pi9 *pi9, struct node *node); + } procs; +}; + +enum { + NONE = 0, + ROOT, +}; + +static bool +node_init(struct node *node, struct node_procs *procs) +{ + memset(node, 0, sizeof(struct node)); + memcpy(&node->procs, procs, sizeof(struct node_procs)); + node->stat.qid.path = node->parent = NONODE; + return chck_iter_pool(&node->childs, 4, 0, sizeof(size_t)); +} + +static void +node_release(struct node *node) +{ + if (!node) + return; + + pi9_stat_release(&node->stat); + chck_iter_pool_release(&node->childs); +} + +static struct node* +get_node(struct fs *fs, size_t node) +{ + return (node == NONODE ? NULL : chck_pool_get(&fs->nodes, node)); +} + +static bool +unlink_nodes(struct fs *fs, struct node *parent, struct node *child) +{ + assert(fs); + + if (!child) + return false; + + if (parent) { + size_t *n; + chck_iter_pool_for_each(&parent->childs, n) { + if (*n != child->stat.qid.path) + continue; + + chck_iter_pool_remove(&parent->childs, _I - 1); + break; + } + } + + child->parent = NONODE; + return true; +} + +static void +unlink_childs(struct fs *fs, struct node *parent) +{ + if (!parent) + return; + + size_t *c; + chck_iter_pool_for_each(&parent->childs, c) + unlink_nodes(fs, parent, get_node(fs, *c)); + + chck_iter_pool_release(&parent->childs); +} + +static bool +link_nodes(struct fs *fs, struct node *parent, struct node *child) +{ + assert(fs); + + if (!parent || !child) + return false; + + if (child->parent != NONODE && !unlink_nodes(fs, get_node(fs, child->parent), child)) + return false; + + if (child->stat.qid.path != NONODE && !chck_iter_pool_push_back(&parent->childs, &child->stat.qid.path)) + return false; + + child->parent = parent->stat.qid.path; + return true; +} + +static size_t +add_node(struct fs *fs, struct node *node) +{ + assert(fs && node); + + size_t n; + struct node *p; + if (!(p = chck_pool_add(&fs->nodes, node, &n))) + return NONODE; + + return (p->stat.qid.path = n); +} + +static void +remove_node(struct fs *fs, struct node *node) +{ + if (!node) + return; + + unlink_childs(fs, node); + unlink_nodes(fs, get_node(fs, node->parent), node); + node_release(node); + + if (node->stat.qid.path != NONODE) { + chck_pool_remove(&fs->nodes, node->stat.qid.path); + node = NULL; // ↑ node pointer is garbage after this + } +} + +static size_t +add_node_linked(struct fs *fs, struct node *node, size_t parent) +{ + assert(fs && node); + + size_t n; + if ((n = add_node(fs, node)) == NONODE) + return NONODE; + + if (!link_nodes(fs, chck_pool_get(&fs->nodes, parent), get_node(fs, n))) { + remove_node(fs, get_node(fs, n)); + return NONODE; + } + + return n; +} + +static inline struct node* +fid_to_node(struct fs *fs, uint32_t fid, size_t *n) +{ + if (n) *n = 0; + + if (fid == PI9_NOFID) + return NULL; + + size_t *i; + if (!(i = chck_hash_table_get(&fs->fids, fid))) + return NULL; + + if (n) *n = *i; + return get_node(fs, *i); +} + +static bool +set_fid(struct fs *fs, uint32_t fid, const size_t *node) +{ + return chck_hash_table_set(&fs->fids, fid, node); +} + +static bool +clunk_fid(struct fs *fs, uint32_t fid) +{ + struct node *f; + if ((f = fid_to_node(fs, fid, NULL))) { + if (f->omode & PI9_ORCLOSE) { + remove_node(fs, f); + f = NULL; // ↑ f is garbage after this + } else { + f->omode = 0; + f->open = false; + } + } + + return set_fid(fs, fid, NULL); +} + +static bool +cb_read_qtdir(struct pi9 *pi9, struct node *node, uint64_t offset, uint32_t count) +{ + (void)offset, (void)count; + + // For directories, read returns an integral number of directory entries exactly as in stat (see stat(5)), + // one for each member of the directory. The read request message must have offset equal to zero or the + // value of offset in the previous read on the directory, plus the number of bytes returned in the previous read. + // In other words, seeking other than to the beginning is illegal in a directory (see seek(2)). + + struct pi9_stat stats[2]; + memcpy(&stats[0], &node->stat, sizeof(struct pi9_stat)); + memcpy(&stats[1], &node->stat, sizeof(struct pi9_stat)); + pi9_string_set_cstr_with_length(&stats[0].name, ".", 1, false); + pi9_string_set_cstr_with_length(&stats[1].name, "..", 2, false); + + for (uint32_t i = 0; i < 2; ++i) { + if (!pi9_write_stat(&stats[i], pi9->out)) + return false; + } + + for (size_t i = 0; i < node->childs.items.count; ++i) { + size_t *n = chck_iter_pool_get(&node->childs, i); + struct node *c = get_node(pi9->userdata, *n); + assert(c); + + // update size from callback + if (c->procs.size) + c->stat.length = c->procs.size(pi9, c); + + if (!pi9_write_stat(&c->stat, pi9->out)) + return false; + } + + return true; +} + +static bool +cb_read_hello(struct pi9 *pi9, struct node *node, uint64_t offset, uint32_t count) +{ + (void)node, (void)offset, (void)count; + + // The read request asks for count bytes of data from the file identified by fid, + // which must be opened for reading, starting offset bytes after the beginning of the file. + // The bytes are returned with the read reply message. + + // XXX: Just example here, we ignore offset and count + if (pi9_write("Hello World!", 1, sizeof("Hello World!"), pi9->out) != sizeof("Hello World!")) + return false; + + return true; +} + +static bool +cb_write_hello(struct pi9 *pi9, struct node *node, uint64_t offset, uint32_t count, const void *data) +{ + (void)pi9, (void)offset; + + fprintf(stderr, "\n--- wrote to %s\n", node->stat.name.data); + fprintf(stderr, "(%u) ", count); + for (size_t i = 0; i < count; ++i) putc(*(char*)(data + i), stderr); + fprintf(stderr, "---\n\n"); + return true; +} + +static uint64_t +cb_size_hello(struct pi9 *pi9, struct node *node) +{ + (void)pi9, (void)node; + return sizeof("Hello World!"); +} + +static void +fs_release(struct fs *fs) +{ + if (!fs) + return; + + chck_hash_table_release(&fs->fids); + chck_pool_release(&fs->nodes); +} + +static bool +fs_init(struct fs *fs, uint32_t initial_nodes) +{ + assert(fs); + memset(fs, 0, sizeof(struct fs)); + + if (!chck_hash_table(&fs->fids, PI9_NOFID, 128, sizeof(size_t))) + goto fail; + + chck_hash_table_uint_algorithm(&fs->fids, chck_incremental_uint_hash); + + if (!chck_pool(&fs->nodes, 32, initial_nodes, sizeof(struct node))) + goto fail; + + struct node_procs dir_procs = { + .read = cb_read_qtdir, + .write = NULL, + .size = NULL, + }; + + struct node root; + if (!node_init(&root, &dir_procs)) + goto fail; + + root.stat = (struct pi9_stat) { + .type = ROOT, + .dev = 0, + .qid = { PI9_QTDIR, 0, 0 }, + .mode = PI9_DMDIR | 0700, + .atime = time(NULL), + .mtime = time(NULL), + .length = 0, // dirs are always 0 + .name = { NULL, 0, false }, + .uid = { NULL, 0, false }, + .gid = { NULL, 0, false }, + .muid = { NULL, 0, false }, + }; + + struct node_procs test_procs = { + .read = cb_read_hello, + .write = cb_write_hello, + .size = cb_size_hello, + }; + + struct node test; + node_init(&test, &test_procs); + test.stat = (struct pi9_stat) { + .type = 0, + .dev = 0, + .qid = { PI9_QTFILE, 0, 1 }, + .mode = 0600, + .atime = time(NULL), + .mtime = time(NULL), + .length = 0, // we use callback + .name = { "hello", sizeof("hello"), false }, + .uid = { NULL, 0, false }, + .gid = { NULL, 0, false }, + .muid = { NULL, 0, false }, + }; + + if ((fs->root = add_node(fs, &root)) == NONODE) { + node_release(&root); + node_release(&test); + goto fail; + } + + if (add_node_linked(fs, &test, fs->root) == NONODE) { + node_release(&root); + node_release(&test); + goto fail; + } + + return true; + +fail: + fs_release(fs); + return false; +} + +static bool +cb_attach(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint32_t afid, const struct pi9_string *uname, const struct pi9_string *aname, struct pi9_qid **qid) +{ + (void)tag, (void)uname, (void)aname; + + // if afid == NOFID then no authentication is requested + struct node *af = NULL; + if (afid != PI9_NOFID && !(af = fid_to_node(pi9->userdata, afid, NULL))) + goto err_not_allowed; // the node for authentication does not exist + + // asked for authentication but we don't support it + if (af) + goto err_no_auth; + + // fid that is in use cannot be used to access root node + if (fid_to_node(pi9->userdata, fid, NULL)) + goto err_not_allowed; + + struct fs *fs = pi9->userdata; + if (!set_fid(fs, fid, &fs->root)) + goto err_oom; + + struct node *f; + if (!(f = get_node(fs, fs->root))) + goto err_nofid; + + *qid = &f->stat.qid; + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_oom: + pi9_write_error(tag, ERR_OUT_OF_MEMORY, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +err_no_auth: + pi9_write_error(tag, ERR_NO_AUTH, pi9->out); + return false; +} + +static bool +cb_flush(struct pi9 *pi9, uint16_t tag, uint16_t oldtag) +{ + (void)pi9, (void)tag, (void)oldtag; + + // When the response to a request is no longer needed, such as when a user interrupts a process doing a read(2), a + // Tflush request is sent to the server to purge the pending response. The message being flushed is identified by oldtag. + // The semantics of flush depends on messages arriving in order. + + // The server may respond to the pending request before responding to the Tflush. + // It is possible for a client to send multiple Tflush messages for a particular pending request. + // Each subsequent Tflush must contain as oldtag the tag of the pending request (not a previous Tflush). + // Should multiple Tflushes be received for a pending request, they must be answered in order. + // A Rflush for any of the multiple Tflushes implies an answer for all previous ones. Therefore, + // should a server receive a request and then multiple flushes for that request, it need respond only to the last flush. + + return true; +} + +static bool +cb_walk(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint32_t newfid, uint16_t nwname, const struct pi9_string *walks, struct pi9_qid **qids, uint16_t *out_nwqid) +{ + (void)tag; + + size_t index; + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, &index))) + goto err_nofid; + + // The walk request carries as arguments an existing fid and a proposed newfid + // (which must not be in use unless it is the same as fid) + if (fid != newfid && fid_to_node(pi9->userdata, newfid, NULL)) + goto err_fid_in_use; + + struct node *wnode = f; + for (uint32_t i = 0; i < nwname; ++i) { + // The fid must represent a directory unless zero path name elements are specified. + if (wnode->stat.qid.type != PI9_QTDIR) { + // Otherwise, the walk will return an Rwalk message containing nwqid qids corresponding, in order, + // to the files that are visited by the nwqid successful elementwise walks; + break; + } + + // The name ``..'' (dot-dot) represents the parent directory. + // Single (dot) is not used in 9p protocol + if (pi9_string_eq_cstr(&walks[i], "..")) { + + struct node *n; + if (wnode->stat.type == ROOT) { + // A walk of the name ``..'' in the root directory of a server is equivalent to a walk with no name elements. + n = wnode; + } else if (!(n = get_node(pi9->userdata, wnode->parent))) { + break; + } + + qids[*out_nwqid] = &n->stat.qid; + index = (wnode->parent != NONODE ? wnode->parent : index); + wnode = n; + (*out_nwqid)++; + } else { + size_t *c; + chck_iter_pool_for_each(&wnode->childs, c) { + struct node *n; + if (!(n = get_node(pi9->userdata, *c)) || pi9_string_eq(&walks[i], &n->stat.name)) + continue; + + qids[*out_nwqid] = &n->stat.qid; + index = *c; + wnode = n; + (*out_nwqid)++; + break; + } + } + } + + // If the first element cannot be walked for any reason, Rerror is returned. + if (nwname > 0 && *out_nwqid == 0) + goto err_not_directory; + + // Index is only affected if equal + if (*out_nwqid != nwname) + index = NONODE; + + // Always same when affected, otherwise unaffected (index == NONODE) + assert(*out_nwqid == nwname || index == NONODE); + + // It is legal for nwname to be zero, in which case newfid will represent the same file as fid + if (index != NONODE && !set_fid(pi9->userdata, newfid, &index)) + goto err_oom; + + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_fid_in_use: + pi9_write_error(tag, ERR_FID_IN_USE, pi9->out); + return false; +err_not_directory: + pi9_write_error(tag, ERR_NOT_DIRECTORY, pi9->out); + return false; +err_oom: + pi9_write_error(tag, ERR_OUT_OF_MEMORY, pi9->out); + return false; +} + +static bool +cb_open(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint8_t mode, struct pi9_qid **out_qid, uint32_t *out_iounit) +{ + (void)tag, (void)out_iounit; + + // if mode has the OTRUNC (0x10) bit set, the file is to be truncated, which requires write permission + // (if the file is append-only, and permission is granted, the open succeeds but the file will not be truncated); + + // if the mode has the ORCLOSE (0x40) bit set, the file is to be removed when the fid is clunked, + // which requires permission to remove the file from its directory. + + // It is illegal to write a directory, truncate it, or attempt to remove it on close. + + // If the file is marked for exclusive use (see stat(5)), only one client can have the file open at any time. + // That is, after such a file has been opened, further opens will fail until fid has been clunked. + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // It is an error for either of these messages if the fid is already the product of a successful open or create message. + if (f->open) + goto err_not_allowed; + + // The iounit field returned by open and create may be zero. If it is not, + // it is the maximum number of bytes that are guaranteed to be read from or written to the file + // without breaking the I/O transfer into multiple 9P messages. + // (out_iounit is by default 0) + + f->omode = mode; + f->open = true; + *out_qid = &f->stat.qid; + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool +cb_create(struct pi9 *pi9, uint16_t tag, uint32_t fid, const struct pi9_string *name, uint32_t perm, uint8_t mode, struct pi9_qid **out_qid, uint32_t *out_iounit) +{ + (void)tag, (void)perm, (void)mode, (void)out_qid, (void)out_iounit; + + // The create request asks the file server to create a new file with the name supplied, + // in the directory (dir) represented by fid, and requires write permission in the directory. + // The owner of the file is the implied user id of the request, the group of the file is the same as dir, + // and the permissions are the value of ```perm & (~0666 | (dir.perm & 0666))``` if a regular file is being created + // and ```perm & (~0777 | (dir.perm & 0777))``` if a directory is being created. This means, for example, + // that if the create allows read permission to others, but the containing directory does not, + // then the created file will not allow others to read the file. + + // Finally, the newly created file is opened according to mode, and fid will represent the newly opened file. + // Mode is not checked against the permissions in perm. The qid for the new file is returned with the create reply message. + + // Directories are created by setting the DMDIR bit (0x80000000) in the perm. + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // It is an error for either of these messages if the fid is already the product of a successful open or create message. + if (f->open || f->stat.qid.type != PI9_QTDIR) + goto err_not_allowed; + + // An attempt to create a file in a directory where the given name already exists will be rejected + size_t *c; + chck_iter_pool_for_each(&f->childs, c) { + struct node *n = get_node(pi9->userdata, *c); + assert(n); + + if (pi9_string_eq(&n->stat.name, name)) + goto err_not_allowed; + } + + // The iounit field returned by open and create may be zero. If it is not, + // it is the maximum number of bytes that are guaranteed to be read from or written to the file + // without breaking the I/O transfer into multiple 9P messages. + // (out_iounit is by default 0) + + // FIXME: creation code here + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool +cb_read(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint64_t offset, uint32_t count) +{ + (void)tag; + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + if (M(f->omode) != PI9_OREAD) + goto err_not_allowed; + + if (f->procs.read && !f->procs.read(pi9, f, offset, count)) + goto err_write; + + return true; + +err_write: + pi9_write_error(tag, ERR_WRITE, pi9->out); + return false; +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool +cb_write(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint64_t offset, uint32_t count, const void *data) +{ + (void)tag; + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // Directories may not be written. + if (f->stat.qid.type == PI9_QTDIR || M(f->omode) != PI9_OWRITE) + goto err_not_allowed; + + // The write request asks that count bytes of data be recorded in the file identified by fid, + // which must be opened for writing, starting offset bytes after the beginning of the file. + // If the file is append-only, the data will be placed at the end of the file regardless of offset. + + if (f->procs.write && !f->procs.write(pi9, f, offset, count, data)) + goto err_write; + + return true; + +err_write: + pi9_write_error(tag, ERR_WRITE, pi9->out); + return false; +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool +cb_clunk(struct pi9 *pi9, uint16_t tag, uint32_t fid) +{ + (void)tag; + + if (!clunk_fid(pi9->userdata, fid)) + goto err_oom; + + // The actual file is not removed on the server unless the fid had been opened with ORCLOSE. + // Once a fid has been clunked, the same fid can be reused in a new walk or attach request. + // Even if the clunk returns an error, the fid is no longer valid. + // A clunk message is generated by close and indirectly by other actions such as failed open calls. + + return true; + +err_oom: + pi9_write_error(tag, ERR_OUT_OF_MEMORY, pi9->out); + return false; +} + +static bool +cb_remove(struct pi9 *pi9, uint16_t tag, uint32_t fid) +{ + (void)tag; + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // The remove request asks the file server both to remove the file represented by fid and to clunk the fid, even if the remove fails. + // This request will fail if the client does not have write permission in the parent directory. + + // It is correct to consider remove to be a clunk with the side effect of removing the file if permissions allow. + + // If a file has been opened as multiple fids, possibly on different connections, and one fid is used to remove the file, + // whether the other fids continue to provide access to the file is implementation-defined. + + if (!clunk_fid(pi9->userdata, fid)) + goto err_oom; + + if (f->stat.type == ROOT) + goto err_not_allowed; + + remove_node(pi9->userdata, f); + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_oom: + pi9_write_error(tag, ERR_OUT_OF_MEMORY, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool +cb_stat(struct pi9 *pi9, uint16_t tag, uint32_t fid, struct pi9_stat **out_stat) +{ + (void)tag; + + // The stat request requires no special permissions. + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // update size from callback + if (f->procs.size) + f->stat.length = f->procs.size(pi9, f); + + *out_stat = &f->stat; + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +} + +static bool +cb_twstat(struct pi9 *pi9, uint16_t tag, uint32_t fid, const struct pi9_stat *stat) +{ + (void)tag; + + struct node *f; + if (!(f = fid_to_node(pi9->userdata, fid, NULL))) + goto err_nofid; + + // It is an error to attempt to set the length of a directory to a non-zero value, and servers may decide to reject + // length changes for other reasons. + if (f->stat.qid.type == PI9_QTDIR && stat->length != f->stat.length) + goto err_not_allowed; + + // wstat request can avoid modifying some properties of the file by providing explicit ``don't touch'' values in the stat data + // that is sent: zero-length strings for text values and the maximum unsigned value of appropriate size for integral values. + // As a special case, if all the elements of the directory entry in a Twstat message are ``don't touch'' values, the server + // may interpret it as a request to guarantee that the contents of the associated file are committed to stable storage + // before the Rwstat message is returned. + // (Consider the message to mean, ``make the state of the file exactly what it claims to be.'') + + struct pi9_stat wstat; + memcpy(&wstat, stat, sizeof(wstat)); + if (wstat.type == (uint16_t)~0) wstat.type = f->stat.type; + if (wstat.dev == (uint32_t)~0) wstat.dev = f->stat.dev; + if (wstat.qid.type == (uint8_t)~0) wstat.qid.type = f->stat.qid.type; + if (wstat.qid.vers == (uint32_t)~0) wstat.qid.vers = f->stat.qid.vers; + if (wstat.qid.path == (uint64_t)~0) wstat.qid.path = f->stat.qid.path; + if (wstat.mode == (uint32_t)~0) wstat.mode = f->stat.mode; + if (wstat.atime == (uint32_t)~0) wstat.atime = f->stat.atime; + if (wstat.mtime == (uint32_t)~0) wstat.mtime = f->stat.mtime; + if (wstat.length == (uint64_t)~0) wstat.length = f->stat.length; + + struct pi9_string *fields[4] = { &wstat.name, &wstat.uid, &wstat.gid, &wstat.muid }; + struct pi9_string *fields2[4] = { &f->stat.name, &f->stat.uid, &f->stat.gid, &wstat.muid }; + for (uint32_t i = 0; i < 4; ++i) { + if (fields[i]->size > 0) + continue; + + pi9_string_set(fields[i], fields2[i], false); + } + + // The length can be changed (affecting the actual length of the file) by anyone with write permission on the file. + // of the file's current group. The directory bit cannot be changed by a wstat; + // The mode and mtime can be changed by the owner of the file or the group leader the other defined permission and mode bits can. + // The gid can be changed: by the owner if also a member of the new group; + // or by the group leader of the file's current group if also leader of the new group + // (see intro(5) for more information about permissions and users(6) for users and groups). + // None of the other data can be altered by a wstat and attempts to change them will trigger an error. + // In particular, it is illegal to attempt to change the owner of a file. + // (These conditions may be relaxed when establishing the initial state of a file server; see fsconfig(8).) + if (wstat.type != f->stat.type || + wstat.dev != f->stat.dev || + wstat.atime != f->stat.atime || + memcmp(&wstat.qid, &f->stat.qid, sizeof(wstat.qid)) || + !pi9_string_eq(&wstat.uid, &f->stat.uid) || + !pi9_string_eq(&wstat.muid, &f->stat.muid)) + goto err_not_allowed; + + if (wstat.name.size > 0 && !pi9_string_eq(&wstat.name, &f->stat.name)) { + // The name can be changed by anyone with write permission in the parent directory; + // it is an error to change the name to that of an existing file. + struct node *p; + if ((p = get_node(pi9->userdata, f->parent))) { + size_t *c; + chck_iter_pool_for_each(&p->childs, c) { + struct node *n = get_node(pi9->userdata, *c); + assert(n); + + if (pi9_string_eq(&n->stat.name, &wstat.name)) + goto err_not_allowed; + } + } + } + + // ↑ + // Either all the changes in wstat request happen, or none of them does: + // if the request succeeds, all changes were made; if it fails, none were. + memcpy(&f->stat, &wstat, offsetof(struct pi9_stat, name)); + + if (f->parent != NONODE && wstat.name.size > 0) + pi9_string_set(&f->stat.name, &wstat.name, true); // this can fail in OOM, but oh well + + return true; + +err_nofid: + pi9_write_error(tag, ERR_NO_FID, pi9->out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, pi9->out); + return false; +} + +static bool running = true; + +static void +sigint(int32_t signal) +{ + (void)signal; + running = false; +} + +static int32_t +sock_unix(char *address, struct sockaddr_un *sa, socklen_t *salen) +{ + assert(address && sa && salen); + memset(sa, 0, sizeof(*sa)); + + sa->sun_family = AF_UNIX; + strncpy(sa->sun_path, address, sizeof(sa->sun_path)); + *salen = SUN_LEN(sa); + + int32_t fd; + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) + return -1; + + return fd; +} + +static int32_t +announce_unix(char *file) +{ + assert(file); + + int32_t fd; + socklen_t salen; + struct sockaddr_un sa; + if ((fd = sock_unix(file, &sa, &salen)) < 0) + return -1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, sizeof(int)) < 0) + goto fail; + + unlink(file); + if (bind(fd, (struct sockaddr*)&sa, salen) < 0) + goto fail; + + chmod(file, S_IRWXU); + if (listen(fd, 0) < 0) + goto fail; + + return fd; + +fail: + close(fd); + return -1; +} + +int +main(int argc, char *argv[]) +{ + (void)argc, (void)argv; + + struct fs fs; + if (!fs_init(&fs, 1)) + return EXIT_FAILURE; + + struct pi9_procs procs = { + .auth = NULL, + .attach = cb_attach, + .flush = cb_flush, + .walk = cb_walk, + .open = cb_open, + .create = cb_create, + .read = cb_read, + .write = cb_write, + .clunk = cb_clunk, + .remove = cb_remove, + .stat = cb_stat, + .twstat = cb_twstat + }; + + struct pi9 pi9; + if (!pi9_init(&pi9, 0, &procs, &fs)) + return EXIT_FAILURE; + + struct pollfd fds[2] = {{0}}; + fds[0].events = POLLIN; + fds[1].events = POLLIN; + fds[1].fd = -1; + + if ((fds[0].fd = announce_unix("9p")) < 0) + return EXIT_FAILURE; + + signal(SIGINT, sigint); + + int32_t clients = 0; + while (running) { + int32_t ret; + if ((ret = poll(fds, 1 + clients, 500)) <= 0) + continue; + + for (int32_t i = 0; i < 1 + clients; ++i) { + if (fds[i].revents & POLLIN) { + if (i == 0) { + int32_t fd; + if ((fd = accept(fds[i].fd, NULL, NULL)) >= 0) { + if (clients > 0) { + fprintf(stderr, "Rejected client\n"); + close(fd); + } else { + fds[1 + clients].fd = fd; + ++clients; + fprintf(stderr, "Accepted a new client\n"); + } + } + } else { + char buffer[32]; + if (recv(fds[i].fd, buffer, sizeof(buffer), MSG_PEEK | MSG_DONTWAIT) == 0) { + close(fds[i].fd); + fds[i].fd = -1; + + if (clients > 0) + --clients; + + fprintf(stderr, "Client disconnected\n"); + } else { + pi9_process(&pi9, fds[i].fd); + } + } + } + } + } + + for (int32_t i = 0; i < 1 + clients; ++i) + close(fds[i].fd); + + pi9_release(&pi9); + fs_release(&fs); + unlink("9p"); + return EXIT_SUCCESS; +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..6c543cb --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(chck_buffer STATIC chck/chck/buffer/buffer.c) +add_library(chck_pool STATIC chck/chck/pool/pool.c) +add_library(chck_lut STATIC chck/chck/lut/lut.c) diff --git a/lib/chck b/lib/chck new file mode 160000 index 0000000..8ebf54a --- /dev/null +++ b/lib/chck @@ -0,0 +1 @@ +Subproject commit 8ebf54a04ff2119fe36bf81b8b3ac52df45be38b diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..96f8e35 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(pi9 pi9.c pi9_string.c) +target_link_libraries(pi9 chck_buffer) diff --git a/src/pi9.c b/src/pi9.c new file mode 100644 index 0000000..da62e5d --- /dev/null +++ b/src/pi9.c @@ -0,0 +1,905 @@ +#include +#include +#include + +#define __STDC_FORMAT_MACROS +#include + +#include "pi9.h" +#include "chck/buffer/buffer.h" + +#define VERBOSE true +#define MAXWELEM 16 + +static const uint16_t NOTAG = (uint16_t)~0; + +static const uint32_t HDRSZ = 7; // size of header +static const uint32_t QIDSZ = 13; // size of serialized pi9_qid +static const uint32_t STATHDRSZ = 47; // size of serialized pi9_stat, until the variable length data + +enum op { + OPFIRST, + Tversion = 0x64, + Rversion, + Tauth = 0x66, + Rauth, + Tattach = 0x68, + Rattach, + Terror = 0x6A, // illegal + Rerror, + Tflush = 0x6C, + Rflush, + Twalk = 0x6E, + Rwalk, + Topen = 0x70, + Ropen, + Tcreate = 0x72, + Rcreate, + Tread = 0x74, + Rread, + Twrite = 0x76, + Rwrite, + Tclunk = 0x78, + Rclunk, + Tremove = 0x7A, + Rremove, + Tstat = 0x7C, + Rstat, + Twstat = 0x7E, + Rwstat, + OPLAST, +}; + +#define DECOP(x) static bool op_##x(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +DECOP(Tversion); +DECOP(Tauth); +DECOP(Tattach); +DECOP(Tflush); +DECOP(Twalk); +DECOP(Topen); +DECOP(Tcreate); +DECOP(Tread); +DECOP(Twrite); +DECOP(Tclunk); +DECOP(Tremove); +DECOP(Tstat); +DECOP(Twstat); +#undef DECOP + +static struct { + size_t msz; + bool (*cb)(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out); +} ops[] = { + [Tversion] = { 6, op_Tversion }, + [Tauth] = { 8, op_Tauth }, + [Terror] = { 0, NULL }, // invalid + [Tflush] = { 2, op_Tflush }, + [Tattach] = { 12, op_Tattach }, + [Twalk] = { 10, op_Twalk }, + [Topen] = { 5, op_Topen }, + [Tcreate] = { 11, op_Tcreate }, + [Tread] = { 16, op_Tread }, + [Twrite] = { 16, op_Twrite }, + [Tclunk] = { 4, op_Tclunk }, + [Tremove] = { 4, op_Tremove }, + [Tstat] = { 4, op_Tstat }, + [Twstat] = { 6, op_Twstat }, +}; + +static const struct { + const char *msg; + size_t size; +} errors[ERR_LAST] = { +#define MSG(x) { x, sizeof(x) } + MSG("Could not read message."), + MSG("Could not write message."), + MSG("Authentication unnecessary."), + MSG("File is not a directory."), + MSG("No such file or directory."), + MSG("Fid already in user."), + MSG("Operation not allowed."), + MSG("Unknown op code."), + MSG("Out of memory."), +#undef MSG +}; + +static inline bool +write_qid(struct pi9_qid *qid, struct chck_buffer *out) +{ + return (chck_buffer_write_int(&qid->type, sizeof(qid->type), out) && + chck_buffer_write_int(&qid->vers, sizeof(qid->vers), out) && + chck_buffer_write_int(&qid->path, sizeof(qid->path), out)); +} + +static inline bool +read_qid(struct pi9_qid *qid, struct chck_buffer *in) +{ + return (chck_buffer_read_int(&qid->type, sizeof(qid->type), in) && + chck_buffer_read_int(&qid->vers, sizeof(qid->vers), in) && + chck_buffer_read_int(&qid->path, sizeof(qid->path), in)); +} + +static inline bool +read_stat(struct pi9_stat *stat, struct chck_buffer *in) +{ + uint16_t size; + if (!chck_buffer_read_int(&size, sizeof(size), in) || + !chck_buffer_read_int(&stat->type, sizeof(stat->type), in) || + !chck_buffer_read_int(&stat->dev, sizeof(stat->dev), in) || + !read_qid(&stat->qid, in) || + !chck_buffer_read_int(&stat->mode, sizeof(stat->mode), in) || + !chck_buffer_read_int(&stat->atime, sizeof(stat->atime), in) || + !chck_buffer_read_int(&stat->mtime, sizeof(stat->mtime), in) || + !chck_buffer_read_int(&stat->length, sizeof(stat->length), in)) + return false; + + struct pi9_string *fields[4] = { &stat->name, &stat->uid, &stat->gid, &stat->muid }; + for (uint32_t i = 0; i < 4; ++i) { + uint16_t len; + if (!chck_buffer_read_int(&len, sizeof(len), in)) + return false; + + pi9_string_set_cstr_with_length(fields[i], in->curpos, len, false); + } + + return true; +} + +static bool +op_Tversion(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + // tag should always be NOTAG in version messages + if (tag != NOTAG) + goto err_not_allowed; + + uint32_t msize; + uint16_t vsize; + if (!chck_buffer_read_int(&msize, sizeof(msize), in) || + !chck_buffer_read_int(&vsize, sizeof(vsize), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tversion %u %u\n", tag, msize); +#endif + + struct pi9_string version = {0}; + pi9_string_set_cstr_with_length(&version, in->curpos, vsize, false); + + // A successful version request initializes the connection. + // All outstanding I/O on the connection is aborted; all active fids are freed (`clunked') automatically. + // The set of messages between version requests is called a session. + + // FIXME: ↑ we need to tell higher level to abort all outstanding I/O + + // honor the buffer size request + pi9->msize = (msize > 0 ? msize : pi9->msize); + + // only support 9P2000, maybe 9P2000.L later, .u probably never + if (!pi9_string_eq_cstr(&version, "9P2000")) { + static const char *preferred = "9P2000"; + const char *reply = (vsize > 0 && pi9_cstrneq(version.data, "9P", (vsize >= 2 ? 2 : vsize)) ? preferred : "unknown"); + vsize = strlen(reply); + const uint32_t size = HDRSZ + sizeof(msize) + sizeof(vsize) + vsize; + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rversion}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !chck_buffer_write_int(&msize, sizeof(msize), out) || + !chck_buffer_write_string_of_type(reply, vsize, sizeof(uint16_t), out)) + goto err_write; + } else { + const size_t size = HDRSZ + sizeof(msize) + sizeof(vsize) + vsize; + if (chck_buffer_write(in->buffer, 1, size, out) != size) + goto err_write; + + *(uint8_t*)(out->buffer + sizeof(uint32_t)) = Rversion; + } + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, out); + return false; +} + +static bool +op_Tauth(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t afid; + if (!chck_buffer_read_int(&afid, sizeof(afid), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tauth %u %u\n", tag, afid); +#endif + + uint16_t usize; + if (!chck_buffer_read_int(&usize, sizeof(usize), in)) + goto err_read; + + struct pi9_string uname = {0}; + pi9_string_set_cstr_with_length(&uname, in->curpos, usize, false); + chck_buffer_seek(in, usize, SEEK_CUR); + + uint16_t asize; + if (!chck_buffer_read_int(&asize, sizeof(asize), in)) + goto err_read; + + struct pi9_string aname = {0}; + pi9_string_set_cstr_with_length(&aname, in->curpos, asize, false); + + struct pi9_qid *qid = NULL; + if (pi9->procs.auth && !pi9->procs.auth(pi9, tag, afid, &uname, &aname, &qid)) + return false; + + if (!qid) + goto err_no_auth; + + const uint32_t size = HDRSZ + QIDSZ; + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rauth}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !write_qid(qid, out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_no_auth: + pi9_write_error(tag, ERR_NO_AUTH, out); + return false; +} + +static bool +op_Tattach(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid, afid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&afid, sizeof(afid), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tattach %u %u %u\n", tag, fid, afid); +#endif + + uint16_t usize; + if (!chck_buffer_read_int(&usize, sizeof(usize), in)) + goto err_read; + + struct pi9_string uname = {0}; + pi9_string_set_cstr_with_length(&uname, in->curpos, usize, false); + chck_buffer_seek(in, usize, SEEK_CUR); + + uint16_t asize; + if (!chck_buffer_read_int(&asize, sizeof(asize), in)) + goto err_read; + + struct pi9_string aname = {0}; + pi9_string_set_cstr_with_length(&aname, in->curpos, asize, false); + + struct pi9_qid *qid = NULL; + if (pi9->procs.attach && !pi9->procs.attach(pi9, tag, fid, afid, &uname, &aname, &qid)) + return false; + + const uint32_t size = HDRSZ + QIDSZ; + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rattach}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !write_qid(qid, out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Tflush(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint16_t oldtag; + if (!chck_buffer_read_int(&oldtag, sizeof(oldtag), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tflush %u %u\n", tag, oldtag); +#endif + + if (pi9->procs.flush && !pi9->procs.flush(pi9, tag, oldtag)) + return false; + + // The server should answer the flush message immediately. + // If it recognizes oldtag as the tag of a pending transaction, it should abort any pending response and discard that tag. + // In either case, it should respond with an Rflush echoing the tag (not oldtag) of the Tflush message. + // A Tflush can never be responded to by an Rerror message. + + if (!chck_buffer_write_int(&HDRSZ, sizeof(HDRSZ), out) || + !chck_buffer_write_int((uint8_t[]){Rflush}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Twalk(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint16_t nwname; + uint32_t fid, newfid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&newfid, sizeof(newfid), in) || + !chck_buffer_read_int(&nwname, sizeof(nwname), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Twalk %u %u %u %u\n", tag, fid, newfid, nwname); +#endif + + if (nwname > MAXWELEM) + goto err_not_allowed; + + struct pi9_string walks[MAXWELEM] = {{0}}; + for (uint32_t i = 0; i < MAXWELEM; ++i) { + uint16_t len; + if (!chck_buffer_read_int(&len, sizeof(len), in)) + goto err_read; + + pi9_string_set_cstr_with_length(&walks[i], in->curpos, len, false); + } + + uint16_t nwqid = 0; + struct pi9_qid *qids[MAXWELEM]; + if (pi9->procs.walk && !pi9->procs.walk(pi9, tag, fid, newfid, nwname, walks, qids, &nwqid)) + return false; + + // must be always less or equal + assert(nwqid <= nwname); + + const uint32_t size = HDRSZ + sizeof(nwqid) + nwqid * QIDSZ; + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rwalk}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !chck_buffer_write_int(&nwqid, sizeof(nwqid), out)) + goto err_write; + + for (uint32_t i = 0; i < nwqid; ++i) { + if (!write_qid(qids[i], out)) + goto err_write; + } + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, out); + return false; +} + +static bool +op_Topen(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint8_t mode; + uint32_t fid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&mode, sizeof(mode), in)) + goto err_read; + + // All other bits in mode should be zero + // ↑ Means that I should validate the mode for unknown bits to p9 protocol + +#if VERBOSE + fprintf(stderr, "Topen %u %u %u\n", tag, fid, mode); +#endif + + uint32_t iounit = 0; + struct pi9_qid *qid = NULL; + if (pi9->procs.open && !pi9->procs.open(pi9, tag, fid, mode, &qid, &iounit)) + return false; + + if (!qid) + goto err_not_allowed; + + const uint32_t size = HDRSZ + QIDSZ + sizeof(iounit); + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Ropen}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !write_qid(qid, out) || + !chck_buffer_write_int(&iounit, sizeof(iounit), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, out); + return false; +} + +static bool +op_Tcreate(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid; + uint16_t nsize; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&nsize, sizeof(nsize), in)) + goto err_read; + + struct pi9_string name = {0}; + pi9_string_set_cstr_with_length(&name, in->curpos, nsize, false); + + uint8_t mode; + uint32_t perm; + if (!chck_buffer_read_int(&perm, sizeof(perm), in) || + !chck_buffer_read_int(&mode, sizeof(mode), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tcreate %u %u\n", tag, fid); +#endif + + // The names . and .. are special; it is illegal to create files with these names. + if (pi9_string_eq_cstr(&name, ".") || pi9_string_eq_cstr(&name, "..")) + goto err_not_allowed; + + uint32_t iounit = 0; + struct pi9_qid *qid = NULL; + if (pi9->procs.create && !pi9->procs.create(pi9, tag, fid, &name, perm, mode, &qid, &iounit)) + return false; + + if (!qid) + goto err_not_allowed; + + const uint32_t size = HDRSZ + QIDSZ + sizeof(iounit); + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rcreate}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !write_qid(qid, out) || + !chck_buffer_write_int(&iounit, sizeof(iounit), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_not_allowed: + pi9_write_error(tag, ERR_NOT_ALLOWED, out); + return false; +} + +static bool +op_Tread(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint64_t offset; + uint32_t fid, count; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&offset, sizeof(offset), in) || + !chck_buffer_read_int(&count, sizeof(count), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tread %u %u %"PRIu64" %u\n", tag, fid, offset, count); +#endif + + chck_buffer_seek(out, HDRSZ + sizeof(uint32_t), SEEK_SET); + void *start = out->curpos; + + if (pi9->procs.read && !pi9->procs.read(pi9, tag, fid, offset, count)) + return false; + + const uint32_t sbufsz = (offset != 0 ? 0 : (out->curpos - start)); + const uint32_t size = HDRSZ + sizeof(sbufsz) + sbufsz; + chck_buffer_seek(out, 0, SEEK_SET); + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rread}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !chck_buffer_write_int(&sbufsz, sizeof(sbufsz), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Twrite(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint64_t offset; + uint32_t fid, count; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&offset, sizeof(offset), in) || + !chck_buffer_read_int(&count, sizeof(count), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Twrite %u %"PRIu64" %u\n", fid, offset, count); +#endif + + if (pi9->procs.write && !pi9->procs.write(pi9, tag, fid, offset, count, (count > 0 ? in->curpos : NULL))) + return false; + + const uint32_t size = HDRSZ + sizeof(count); + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rwrite}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !chck_buffer_write_int(&count, sizeof(count), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Tclunk(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tclunk %u %u\n", tag, fid); +#endif + + if (pi9->procs.clunk && !pi9->procs.clunk(pi9, tag, fid)) + return false; + + if (!chck_buffer_write_int(&HDRSZ, sizeof(HDRSZ), out) || + !chck_buffer_write_int((uint8_t[]){Rclunk}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Tremove(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tremove %u %u\n", tag, fid); +#endif + + if (pi9->procs.remove && !pi9->procs.remove(pi9, tag, fid)) + return false; + + if (!chck_buffer_write_int(&HDRSZ, sizeof(HDRSZ), out) || + !chck_buffer_write_int((uint8_t[]){Rremove}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Tstat(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid; + if (!chck_buffer_read_int(&fid, sizeof(fid), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Tstat %u %u\n", tag, fid); +#endif + + chck_buffer_seek(out, HDRSZ + sizeof(uint16_t), SEEK_SET); + void *start = out->curpos; + + struct pi9_stat *stat = NULL; + if (pi9->procs.stat && !pi9->procs.stat(pi9, tag, fid, &stat)) + return false; + + if (stat && !pi9_write_stat(stat, out)) + goto err_write; + + // too big + if (out->curpos - start > 0xFFFF) + goto err_write; + + const uint16_t sbufsz = (out->curpos - start); + const uint32_t size = HDRSZ + sizeof(sbufsz) + sbufsz; + chck_buffer_seek(out, 0, SEEK_SET); + if (!chck_buffer_write_int(&size, sizeof(size), out) || + !chck_buffer_write_int((uint8_t[]){Rstat}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out) || + !chck_buffer_write_int(&sbufsz, sizeof(sbufsz), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static bool +op_Twstat(struct pi9 *pi9, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + uint32_t fid; + uint16_t sbufsz; + if (!chck_buffer_read_int(&fid, sizeof(fid), in) || + !chck_buffer_read_int(&sbufsz, sizeof(sbufsz), in)) + goto err_read; + +#if VERBOSE + fprintf(stderr, "Twstat %u %u %u\n", tag, fid, sbufsz); +#endif + + struct pi9_stat stat = {0}; + if (!read_stat(&stat, in)) + goto err_read; + + if (pi9->procs.twstat && !pi9->procs.twstat(pi9, tag, fid, &stat)) + return false; + + if (!chck_buffer_write_int(&HDRSZ, sizeof(HDRSZ), out) || + !chck_buffer_write_int((uint8_t[]){Rwstat}, sizeof(uint8_t), out) || + !chck_buffer_write_int(&tag, sizeof(tag), out)) + goto err_write; + + return true; + +err_read: + pi9_write_error(tag, ERR_READ, out); + return false; +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +} + +static inline bool +call_op(struct pi9 *pi9, enum op op, uint16_t tag, struct chck_buffer *in, struct chck_buffer *out) +{ + // check opcode range, and only allow T opcodes (% 2), also check that message meets minimum size + if (op <= OPFIRST || op >= OPLAST || op % 2 != 0 || in->size < ops[op].msz) + goto err_unknown_op; + + if (op == Terror) { +#if VERBOSE + fprintf(stderr, "Got error from the client.\n"); +#endif + return true; + } + + if (out->size < ops[op].msz && !chck_buffer_resize(out, ops[op].msz)) + goto err_write; + + return ops[op].cb(pi9, tag, in, out); + +err_write: + pi9_write_error(tag, ERR_WRITE, out); + return false; +err_unknown_op: + pi9_write_error(tag, ERR_UNKNOWN_OP, out); + return false; +} + +static bool +read_msg(struct pi9 *pi9, int32_t fd, struct chck_buffer *in, struct chck_buffer *out) +{ + assert(pi9 && fd >= 0); + + if (chck_buffer_fill_from_fd(fd, 1, pi9->msize, in) < HDRSZ) + return false; + + uint32_t size; + if (!chck_buffer_read_int(&size, sizeof(size), in) || size < HDRSZ) + return false; + + if (in->size < size) { + const size_t to_read = size - in->size; + chck_buffer_seek(in, 0, SEEK_END); + if (chck_buffer_fill_from_fd(fd, 1, to_read, in) < to_read) + return false; + chck_buffer_seek(in, sizeof(size), SEEK_SET); + } + + uint8_t op; + uint16_t tag; + if (!chck_buffer_read_int(&op, sizeof(uint8_t), in) || + !chck_buffer_read_int(&tag, sizeof(uint16_t), in)) + return false; + +#if VERBOSE + fprintf(stderr, "Read message of size: %u (%u : %u)\n", size, op, tag); +#endif + for (uint32_t i = 0; i < size; ++i) + putc(*(char*)(in->buffer + i), stderr); + putc('\n', stderr); + + return call_op(pi9, op, tag, in, out); +} + +static inline bool +write_msg(int32_t fd, struct chck_buffer *out) +{ + assert(fd >= 0 && out->buffer); + const uint32_t size = *(uint32_t*)out->buffer; +#if VERBOSE + fprintf(stderr, "Write message of size: %u\n", size); +#endif + return write(fd, out->buffer, size) == size; +} + +bool +pi9_write_stat(struct pi9_stat *stat, struct chck_buffer *out) +{ + const size_t size = STATHDRSZ + stat->name.size + stat->uid.size + stat->gid.size + stat->muid.size; + + // too big + if (size > 0xFFFF) + return false; + + const uint16_t size16 = size; + return (chck_buffer_write_int(&size16, sizeof(size16), out) && + chck_buffer_write_int(&stat->type, sizeof(stat->type), out) && + chck_buffer_write_int(&stat->dev, sizeof(stat->dev), out) && + write_qid(&stat->qid, out) && + chck_buffer_write_int(&stat->mode, sizeof(stat->mode), out) && + chck_buffer_write_int(&stat->atime, sizeof(stat->atime), out) && + chck_buffer_write_int(&stat->mtime, sizeof(stat->mtime), out) && + chck_buffer_write_int(&stat->length, sizeof(stat->length), out) && + chck_buffer_write_string_of_type(stat->name.data, stat->name.size, sizeof(uint16_t), out) && + chck_buffer_write_string_of_type(stat->uid.data, stat->uid.size, sizeof(uint16_t), out) && + chck_buffer_write_string_of_type(stat->gid.data, stat->gid.size, sizeof(uint16_t), out) && + chck_buffer_write_string_of_type(stat->muid.data, stat->muid.size, sizeof(uint16_t), out)); +} + +size_t +pi9_write(const void *src, size_t size, size_t nmemb, struct chck_buffer *out) +{ + return chck_buffer_write(src, size, nmemb, out); +} + +void +pi9_stat_release(struct pi9_stat *stat) +{ + if (!stat) + return; + + struct pi9_string *fields[4] = { &stat->name, &stat->uid, &stat->gid, &stat->muid }; + for (uint32_t i = 0; i < 4; ++i) + pi9_string_release(fields[i]); +} + +bool +pi9_process(struct pi9 *pi9, int32_t fd) +{ + assert(pi9 && fd >= 0 && pi9->in && pi9->out); + + chck_buffer_seek(pi9->in, 0, SEEK_SET); + chck_buffer_seek(pi9->out, 0, SEEK_SET); + + bool ret = true; + if (!read_msg(pi9, fd, pi9->in, pi9->out)) { + if (pi9->out->curpos == pi9->out->buffer) + pi9_write_error(NOTAG, ERR_READ, pi9->out); + ret = false; + } + + if (!write_msg(fd, pi9->out)) { + pi9_write_error(NOTAG, ERR_WRITE, pi9->out); + write_msg(fd, pi9->out); + ret = false; + } + + return ret; +} + +void +pi9_release(struct pi9 *pi9) +{ + if (!pi9) + return; + + chck_buffer_release(pi9->out); + chck_buffer_release(pi9->in); + memset(pi9, 0, sizeof(struct pi9)); +} + +bool +pi9_init(struct pi9 *pi9, uint32_t msize, struct pi9_procs *procs, void *userdata) +{ + assert(pi9); + memset(pi9, 0, sizeof(struct pi9)); + memcpy(&pi9->procs, procs, sizeof(struct pi9_procs)); + pi9->msize = (msize > 0 ? msize : 8192); + pi9->userdata = userdata; + + if (!(pi9->in = malloc(sizeof(struct chck_buffer)))) + goto fail; + + if (!(pi9->out = malloc(sizeof(struct chck_buffer)))) + goto fail; + + if (!chck_buffer(pi9->in, pi9->msize, CHCK_ENDIANESS_LITTLE)) + goto fail; + + if (!chck_buffer(pi9->out, pi9->msize, CHCK_ENDIANESS_LITTLE)) + goto fail; + + return true; + +fail: + pi9_release(pi9); + return false; +} + +#undef pi9_write_error + +void +pi9_write_error(uint16_t tag, enum pi9_error error, struct chck_buffer *out) +{ + assert(out); + const int32_t size = HDRSZ + sizeof(uint16_t) + errors[error].size; + chck_buffer_seek(out, 0, SEEK_SET); + chck_buffer_write_int(&size, sizeof(size), out); + chck_buffer_write_int((uint8_t[]){ Rerror }, sizeof(uint8_t), out); + chck_buffer_write_int(&tag, sizeof(tag), out); + chck_buffer_write_string_of_type(errors[error].msg, errors[error].size, sizeof(uint16_t), out); + fprintf(stderr, "%s\n", errors[error].msg); +} diff --git a/src/pi9.h b/src/pi9.h new file mode 100644 index 0000000..45f04f8 --- /dev/null +++ b/src/pi9.h @@ -0,0 +1,120 @@ +#ifndef __pi9_h__ +#define __pi9_h__ + +#include +#include "pi9_string.h" + +static const uint32_t PI9_NOFID = (uint32_t)~0; + +struct chck_buffer; + +struct pi9_qid { + uint8_t type; + uint32_t vers; + uint64_t path; +}; + +struct pi9_stat { + uint16_t type; + uint32_t dev; + struct pi9_qid qid; + uint32_t mode; + uint32_t atime; + uint32_t mtime; + uint64_t length; + struct pi9_string name; + struct pi9_string uid; + struct pi9_string gid; + struct pi9_string muid; +}; + +struct pi9 { + void *userdata; + struct chck_buffer *in, *out; + + uint32_t msize; + + struct pi9_procs { + bool (*auth)(struct pi9 *pi9, uint16_t tag, uint32_t afid, const struct pi9_string *uname, const struct pi9_string *aname, struct pi9_qid **qid); + bool (*attach)(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint32_t afid, const struct pi9_string *uname, const struct pi9_string *aname, struct pi9_qid **qid); + bool (*flush)(struct pi9 *pi9, uint16_t tag, uint16_t oldtag); + bool (*walk)(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint32_t newfid, uint16_t nwname, const struct pi9_string *walks, struct pi9_qid **qids, uint16_t *out_nwqid); + bool (*open)(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint8_t mode, struct pi9_qid **out_qid, uint32_t *out_iounit); + bool (*create)(struct pi9 *pi9, uint16_t tag, uint32_t fid, const struct pi9_string *name, uint32_t perm, uint8_t mode, struct pi9_qid **out_qid, uint32_t *out_iounit); + bool (*read)(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint64_t offset, uint32_t count); + bool (*write)(struct pi9 *pi9, uint16_t tag, uint32_t fid, uint64_t offset, uint32_t count, const void *data); + bool (*clunk)(struct pi9 *pi9, uint16_t tag, uint32_t fid); + bool (*remove)(struct pi9 *pi9, uint16_t tag, uint32_t fid); + bool (*stat)(struct pi9 *pi9, uint16_t tag, uint32_t fid, struct pi9_stat **out_stat); + bool (*twstat)(struct pi9 *pi9, uint16_t tag, uint32_t fid, const struct pi9_stat *stat); + } procs; +}; + +// from libc.h +enum { + PI9_OREAD = 0x0000, // open for read + PI9_OWRITE = 0x0001, // write + PI9_ORDWR = 0x0002, // read and write + PI9_OEXEC = 0x0003, // execute, == read but check execute permission + PI9_OTRUNC = 0x0010, // or'ed in (except for exec), truncate file first + PI9_OCEXEC = 0x0020, // or'ed in, close on exec + PI9_ORCLOSE = 0x0040, // or'ed in, remove on close + PI9_ODIRECT = 0x0080, // or'ed in, direct access + PI9_ONONBLOCK = 0x0100, // or'ed in, non-blocking call + PI9_OEXCL = 0x1000, // or'ed in, exclusive use (create only) + PI9_OLOCK = 0x2000, // or'ed in, lock after opening + PI9_OAPPEND = 0x4000 // or'ed in, append only +}; + +// bits in pi9_qid.type +enum { + PI9_QTDIR = 0x80, // type bit for directories + PI9_QTAPPEND = 0x40, // type bit for append only files + PI9_QTEXCL = 0x20, // type bit for exclusive use files + PI9_QTMOUNT = 0x10, // type bit for mounted channel + PI9_QTAUTH = 0x08, // type bit for authentication file + PI9_QTTMP = 0x04, // type bit for non-backed-up file + PI9_QTSYMLINK = 0x02, // type bit for symbolic link + PI9_QTFILE = 0x00 // type bits for plain file +}; + +// bits in pi9_stat.mode +enum { + PI9_DMEXEC = 0x1, // mode bit for execute permission + PI9_DMWRITE = 0x2, // mode bit for write permission + PI9_DMREAD = 0x4, // mode bit for read permission + + PI9_DMDIR = 0x80000000, // mode bit for directories + PI9_DMAPPEND = 0x40000000, // mode bit for append only files + PI9_DMEXCL = 0x20000000, // mode bit for exclusive use files + PI9_DMMOUNT = 0x10000000, // mode bit for mounted channel + PI9_DMAUTH = 0x08000000, // mode bit for authentication file + PI9_DMTMP = 0x04000000, // mode bit for non-backed-up file +}; + +enum pi9_error { + ERR_READ, + ERR_WRITE, + ERR_NO_AUTH, + ERR_NOT_DIRECTORY, + ERR_NO_FID, + ERR_FID_IN_USE, + ERR_NOT_ALLOWED, + ERR_UNKNOWN_OP, + ERR_OUT_OF_MEMORY, + ERR_LAST, +}; + +bool pi9_write_stat(struct pi9_stat *stat, struct chck_buffer *out); +void pi9_write_error(uint16_t tag, enum pi9_error error, struct chck_buffer *out); +size_t pi9_write(const void *src, size_t size, size_t nmemb, struct chck_buffer *out); +void pi9_stat_release(struct pi9_stat *stat); +bool pi9_process(struct pi9 *pi9, int32_t fd); +bool pi9_init(struct pi9 *pi9, uint32_t msize, struct pi9_procs *procs, void *userdata); +void pi9_release(struct pi9 *pi9); + +#define pi9_write_error(t, e, o) \ +{ fprintf(stderr, "%s (%s) @ %u: ", ((strrchr(__FILE__, '/') ?: __FILE__ - 1) + 1), __FUNCTION__, __LINE__); \ + pi9_write_error(t, e, o); } + +#endif /* __pi9_h__ */ diff --git a/src/pi9_string.c b/src/pi9_string.c new file mode 100644 index 0000000..e7edaba --- /dev/null +++ b/src/pi9_string.c @@ -0,0 +1,58 @@ +#include +#include +#include + +#include "pi9_string.h" + +static inline char* +ccopy(const char *str, size_t len) +{ + char *cpy = calloc(1, len); + return (cpy ? memcpy(cpy, str, len) : NULL); +} + +void +pi9_string_release(struct pi9_string *string) +{ + assert(string); + + if (string->is_heap && string->data) + free(string->data); + + string->data = NULL; + string->size = 0; +} + +bool +pi9_string_set_cstr_with_length(struct pi9_string *string, const char *data, uint16_t length, bool is_heap) +{ + assert(string); + + char *copy = (char*)data; + if (is_heap && data && length > 0 && !(copy = ccopy(data, length))) + return false; + + pi9_string_release(string); + string->is_heap = is_heap; + string->data = (length > 0 ? copy : NULL); + string->size = length; + return true; +} + +bool +pi9_string_set_cstr(struct pi9_string *string, const char *data, bool is_heap) +{ + assert(string); + return pi9_string_set_cstr_with_length(string, data, (data ? strlen(data) : 0), is_heap); +} + +bool +pi9_string_set(struct pi9_string *string, const struct pi9_string *other, bool is_heap) +{ + if (string->data == other->data) { + string->size = other->size; + return true; + } + + return pi9_string_set_cstr_with_length(string, other->data, other->size, is_heap); +} diff --git a/src/pi9_string.h b/src/pi9_string.h new file mode 100644 index 0000000..5064492 --- /dev/null +++ b/src/pi9_string.h @@ -0,0 +1,44 @@ +#ifndef __pi9_string_h__ +#define __pi9_string_h__ + +#include +#include +#include + +struct pi9_string { + char *data; + uint16_t size; + bool is_heap; +}; + +static inline bool +pi9_string_eq(const struct pi9_string *a, const struct pi9_string *b) +{ + return (a->data == b->data) || (a->size == b->size && !memcmp(a->data, b->data, a->size)); +} + +static inline bool +pi9_string_eq_cstr(const struct pi9_string *a, const char *cstr) +{ + return (cstr == a->data) || (a->data && cstr && !strcmp(a->data, cstr)); +} + +static inline bool +pi9_cstreq(const char *a, const char *b) +{ + return (a == b) || (a && b && !strcmp(a, b)); +} + +static inline bool +pi9_cstrneq(const char *a, const char *b, size_t len) +{ + return (a == b) || (a && b && !strncmp(a, b, len)); +} + + +void pi9_string_release(struct pi9_string *string); +bool pi9_string_set_cstr(struct pi9_string *string, const char *data, bool is_heap); +bool pi9_string_set_cstr_with_length(struct pi9_string *string, const char *data, uint16_t length, bool is_heap); +bool pi9_string_set(struct pi9_string *string, const struct pi9_string *other, bool is_heap); + +#endif /* __pi9_string_h__ */