GUACAMOLE-637: Add unit tests for RDP filesystem path normalization.
diff --git a/configure.ac b/configure.ac
index 1323123..505f696 100644
--- a/configure.ac
+++ b/configure.ac
@@ -151,6 +151,10 @@
 AC_SUBST([COMMON_SSH_LTLIB],   '$(top_builddir)/src/common-ssh/libguac_common_ssh.la')
 AC_SUBST([COMMON_SSH_INCLUDE], '-I$(top_srcdir)/src/common-ssh')
 
+# RDP support
+AC_SUBST([LIBGUAC_CLIENT_RDP_LTLIB],   '$(top_builddir)/src/protocols/rdp/libguac-client-rdp.la')
+AC_SUBST([LIBGUAC_CLIENT_RDP_INCLUDE], '-I$(top_srcdir)/src/protocols/rdp')
+
 # Terminal emulator
 AC_SUBST([TERMINAL_LTLIB],   '$(top_builddir)/src/terminal/libguac_terminal.la')
 AC_SUBST([TERMINAL_INCLUDE], '-I$(top_srcdir)/src/terminal $(PANGO_CFLAGS) $(PANGOCAIRO_CFLAGS) $(COMMON_INCLUDE)')
@@ -1331,6 +1335,7 @@
                  src/pulse/Makefile
                  src/protocols/kubernetes/Makefile
                  src/protocols/rdp/Makefile
+                 src/protocols/rdp/tests/Makefile
                  src/protocols/ssh/Makefile
                  src/protocols/telnet/Makefile
                  src/protocols/vnc/Makefile])
diff --git a/src/protocols/rdp/.gitignore b/src/protocols/rdp/.gitignore
new file mode 100644
index 0000000..9f87ecb
--- /dev/null
+++ b/src/protocols/rdp/.gitignore
@@ -0,0 +1,8 @@
+
+# Auto-generated test runner and binary
+_generated_runner.c
+test_rdp
+
+# Autogenerated sources
+_generated_keymaps.c
+
diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am
index 05a7a3a..cbb6c4f 100644
--- a/src/protocols/rdp/Makefile.am
+++ b/src/protocols/rdp/Makefile.am
@@ -27,6 +27,7 @@
 ACLOCAL_AMFLAGS = -I m4
 
 lib_LTLIBRARIES = libguac-client-rdp.la
+SUBDIRS = . tests
 
 nodist_libguac_client_rdp_la_SOURCES = \
     _generated_keymaps.c
diff --git a/src/protocols/rdp/tests/Makefile.am b/src/protocols/rdp/tests/Makefile.am
new file mode 100644
index 0000000..a803b63
--- /dev/null
+++ b/src/protocols/rdp/tests/Makefile.am
@@ -0,0 +1,64 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim
+# into Makefile.in. Though the build system (GNU Autotools) automatically adds
+# its own license boilerplate to the generated Makefile.in, that boilerplate
+# does not apply to the transcluded portions of Makefile.am which are licensed
+# to you by the ASF under the Apache License, Version 2.0, as described above.
+#
+
+AUTOMAKE_OPTIONS = foreign 
+ACLOCAL_AMFLAGS = -I m4
+
+#
+# Unit tests for RDP support
+#
+
+check_PROGRAMS = test_rdp
+TESTS = $(check_PROGRAMS)
+
+test_rdp_SOURCES =      \
+    fs/normalize_path.c
+
+test_rdp_CFLAGS =                \
+    -Werror -Wall -pedantic      \
+    @LIBGUAC_CLIENT_RDP_INCLUDE@
+
+test_rdp_LDADD =               \
+    @CUNIT_LIBS@               \
+    @LIBGUAC_CLIENT_RDP_LTLIB@
+
+#
+# Autogenerate test runner
+#
+
+GEN_RUNNER = $(top_srcdir)/util/generate-test-runner.pl
+CLEANFILES = _generated_runner.c
+
+_generated_runner.c: $(test_rdp_SOURCES)
+	$(AM_V_GEN) $(GEN_RUNNER) $(test_rdp_SOURCES) > $@
+
+nodist_test_rdp_SOURCES = \
+    _generated_runner.c
+
+# Use automake's TAP test driver for running any tests
+LOG_DRIVER =                \
+    env AM_TAP_AWK='$(AWK)' \
+    $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh
+
diff --git a/src/protocols/rdp/tests/fs/normalize_path.c b/src/protocols/rdp/tests/fs/normalize_path.c
new file mode 100644
index 0000000..0d8f17f
--- /dev/null
+++ b/src/protocols/rdp/tests/fs/normalize_path.c
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "rdp_fs.h"
+
+#include <CUnit/CUnit.h>
+#include <stdlib.h>
+
+/**
+ * Test which verifies absolute Windows-style paths are correctly normalized to
+ * absolute paths with Windows separators and no relative components.
+ */
+void test_fs__normalize_absolute_windows() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\baz\\", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\..\\baz\\a\\..\\b", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\.\\bar\\baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\bar\\..\\..\\..\\..\\..\\..\\baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized));
+
+}
+
+/**
+ * Test which verifies absolute UNIX-style paths are correctly normalized to
+ * absolute paths with Windows separators and no relative components.
+ */
+void test_fs__normalize_absolute_unix() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../baz/", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../../baz/a/../b", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/./bar/baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo/bar/../../../../../../baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized));
+
+}
+
+/**
+ * Test which verifies absolute paths consisting of mixed Windows and UNIX path
+ * separators are correctly normalized to absolute paths with Windows
+ * separators and no relative components.
+ */
+void test_fs__normalize_absolute_mixed() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("/foo\\bar/..\\baz/", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\../../baz\\a\\..\\b", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz\\b", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo\\.\\bar/baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\foo\\bar\\baz", sizeof(normalized));
+
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path("\\foo/bar\\../..\\..\\..\\../..\\baz", normalized), 0)
+    CU_ASSERT_NSTRING_EQUAL(normalized, "\\baz", sizeof(normalized));
+
+}
+
+/**
+ * Test which verifies relative Windows-style paths are always rejected.
+ */
+void test_fs__normalize_relative_windows() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..\\foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo\\bar\\baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo\\bar\\baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..\\foo\\bar\\baz", normalized), 0)
+
+}
+
+/**
+ * Test which verifies relative UNIX-style paths are always rejected.
+ */
+void test_fs__normalize_relative_unix() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("..", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("./foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo/bar/baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("./foo/bar/baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo/bar/baz", normalized), 0)
+
+}
+
+/**
+ * Test which verifies relative paths consisting of mixed Windows and UNIX path
+ * separators are always rejected.
+ */
+void test_fs__normalize_relative_mixed() {
+
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("foo\\bar/baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(".\\foo/bar/baz", normalized), 0)
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path("../foo\\bar\\baz", normalized), 0)
+
+}
+
+/**
+ * Generates a dynamically-allocated path having the given number of bytes, not
+ * counting the null-terminator. The path will contain only Windows-style path
+ * separators. The returned path must eventually be freed with a call to
+ * free().
+ *
+ * @param length
+ *     The number of bytes to include in the generated path, not counting the
+ *     null-terminator.
+ *
+ * @return
+ *     A dynamically-allocated path containing the given number of bytes, not
+ *     counting the null-terminator. This path must eventually be freed with a
+ *     call to free().
+ */
+static char* generate_path(int length) {
+
+    int i;
+    char* input = malloc(length + 1);
+
+    /* Fill path with \x\x\x\x\x\x\x\x\x\x\... */
+    for (i = 0; i < length; i++) {
+        input[i] = (i % 2 == 0) ? '\\' : 'x';
+    }
+
+    /* Add null terminator */
+    input[length] = '\0';
+
+    return input;
+
+}
+
+/**
+ * Test which verifies that paths exceeding the maximum path length are
+ * rejected.
+ */
+void test_fs__normalize_long() {
+
+    char* input;
+    char normalized[GUAC_RDP_FS_MAX_PATH];
+
+    /* Exceeds maximum length by a factor of 2 */
+    input = generate_path(GUAC_RDP_FS_MAX_PATH*2);
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0);
+    free(input);
+
+    /* Exceeds maximum length by one byte */
+    input = generate_path(GUAC_RDP_FS_MAX_PATH);
+    CU_ASSERT_NOT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0);
+    free(input);
+
+    /* Exactly maximum length */
+    input = generate_path(GUAC_RDP_FS_MAX_PATH - 1);
+    CU_ASSERT_EQUAL(guac_rdp_fs_normalize_path(input, normalized), 0);
+    free(input);
+
+}
+