$(filter-out %~ $(call FIND_DIRS,${1}),$(wildcard ${1}/* ${1}/*/* ${1}/*/*/* ${1}/*/*/*/* ${1}/*/*/*/*/* ${1}/*/*/*/*/*/* ))
endef
+# FIND_FILES_SUFFIX - find all the files with a given suffix
+define FIND_FILES_SUFFIX
+$(foreach d,$(wildcard $(1:=/*)),$(call FIND_FILES_SUFFIX,$d,$2) $(filter $(subst *,%,$2),$d))
+endef
+
# ADD_CLEAN_RULE - Parameterized "function" that adds a new rule and phony
# target for cleaning the specified target (removing its build-generated
# files).
UNUSED char *data, UNUSED size_t data_used, char *in, UNUSED size_t inlen)
{
if (fr_file_unlink(in) < 0) RETURN_COMMAND_ERROR();
- if (fr_file_touch(in, 0644) < 0) RETURN_COMMAND_ERROR();
+ if (fr_file_touch(NULL, in, 0644, true, 0755) <= 0) RETURN_COMMAND_ERROR();
RETURN_OK(0);
}
fr_dict_free(&dict);
unlang_free();
xlat_free();
- fr_strerror_free();
- if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(receipt_file, 0644) < 0)) {
+ if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(NULL, receipt_file, 0644, true, 0755) <= 0)) {
fr_perror("unit_test_attribute");
ret = EXIT_FAILURE;
}
+ /*
+ * Must be last, we still need the errors
+ * from fr_file_touch.
+ */
+ fr_strerror_free();
+
return ret;
}
fr_strerror_free();
- if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(receipt_file, 0644) < 0)) {
+ if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(NULL, receipt_file, 0644, true, 0755) <= 0)) {
fr_perror("unit_test_map");
ret = EXIT_FAILURE;
}
*/
trigger_exec_free();
+ if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(NULL, receipt_file, 0644, true, 0755) <= 0)) {
+ fr_perror("unit_test_module");
+ ret = EXIT_FAILURE;
+ }
+
/*
* Explicitly cleanup the buffer used for storing syserror messages
* This cuts down on address sanitiser output on error.
*/
fr_syserror_free();
- if (receipt_file && (ret == EXIT_SUCCESS) && (fr_file_touch(receipt_file, 0644) < 0)) {
- fr_perror("unit_test_module");
- ret = EXIT_FAILURE;
- }
-
/*
* Call pthread destructors. Which aren't normally
* called for the main thread.
#include <freeradius-devel/util/misc.h>
#include <freeradius-devel/util/strerror.h>
#include <freeradius-devel/util/syserror.h>
+#include <freeradius-devel/util/talloc.h>
#include <freeradius-devel/util/time.h>
#include <ctype.h>
return result; /* 0 is OK, !0 is !OK, just like memcmp */
}
+static ssize_t _fr_mkdir(int *fd_out, char const *path, mode_t mode)
+{
+ int ret, fd;
+ char *p;
+
+ /*
+ * Try to make the path. If it exists, chmod it.
+ * If a path doesn't exist, that's OK. Otherwise
+ * return with an error.
+ *
+ * Directories permissions are initially set so
+ * that only we should have access. This prevents
+ * an attacker removing them and swapping them
+ * out for a link to somewhere else.
+ * We change them to the correct permissions later.
+ */
+ ret = mkdir(path, 0700);
+ if (ret >= 0) {
+ fd = open(path, O_DIRECTORY);
+ if (fd < 0) {
+ fr_strerror_printf("Failed opening directory we created: %s",
+ fr_syserror(errno));
+ mkdir_error:
+ p = strrchr(path, FR_DIR_SEP);
+ if (!p) return 0;
+
+ return -(p - path);
+ }
+
+ if (fchmod(fd, mode) < 0) {
+ fr_strerror_printf("Failed setting permissions on "
+ "directory we created: %s", fr_syserror(errno));
+ close(fd);
+ goto mkdir_error;
+ }
+ *fd_out = fd;
+ return PATH_MAX;
+ }
+
+ /*
+ * EEXIST is only OK when we're calling
+ * mkdir on the whole path, and it exists
+ * which should have been caught by
+ * fr_mkdir before calling this function.
+ */
+ if (errno != ENOENT) {
+ fr_strerror_printf("Unexpected error creating "
+ "directory: %s", fr_syserror(errno));
+ goto mkdir_error;
+ }
+
+ /*
+ * A component in the path path doesn't
+ * exist. Look for the LAST path name. Try
+ * to create that. If there's an error, we leave
+ * the path path as the one at which the
+ * error occured.
+ */
+ p = strrchr(path, FR_DIR_SEP);
+ if (!p || (p == path)) return -(p - path);
+
+ *p = '\0';
+ if (_fr_mkdir(fd_out, path, mode) < 0) return -(p - path);
+
+ /*
+ * At this point *fd_out, should be an FD
+ * for the containing directory.
+ */
+ if (mkdirat(*fd_out, p + 1, 0700) < 0) {
+ fr_strerror_printf("Failed creating directory: %s", fr_syserror(errno));
+ mkdirat_error:
+ close(*fd_out);
+ *fd_out = -1;
+ return -(p - path);
+ }
+
+ fd = openat(*fd_out, p + 1, O_DIRECTORY);
+ if (fd < 0) {
+ fr_strerror_printf("Failed opening directory we "
+ "created: %s", fr_syserror(errno));
+ goto mkdirat_error;
+ }
+
+ if (fchmod(fd, mode) < 0) {
+ fr_strerror_printf("Failed setting permissions on "
+ "directory we created: %s", fr_syserror(errno));
+ goto mkdirat_error;
+ }
+
+ /*
+ * Swap active *fd_out to point to the dir
+ * we just created.
+ */
+ close(*fd_out);
+ *fd_out = fd;
+
+ return PATH_MAX;
+}
+
+/** Create directories that are missing in the specified path
+ *
+ * @param[out] fd_out If not NULL, will contain a file descriptor
+ * for the deepest path component created.
+ * @param[in] path to populate with directories.
+ * @param[in] len Length of the path string.
+ * @param[in] mode for new directories.
+ * @return
+ * - >0 on success.
+ * - <= 0 on failure. Negative offset pointing to the
+ * path separator of the path component that caused the error.
+ */
+ssize_t fr_mkdir(int *fd_out, char const *path, size_t len, mode_t mode)
+{
+ char *our_path;
+ int fd = -1;
+ ssize_t slen;
+
+ /*
+ * Fast path (har har)
+ *
+ * Avoids duping the input for the
+ * common case.
+ */
+ fd = open(path, O_DIRECTORY);
+ if (fd >= 0) goto done;
+
+ /*
+ * Dup the amount of input path
+ * we need.
+ */
+ our_path = talloc_bstrndup(NULL, path, len);
+ if (!our_path) {
+ fr_strerror_printf("Out of memory");
+ return -1;
+ }
+
+ /*
+ * Call the recursive function to
+ * create any missing dirs in the
+ * specified path.
+ */
+ slen = _fr_mkdir(&fd, our_path, mode);
+ talloc_free(our_path);
+ if (slen <= 0) return slen;
+
+done:
+ if (fd_out) {
+ *fd_out = fd;
+ } else {
+ close(fd);
+ }
+
+ return len;
+}
+
/** Create an empty file
*
- * @param[in] filename path to file.
- * @param[in] mode Specifies the file mode bits be applied.
+ * @param[out] fd_out If not NULL, will contain a file descriptor
+ * for the file we just opened.
+ * @param[in] filename path to file.
+ * @param[in] mode Specifies the file mode bits be applied.
+ * @param[in] mkdir Whether we should create directories
+ * for any missing path components.
+ * @param[in] dir_mode Mode of any directories created.
* @return
- * - 0 on success.
- * - -1 on failure.
+ * - >0 on success.
+ * - <= 0 on failure. Error available in error stack (use fr_strerror())
*/
-int fr_file_touch(char const *filename, mode_t mode) {
+ssize_t fr_file_touch(int *fd_out, char const *filename, mode_t mode, bool mkdir, mode_t dir_mode) {
int fd;
fd = open(filename, O_WRONLY | O_CREAT, mode);
if (fd < 0) {
- fr_strerror_printf("Failed creating file \"%s\": %s", filename, fr_syserror(errno));
- return -1;
+ ssize_t slen = 0;
+ char *q;
+
+ if (mkdir && (errno == ENOENT) && (q = strrchr(filename, FR_DIR_SEP))) {
+ int dir_fd;
+
+ slen = fr_mkdir(&dir_fd, filename, q - filename, dir_mode);
+ if (slen <= 0) return slen;
+
+ fd = openat(dir_fd, q + 1, O_WRONLY | O_CREAT, mode);
+ if (fd >= 0) {
+ close(dir_fd);
+ close(fd);
+ return strlen(filename);
+ }
+ close(dir_fd);
+ slen = -(q - filename);
+ }
+ fr_strerror_printf("Failed creating file: %s", fr_syserror(errno));
+ return slen;
}
- close(fd);
+ if (fd_out) {
+ *fd_out = fd;
+ } else {
+ close(fd);
+ }
- return 0;
+ return strlen(filename);
}
/** Remove a regular file from the filesystem
void fr_quick_sort(void const *to_sort[], int min_idx, int max_idx, fr_cmp_t cmp);
int fr_digest_cmp(uint8_t const *a, uint8_t const *b, size_t length) CC_HINT(nonnull);
-int fr_file_touch(char const *filename, mode_t mode);
+ssize_t fr_mkdir(int *fd_out, char const *path, size_t len, mode_t mode);
+
+ssize_t fr_file_touch(int *fd_out, char const *filename, mode_t mode, bool mkdir, mode_t dir_mode);
+
int fr_file_unlink(char const *filename);
#ifdef __cplusplus
#
# All of the output files depend on the input files
#
-FILES.$(TEST) := $(addprefix $$(OUTPUT.$(TEST))/,$(notdir $(FILES)))
+FILES.$(TEST) := $(addprefix $$(OUTPUT.$(TEST))/,$(FILES))
#
# The output files also depend on the directory
#
TEST := test.unit
+#
+# Base directory for the test files
+#
+TEST_FILES_DIR := $(top_srcdir)/src/tests/unit
+
#
# The files are put here in order. Later tests need
# functionality from earlier test.
#
-FILES := \
- data_types.txt \
- radius_rfc.txt \
- radius_errors.txt \
- radius_extended.txt \
- radius_lucent.txt \
- radius_wimax.txt \
- radius_tunnel.txt \
- radius_vendor.txt \
- radius_unit.txt \
- radius_struct.txt \
- eap_aka_encode.txt \
- eap_aka_decode.txt \
- eap_aka_error.txt \
- eap_sim_encode.txt \
- eap_sim_decode.txt \
- eap_sim_error.txt \
- dhcpv4.txt \
- dhcpv6_microsoft.txt \
- dhcpv6_rfc3315.txt \
- dhcpv6_rfc3319.txt \
- dhcpv6_rfc3646.txt \
- dhcpv6_rfc6355.txt \
- regex.txt \
- escape.txt \
- condition.txt \
- xlat.txt \
- ethernet.txt
+FILES := $(subst $(TEST_FILES_DIR)/,,$(call FIND_FILES_SUFFIX,$(TEST_FILES_DIR),*.txt))
# dict.txt - removed because the unit tests don't allow for protocol namespaces
# And the actual script to run each test.
#
$(OUTPUT)/%: $(DIR)/% $(TESTBINDIR)/unit_test_attribute
- ${Q}echo UNIT-TEST $(notdir $@)
+ ${Q}echo UNIT-TEST $(subst $(TEST_FILES_DIR)/,,$<)
${Q}if ! $(TESTBIN)/unit_test_attribute -D $(top_srcdir)/share/dictionary -d $(top_srcdir)/src/tests/unit -r "$@" $<; then \
echo "$(TESTBIN)/unit_test_attribute -D $(top_srcdir)/share/dictionary -d $(top_srcdir)/src/tests/unit -r \"$@\" $<"; \
rm -f $(BUILD_DIR)/tests/test.unit; \
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Encode ipv6address
+#
+encode-pair Unicast = 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+match 00 0c 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+# Scope should be ignored. We use %0 as numbered scopes work everywhere, interfaces don't
+encode-pair Unicast = 2001:0db8:85a3:0000:0000:8a2e:0370:7334%0
+match 00 0c 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+#
+# Encode ipv6prefix
+#
+encode-pair MIP6-Home-Net-Prefix = 2001:db8::/32
+match 00 47 00 05 20 20 01 0d b8
+
+# Prefix truncates address portion
+encode-pair MIP6-Home-Net-Prefix = 2001:db8:00ff::/32
+match 00 47 00 05 20 20 01 0d b8
+
+# Prefix longer than address portion
+encode-pair MIP6-Home-Net-Prefix = 2001:db8:00ff::/64
+match 00 47 00 09 40 20 01 0d b8 00 ff 00 00
+
+encode-pair MIP6-Home-Net-Prefix = 2001:0db8:85a3:0000:0000:8a2e:0370:7334/128
+match 00 47 00 11 80 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+# Cast ipv6addr to 128bit prefix
+encode-pair MIP6-Home-Net-Prefix = 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+match 00 47 00 11 80 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+# 0 bit prefix
+encode-pair MIP6-Home-Net-Prefix = ::/0
+match 00 47 00 01 00
+
+# v4 address - Check the correct 4in6 prefix is added
+encode-pair MIP6-Home-Net-Prefix = ::ffff:192.168.0.1
+match 00 47 00 11 80 00 00 00 00 00 00 00 00 00 00 ff ff c0 a8 00 01
+
+# convert ipv4 to ipv6
+# e.g: 0:0:0:0:0:ffff:102:304
+#encode-pair Unicast = 1.2.3.4
+#match 00 0c 00 10 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04
+
+# short ipv6
+encode-pair Unicast = 2001:4860:4860::8888
+match 00 0c 00 10 20 01 48 60 48 60 00 00 00 00 00 00 00 00 88 88
+
+decode-pair -
+match Unicast = 2001:4860:4860::8888
+
+# long ipv6
+encode-pair Unicast = 2001:0db8:85a3:cade:cafe:8a2e:0370:7334
+match 00 0c 00 10 20 01 0d b8 85 a3 ca de ca fe 8a 2e 03 70 73 34
+
+# Leading 0's are omitted
+decode-pair -
+match Unicast = 2001:db8:85a3:cade:cafe:8a2e:370:7334
+
+count
+match 28
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Bool
+#
+encode-pair Rapid-Commit = 0x
+match 00 0e 00 00
+
+decode-pair -
+match Rapid-Commit = 0x
+
+count
+match 6
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Date. Like a 32bit unix timestamp but starts from 1st Jan 2000 instead of 1st Jan 1970
+#
+encode-pair Failover-Expiration-Time = 0
+match 00 78 00 04 00 00 00 00
+
+# Still less than 946080000 (30 years), so still 0 (we can't represent dates earlier than 1st Jan 2000)
+encode-pair Failover-Expiration-Time = 500
+match 00 78 00 04 00 00 00 00
+
+encode-pair Failover-Expiration-Time = 946080000
+match 00 78 00 04 00 00 00 00
+
+# 1st second of 1st Jan 2000
+encode-pair Failover-Expiration-Time = 946080001
+match 00 78 00 04 00 00 00 01
+
+count
+match 10
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+encode-pair SIP-Server-Domain-Name-List = "foo.ca"
+match 00 15 00 08 03 66 6f 6f 02 63 61 00
+
+decode-pair -
+match SIP-Server-Domain-Name-List = "foo.ca"
+
+encode-pair SIP-Server-Domain-Name-List = "foo.ca", SIP-Server-Domain-Name-List = "example.com"
+match 00 15 00 15 03 66 6f 6f 02 63 61 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
+
+decode-pair -
+match SIP-Server-Domain-Name-List = "foo.ca", SIP-Server-Domain-Name-List = "example.com"
+
+encode-pair SIP-Server-Domain-Name-List = "www.example.com", SIP-Server-Domain-Name-List = "ftp.example.com"
+match 00 15 00 22 03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 03 66 74 70 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
+
+decode-pair -
+match SIP-Server-Domain-Name-List = "www.example.com", SIP-Server-Domain-Name-List = "ftp.example.com"
+
+encode-pair SIP-Server-Domain-Name-List = "www.example.com", SIP-Server-Domain-Name-List = "ftp.example.com", SIP-Server-Domain-Name-List = "ns.example.com"
+match 00 15 00 32 03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 03 66 74 70 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 02 6e 73 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
+
+decode-pair -
+match SIP-Server-Domain-Name-List = "www.example.com", SIP-Server-Domain-Name-List = "ftp.example.com", SIP-Server-Domain-Name-List = "ns.example.com"
+
+count
+match 18
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Array of 16bit integers
+#
+encode-pair Client-Arch-Type = x86-BIOS, Client-Arch-Type = ARM-64-Uboot
+match 00 3d 00 04 00 00 00 16
+
+#
+# Two different arrays of 16bit integers
+#
+encode-pair Client-Arch-Type = x86-BIOS, Client-Arch-Type = ARM-64-Uboot, S46-Priority = DS-Lite, S46-Priority = MAP-E
+match 00 3d 00 04 00 00 00 16 00 6f 00 04 00 40 00 5e
+
+#
+# Array type with one element
+#
+encode-pair Client-Arch-Type = x86-BIOS
+match 00 3d 00 02 00 00
+
+#
+# Array of IPv6 addresses
+#
+encode-pair SIP-Server-Address = 2001:0db8:85a3:0000:0000:8a2e:0370:7334, SIP-Server-Address = 2001:0db8:85a3:0000:0000:8a2e:0370:7335, SIP-Server-Address = 2001:4860:4860::8888
+match 00 16 00 30 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 35 20 01 48 60 48 60 00 00 00 00 00 00 00 00 88 88
+
+decode-pair -
+match SIP-Server-Address = 2001:db8:85a3::8a2e:370:7334, SIP-Server-Address = 2001:db8:85a3::8a2e:370:7335, SIP-Server-Address = 2001:4860:4860::8888
+
+count
+match 12
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# 1 byte unsigned integer (uint8)
+#
+encode-pair Preference = 255
+match 00 07 00 01 ff
+
+encode-pair Preference = 0
+match 00 07 00 01 00
+
+#
+# 2 byte unsigned integer (uint16)
+#
+encode-pair ANI-Access-Technology-Type = 3GPP2-NB-IOT
+match 00 69 00 02 00 0d
+
+#
+# 4 byte unsigned integer (uint32)
+#
+encode-pair Information-Refresh-Time = 99
+match 00 20 00 04 00 00 00 63
+
+count
+match 10
\ No newline at end of file
-#
-# Test vectors for DHCPv6
-#
load dhcpv6
dictionary-load dhcpv6
-#
-# Test vectors for DHCPv6 protocol
-#
load dhcpv6
dictionary-load dhcpv6
-#
-# Test vectors for DHCPv6 attributes for (SIP) Servers
-#
load dhcpv6
dictionary-load dhcpv6
# convert ipv4 to ipv6
# e.g: 0:0:0:0:0:ffff:102:304
-#encode-pair SIP-Server-Address = 1.2.3.4
-#match 00 16 00 10 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04
+encode-pair SIP-Server-Address = 1.2.3.4
+match 00 16 00 10 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04
# short ipv6
encode-pair SIP-Server-Address = 2001:4860:4860::8888
match SIP-Server-Address = 2001:db8:85a3::8a2e:370:7334, SIP-Server-Address = 2001:db8:85a3::8a2e:370:7335, SIP-Server-Address = 2001:4860:4860::8888
count
-match 30
+match 32
-#
-# Test vectors for DHCPv6/DNS
-#
load dhcpv6
dictionary-load dhcpv6
-#
-# Test vectors for DHCPv6 attributes for DUID-UUID
-#
load dhcpv6
dictionary-load dhcpv6
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Simple string type
+#
+encode-pair Subscriber-ID = "fred"
+match 00 26 00 04 66 72 65 64
+
+encode-pair Subscriber-ID = "bob", New-Posix-Timezone = "GB"
+match 00 26 00 03 62 6f 62 00 29 00 02 47 42
+
+# Zero length string
+encode-pair Subscriber-ID = ""
+match 00 26 00 00
+
+count
+match 8
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# TLV with array type values
+#
+encode-pair MOS-Address-IS = 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+match 00 36 00 14 00 01 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+encode-pair MOS-Address-IS = 2001:0db8:85a3:0000:0000:8a2e:0370:7334, MOS-Address-IS = 2001:0db8:85a3:0000:0000:8a2e:0370:7335
+match 00 36 00 24 00 01 00 20 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 35
+
+encode-pair MOS-Address-IS = 2001:0db8:85a3:0000:0000:8a2e:0370:7334, MOS-Address-ES = 2001:0db8:85a3:0000:0000:8a2e:0370:7335
+match 00 36 00 28 00 01 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34 00 03 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 35
+
+#
+# TLV with single values
+#
+encode-pair NTP-Server-Address = 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+match 00 38 00 14 00 01 00 10 20 01 0d b8 85 a3 00 00 00 00 8a 2e 03 70 73 34
+
+count
+match 10
\ No newline at end of file
--- /dev/null
+load dhcpv6
+dictionary-load dhcpv6
+
+#
+# Array of strings, each substring should have a 16bit length field containing its length
+#
+encode-pair Bootfile-Param = "I LIKE CHICKEN", Bootfile-Param = "I LIKE LIVER"
+match 00 3c 00 1e 00 0e 49 20 4c 49 4b 45 20 43 48 49 43 4b 45 4e 00 0c 49 20 4c 49 4b 45 20 4c 49 56 45 52
+
+# Still needs to add the length field with only one element
+encode-pair Bootfile-Param = "MEOW MIX MEOW MIX"
+match 00 3c 00 13 00 11 4d 45 4f 57 20 4d 49 58 20 4d 45 4f 57 20 4d 49 58
+
+encode-pair Bootfile-Param = ""
+match 00 3c 00 02 00 00
+
+encode-pair Bootfile-Param = "", Bootfile-Param = "foo"
+match 00 3c 00 07 00 00 00 03 66 6f 6f
+
+#
+# Variable length octets array
+#
+encode-pair User-Class = 0x010203, User-Class = 0x02040810
+match 00 0f 00 0b 00 03 01 02 03 00 04 02 04 08 10
+
+encode-pair User-Class = 0x010203
+match 00 0f 00 05 00 03 01 02 03
+
+encode-pair User-Class = 0x, User-Class = 0x
+match 00 0f 00 04 00 00 00 00
+
+count
+match 16
\ No newline at end of file