diff --git a/README b/README index e9f6601..1662736 100644 --- a/README +++ b/README @@ -3,7 +3,7 @@ Author: Andreas Fredriksson Uploader: deplinenoise@gmail.com Type: dev/cross Architecture: m68k-amigaos -Version: 1.0 +Version: 1.1 ABOUT =============================================================================== @@ -131,7 +131,10 @@ Arguments: Options: -fsroot Specifies the file serving directory. The executable - must live in this directory as well. (default: cwd) + must live in this directory as well. (default: the + directory exe_path names, or cwd when it names none. + An Amiga path like c:info names no host directory; a + Windows drive path like c:\dir\prog does) -port The TCP port to connect to (default: 7001) diff --git a/src/controller.c b/src/controller.c index df5f18c..6c19298 100644 --- a/src/controller.c +++ b/src/controller.c @@ -288,7 +288,10 @@ static const char * const usage_string = "\n" "Options:\n" " -fsroot Specifies the file serving directory. The executable\n" -" must live in this directory as well. (default: cwd)\n" +" must live in this directory as well. (default: the\n" +" directory exe_path names, or cwd when it names none.\n" +" An Amiga path like c:info names no host directory; a\n" +" Windows drive path like c:\\dir\\prog does)\n" "\n" " -port The TCP port to connect to (default: 7001)\n" "\n" @@ -410,6 +413,38 @@ int main(int argc, char** argv) goto cleanup; } + /* Without an explicit -fsroot, derive it from exe_path's directory so + * `rl-controller host some/dir/Exe` works without a separate -fsroot. An + * absolute Amiga path (e.g. c:list) is run directly on the target rather + * than served from the host, and has no directory to derive. */ + char derived_fsroot[sizeof(ctrl.root_handle.native_path)]; + + if (!fsroot[0]) + { + const char *exe_name; + + switch (rl_split_exe_path(sizeof(derived_fsroot), derived_fsroot, &exe_name, ctrl.executable)) + { + case RL_EXE_PATH_SPLIT: + fsroot = derived_fsroot; + ctrl.executable = exe_name; + break; + + case RL_EXE_PATH_DIR_TOO_LONG: + RL_LOG_CONSOLE(("the directory part of '%s' is longer than %d characters; pass -fsroot \n", + ctrl.executable, (int) sizeof(derived_fsroot) - 1)); + goto cleanup; + + case RL_EXE_PATH_NO_NAME: + RL_LOG_CONSOLE(("'%s' ends with a path separator and names no executable\n", + ctrl.executable)); + goto cleanup; + + default: + break; + } + } + #ifdef RL_WIN32 if (0 != rl_init_socket()) goto cleanup; @@ -439,13 +474,25 @@ int main(int argc, char** argv) } } - /* make sure root path stored doesn't contain a path separator */ + /* Make sure the root path stored doesn't end with a path separator, since + * the file server joins it with one when it resolves a name against the + * root. A root that is nothing but a separator needs a spelling that + * survives that join: on Win32 the empty string, which resolves against the + * current drive, and elsewhere the separator itself, since realpath("") + * fails and would deny every request. */ { size_t dirlen = strlen(ctrl.root_handle.native_path); - + if (dirlen > 0 && ctrl.root_handle.native_path[dirlen-1] == NATIVE_PATH_TERMINATOR) { +#ifdef RL_WIN32 ctrl.root_handle.native_path[dirlen-1] = '\0'; +#else + if (dirlen > 1) + { + ctrl.root_handle.native_path[dirlen-1] = '\0'; + } +#endif } } diff --git a/src/util.c b/src/util.c index 31b4fcd..f73ed6a 100644 --- a/src/util.c +++ b/src/util.c @@ -648,6 +648,75 @@ int rl_string_copy(size_t dest_size, char *dest, const char *source) return *source ? -1 : 0; } +static INLINE int is_path_separator(char ch) +{ + if ('/' == ch) + return 1; + +#ifdef RL_WIN32 + if ('\\' == ch) + return 1; +#endif + + return 0; +} + +/* + * Splits the directory part off an executable path so that it can serve as the + * file serving root, leaving *name_out pointing at the name the target is asked + * to launch. Separators are '/', and '\' on Win32 as well. + */ +int rl_split_exe_path(size_t dir_size, char *dir_out, const char **name_out, const char *exe_path) +{ + const char *colon = rl_strchr(exe_path, ':'); + const char *last_separator = NULL; + const char *cursor; + size_t dirlen; + + if (NULL != colon) + { +#ifdef RL_WIN32 + /* A drive prefix is a colon in second position followed by a separator, + * and names a directory on this machine. A colon anywhere else makes + * this one of the target's own paths, e.g. "c:list", which it resolves + * without any help from the file server. */ + if (exe_path + 1 != colon || !is_path_separator(colon[1])) + return RL_EXE_PATH_NO_DIRECTORY; +#else + return RL_EXE_PATH_NO_DIRECTORY; +#endif + } + + for (cursor = exe_path; '\0' != *cursor; ++cursor) + { + if (is_path_separator(*cursor)) + last_separator = cursor; + } + + if (NULL == last_separator) + return RL_EXE_PATH_NO_DIRECTORY; + + if ('\0' == last_separator[1]) + return RL_EXE_PATH_NO_NAME; + + dirlen = (size_t) (last_separator - exe_path); + + /* The filesystem root, and the root of a drive, are nothing but their + * separator, so it stays part of the directory: "/prog" would otherwise + * leave no directory at all, and "c:\prog" the drive-relative "c:". */ + if (0 == dirlen || (NULL != colon && colon + 1 == last_separator)) + ++dirlen; + + if (dirlen + 1 > dir_size) + return RL_EXE_PATH_DIR_TOO_LONG; + + rl_memcpy(dir_out, exe_path, dirlen); + dir_out[dirlen] = '\0'; + *name_out = last_separator + 1; + + return RL_EXE_PATH_SPLIT; +} + static INLINE void strbuf_append_ch(rl_strbuf_t *buf, char ch) { if (buf->buffer != buf->max) diff --git a/src/util.h b/src/util.h index a2df27a..e7b8fda 100644 --- a/src/util.h +++ b/src/util.h @@ -233,6 +233,19 @@ rl_dict_destroy(rl_dict_t *dict); /* String utilities */ +/* + * Result of rl_split_exe_path(). + */ +enum +{ + RL_EXE_PATH_NO_DIRECTORY, /* a bare name, or a path the target resolves itself */ + RL_EXE_PATH_SPLIT, /* dir_out holds the directory, *name_out the name */ + RL_EXE_PATH_DIR_TOO_LONG, /* the directory part does not fit in dir_out */ + RL_EXE_PATH_NO_NAME /* the path ends with a separator and names nothing */ +}; + +int rl_split_exe_path(size_t dir_size, char *dir_out, const char **name_out, const char *exe_path); + typedef struct rl_strbuf_tag { char *buffer; diff --git a/test/rlaunch_test.c b/test/rlaunch_test.c index c3ef2fe..5e0dd6d 100644 --- a/test/rlaunch_test.c +++ b/test/rlaunch_test.c @@ -392,6 +392,141 @@ UTEST(strbuf, append_str_len_stops_at_the_requested_length) ASSERT_STREQ("abc", buffer); } +/* ------------------------------------------------------------------------- + * util.c -- executable path splitting + * + * Without -fsroot the controller serves the directory this splits off, and + * asks the target to launch what is left, so a wrong answer here means the + * wrong directory is exported to the Amiga. + * ------------------------------------------------------------------------- */ + +UTEST(split_exe_path, a_bare_name_has_no_directory) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY, + rl_split_exe_path(sizeof(dir), dir, &name, "Prog")); + ASSERT_TRUE(NULL == name); +} + +UTEST(split_exe_path, splits_the_last_component_off) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "some/dir/Prog")); + ASSERT_STREQ("some/dir", dir); + ASSERT_STREQ("Prog", name); + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "./Prog")); + ASSERT_STREQ(".", dir); + ASSERT_STREQ("Prog", name); +} + +/* "/Prog" has to serve the root itself, not the empty string, which reads as + * "no -fsroot given" and would silently serve the current directory instead. */ +UTEST(split_exe_path, an_executable_in_the_root_keeps_the_separator) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "/Prog")); + ASSERT_STREQ("/", dir); + ASSERT_STREQ("Prog", name); + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "/opt/Prog")); + ASSERT_STREQ("/opt", dir); + ASSERT_STREQ("Prog", name); +} + +/* Paths with a device prefix are resolved by the target itself; there is no + * host-side directory in them to serve. */ +UTEST(split_exe_path, a_target_path_has_no_directory) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY, + rl_split_exe_path(sizeof(dir), dir, &name, "c:list")); + ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY, + rl_split_exe_path(sizeof(dir), dir, &name, "DH0:tools/Prog")); +} + +UTEST(split_exe_path, a_trailing_separator_names_no_executable) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_NO_NAME, + rl_split_exe_path(sizeof(dir), dir, &name, "some/dir/")); + ASSERT_EQ(RL_EXE_PATH_NO_NAME, + rl_split_exe_path(sizeof(dir), dir, &name, "/")); +} + +UTEST(split_exe_path, a_directory_that_does_not_fit_is_reported) +{ + char dir[4]; + const char *name = NULL; + + /* Three characters and the terminator is an exact fit. */ + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "abc/Prog")); + ASSERT_STREQ("abc", dir); + + ASSERT_EQ(RL_EXE_PATH_DIR_TOO_LONG, + rl_split_exe_path(sizeof(dir), dir, &name, "abcd/Prog")); +} + +#if defined(RL_WIN32) +UTEST(split_exe_path, a_drive_letter_path_is_a_host_path) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "C:\\Users\\user\\some.exe")); + ASSERT_STREQ("C:\\Users\\user", dir); + ASSERT_STREQ("some.exe", name); + + /* The drive's own root keeps its separator: "C:" alone would resolve + * against whatever the current directory on that drive happens to be. */ + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "C:\\some.exe")); + ASSERT_STREQ("C:\\", dir); + ASSERT_STREQ("some.exe", name); + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "dir\\Prog")); + ASSERT_STREQ("dir", dir); + ASSERT_STREQ("Prog", name); + + /* A colon that is not a drive prefix still belongs to the target. */ + ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY, + rl_split_exe_path(sizeof(dir), dir, &name, "C:dir")); +} +#else +/* A backslash is an ordinary filename character here, and a drive letter is + * not a thing, so "C:\..." is just another path only the target can resolve. */ +UTEST(split_exe_path, a_backslash_is_not_a_separator) +{ + char dir[260]; + const char *name = NULL; + + ASSERT_EQ(RL_EXE_PATH_NO_DIRECTORY, + rl_split_exe_path(sizeof(dir), dir, &name, "C:\\Users\\user\\some.exe")); + + ASSERT_EQ(RL_EXE_PATH_SPLIT, + rl_split_exe_path(sizeof(dir), dir, &name, "dir/odd\\name")); + ASSERT_STREQ("dir", dir); + ASSERT_STREQ("odd\\name", name); +} +#endif + /* ------------------------------------------------------------------------- * file_server.c -- path handling * ------------------------------------------------------------------------- */ @@ -416,6 +551,21 @@ UTEST(fix_path, joins_against_the_served_root) ASSERT_STREQ(expected, dest); } +#if defined(RL_POSIX) +/* + * An executable in the filesystem root leaves "/" as the served root. The join + * doubles the separator there, which POSIX resolves to the same file, and the + * root check has to accept the result rather than deny every request. + */ +UTEST(fix_path, joins_against_the_filesystem_root) +{ + char dest[260]; + + ASSERT_EQ(0, fix_path(dest, sizeof(dest), "rlaunch-no-such-entry", "/")); + ASSERT_STREQ("//rlaunch-no-such-entry", dest); +} +#endif + UTEST(fix_path, rejects_traversal_outside_the_served_root) { char dest[260];