]> git.ipfire.org Git - thirdparty/libarchive.git/commitdiff
[tar] Harden timestamp parsing
authorTim Kientzle <kientzle@acm.org>
Sat, 2 May 2026 20:58:45 +0000 (13:58 -0700)
committerTim Kientzle <kientzle@acm.org>
Sat, 2 May 2026 22:02:28 +0000 (15:02 -0700)
Improves the parsing of timestamps in a couple of ways:

* Saturate when timestamps exceed the range of time_t.
  In particular, this provides more rational behavior on
  systems with 32-bit time_t.

* Validate the format of overlong pax timestamps.
  We previously failed to check that high-resolution
  timestamps had only digits in the fractional part.
  We now notice and ignore those with a warning.

Makefile.am
libarchive/archive_read_support_format_tar.c
libarchive/test/CMakeLists.txt
libarchive/test/test_read_format_tar_pax_timestamps.c [new file with mode: 0644]
libarchive/test/test_read_format_tar_pax_timestamps.tar.uu [new file with mode: 0644]
libarchive/test/test_read_format_tar_timestamp_overflow.c [new file with mode: 0644]
libarchive/test/test_read_format_tar_timestamp_overflow.tar.uu [new file with mode: 0644]

index b19e6837cb8da71a91705f797fc57262d09e3341..30272333b097bcb58577fcb03744b07196dc0e89 100644 (file)
@@ -554,6 +554,8 @@ libarchive_test_SOURCES= \
        libarchive/test/test_read_format_tar_pax_g_large.c \
        libarchive/test/test_read_format_tar_pax_large_attr.c \
        libarchive/test/test_read_format_tar_pax_negative_time.c \
+       libarchive/test/test_read_format_tar_pax_timestamps.c \
+       libarchive/test/test_read_format_tar_timestamp_overflow.c \
        libarchive/test/test_read_format_tbz.c \
        libarchive/test/test_read_format_tgz.c \
        libarchive/test/test_read_format_tlz.c \
@@ -1019,6 +1021,8 @@ libarchive_test_EXTRA_DIST=\
        libarchive/test/test_read_format_tar_pax_g_large.tar.uu \
        libarchive/test/test_read_format_tar_pax_large_attr.tar.Z.uu \
        libarchive/test/test_read_format_tar_pax_negative_time.tar.uu \
+       libarchive/test/test_read_format_tar_pax_timestamps.tar.uu \
+       libarchive/test/test_read_format_tar_timestamp_overflow.tar.uu \
        libarchive/test/test_read_format_ustar_filename_cp866.tar.Z.uu \
        libarchive/test/test_read_format_ustar_filename_eucjp.tar.Z.uu \
        libarchive/test/test_read_format_ustar_filename_koi8r.tar.Z.uu \
index 98800ec31aa49397e4f4df5e06a3dd74586d609e..ac5e6197712ef07b8529a245ce47c44f8f340532 100644 (file)
@@ -248,6 +248,36 @@ static const size_t fflags_limit = 512; /* Longest fflags */
 static const size_t acl_limit = 131072; /* Longest textual ACL: 128kiB */
 static const int64_t entry_limit = 0xfffffffffffffffLL; /* 2^60 bytes = 1 ExbiByte */
 
+/*
+ * There's no standard for TIME_T_MAX.  So we compute it
+ * here.  TODO: Move this to configure time, but be careful
+ * about cross-compile environments.
+ */
+static int64_t
+get_time_t_max(void)
+{
+#if defined(TIME_T_MAX)
+        return TIME_T_MAX;
+#else
+        /* ISO C allows time_t to be a floating-point type,
+           but POSIX requires an integer type.  The following
+           should work on any system that follows the POSIX
+           conventions. */
+        if (((time_t)0) < ((time_t)-1)) {
+                /* Time_t is unsigned */
+                return (~(time_t)0);
+        } else {
+                /* Time_t is signed. */
+                /* Assume it's the same as int64_t or int32_t */
+                if (sizeof(time_t) == sizeof(int64_t)) {
+                        return (time_t)INT64_MAX;
+                } else {
+                        return (time_t)INT32_MAX;
+                }
+        }
+#endif
+}
+
 int
 archive_read_support_format_gnutar(struct archive *a)
 {
@@ -1369,7 +1399,12 @@ header_common(struct archive_read *a, struct tar *tar,
                archive_entry_set_gid(entry, tar_atol(header->gid, sizeof(header->gid)));
        }
        if (!archive_entry_mtime_is_set(entry)) {
-               archive_entry_set_mtime(entry, tar_atol(header->mtime, sizeof(header->mtime)), 0);
+               int64_t t64 = tar_atol(header->mtime, sizeof(header->mtime));
+               time_t t = (time_t)t64;
+               if ((int64_t)t != t64) { /* time_t overflowed */
+                       t = get_time_t_max();
+               }
+               archive_entry_set_mtime(entry, t, 0);
        }
 
        /* Reconcile the size info. */
@@ -2268,7 +2303,7 @@ pax_attribute_SCHILY_acl(struct archive_read *a, struct tar *tar,
 }
 
 static int
-pax_attribute_read_time(struct archive_read *a, size_t value_length, int64_t *ps, long *pn, int64_t *unconsumed) {
+pax_attribute_read_time(struct archive_read *a, size_t value_length, __LA_TIME_T *ps, long *pn, int64_t *unconsumed) {
        struct archive_string as;
        int r;
 
@@ -2288,12 +2323,16 @@ pax_attribute_read_time(struct archive_read *a, size_t value_length, int64_t *ps
                return (r);
        }
 
-       pax_time(as.s, archive_strlen(&as), ps, pn);
+       int64_t sec = 0;
+       pax_time(as.s, archive_strlen(&as), &sec, pn);
        archive_string_free(&as);
-       if (*ps == INT64_MIN) {
+
+       if (sec == INT64_MIN) {
                *ps = 0;
                *pn = 0;
                return (ARCHIVE_WARN);
+       } else {
+               *ps = (__LA_TIME_T)sec;
        }
        return (ARCHIVE_OK);
 }
@@ -2509,8 +2548,13 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
                        */
                        if (key_length == 12 && memcmp(key, "creationtime", 12) == 0) {
                                /* LIBARCHIVE.creationtime */
-                               if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
-                                       archive_entry_set_birthtime(entry, t, n);
+                               __LA_TIME_T sec = 0;
+                               if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
+                                       archive_entry_set_birthtime(entry, sec, n);
+                               } else {
+                                       archive_set_error(&a->archive,
+                                                         ARCHIVE_ERRNO_MISC,
+                                                         "Ignoring malformed pax creationtime");
                                }
                                return (err);
                        }
@@ -2738,16 +2782,26 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
                break;
        case 'a':
                if (key_length == 5 && memcmp(key, "atime", 5) == 0) {
-                       if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
-                               archive_entry_set_atime(entry, t, n);
+                       __LA_TIME_T sec = 0;
+                       if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
+                               archive_entry_set_atime(entry, sec, n);
+                       } else {
+                               archive_set_error(&a->archive,
+                                   ARCHIVE_ERRNO_MISC,
+                                   "Ignoring malformed pax atime");
                        }
                        return (err);
                }
                break;
        case 'c':
                if (key_length == 5 && memcmp(key, "ctime", 5) == 0) {
-                       if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
-                               archive_entry_set_ctime(entry, t, n);
+                       __LA_TIME_T sec = 0;
+                       if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
+                               archive_entry_set_ctime(entry, sec, n);
+                       } else {
+                               archive_set_error(&a->archive,
+                                   ARCHIVE_ERRNO_MISC,
+                                   "Ignoring malformed pax ctime");
                        }
                        return (err);
                } else if (key_length == 7 && memcmp(key, "charset", 7) == 0) {
@@ -2819,8 +2873,13 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
                break;
        case 'm':
                if (key_length == 5 && memcmp(key, "mtime", 5) == 0) {
-                       if ((err = pax_attribute_read_time(a, value_length, &t, &n, unconsumed)) == ARCHIVE_OK) {
-                               archive_entry_set_mtime(entry, t, n);
+                       __LA_TIME_T sec;
+                       if ((err = pax_attribute_read_time(a, value_length, &sec, &n, unconsumed)) == ARCHIVE_OK) {
+                               archive_entry_set_mtime(entry, sec, n);
+                       } else {
+                               archive_set_error(&a->archive,
+                                   ARCHIVE_ERRNO_MISC,
+                                   "Ignoring malformed pax mtime");
                        }
                        return (err);
                }
@@ -2887,7 +2946,8 @@ pax_attribute(struct archive_read *a, struct tar *tar, struct archive_entry *ent
 /*
  * Parse a decimal time value, which may include a fractional portion
  *
- * Sets ps to INT64_MIN on error.
+ * Sets ps to INT64_MIN on error, including syntax issues such as non-digits,
+ * or a time value that's outside the range of time_t.
  */
 static void
 pax_time(const char *p, size_t length, int64_t *ps, long *pn)
@@ -2928,21 +2988,61 @@ pax_time(const char *p, size_t length, int64_t *ps, long *pn)
 
        *ps = s * sign;
 
+#if ARCHIVE_VERSION_NUMBER < 4000000
+       /* Libarchive 4.0 will have __LA_TIME_T == int64_t, so
+          this will be unnecessary. */
+       /* Test whether it overflows __LA_TIME_T */
+       __LA_TIME_T sec = (__LA_TIME_T)*ps;
+       if ((int64_t)sec != *ps) {
+               *ps = INT64_MIN;
+               *pn = 0;
+               return;
+       }
+#endif
+
        /* Calculate nanoseconds. */
        *pn = 0;
 
-       if (length <= 0 || *p != '.')
+       if (length <= 0) {
                return;
+       }
+
+       /* Skip `.` */
+       if (*p != '.') {
+               *ps = INT64_MIN;
+               *pn = 0;
+               return;
+       }
+       ++p;
+       --length;
 
        l = 100000000UL;
        do {
+               if (length <= 0) {
+                       return;
+               }
+               if (*p >= '0' && *p <= '9') {
+                       *pn += (*p - '0') * l;
+               } else {
+                       *ps = INT64_MIN;
+                       *pn = 0;
+                       return;
+               }
                ++p;
                --length;
-               if (length > 0 && *p >= '0' && *p <= '9')
-                       *pn += (*p - '0') * l;
-               else
-                       break;
        } while (l /= 10);
+
+       /* Ignore resolution beyond nanoseconds,
+          but verify it's all decimal digits. */
+       while (length > 0) {
+               if (*p < '0' || *p > '9') {
+                       *ps = INT64_MIN;
+                       *pn = 0;
+                       return;
+               }
+               ++p;
+               --length;
+       }
 }
 
 /*
index 2d2ff013f9cae6d0f0a4092785e7d5305cc465d9..16031faef6a9ea6a06d4791523a4a1a25ce255a5 100644 (file)
@@ -188,6 +188,8 @@ IF(ENABLE_TEST)
     test_read_format_tar_pax_g_large.c
     test_read_format_tar_pax_large_attr.c
     test_read_format_tar_pax_negative_time.c
+    test_read_format_tar_pax_timestamps.c
+    test_read_format_tar_timestamp_overflow.c
     test_read_format_tbz.c
     test_read_format_tgz.c
     test_read_format_tlz.c
diff --git a/libarchive/test/test_read_format_tar_pax_timestamps.c b/libarchive/test/test_read_format_tar_pax_timestamps.c
new file mode 100644 (file)
index 0000000..7850cbe
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * Copyright (c) 2025 Tobias Stoeckmann
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "test.h"
+
+/*
+ * Read a pax formatted tar archive that has a negative modification time.
+ */
+DEFINE_TEST(test_read_format_tar_pax_timestamps)
+{
+       char name[] = "test_read_format_tar_pax_timestamps.tar";
+       struct archive_entry *ae;
+       struct archive *a;
+
+       assert((a = archive_read_new()) != NULL);
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a));
+       extract_reference_file(name);
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, name, 10240));
+
+       /* Read first entry. */
+       assertEqualIntA(a, ARCHIVE_WARN, archive_read_next_header(a, &ae));
+       assertEqualString("empty", archive_entry_pathname(ae));
+       assertEqualInt(224165920, archive_entry_mtime(ae));
+       assertEqualInt(0, archive_entry_mtime_nsec(ae));
+       assertEqualInt(0, archive_entry_uid(ae));
+       assertEqualString("root", archive_entry_uname(ae));
+       assertEqualInt(0, archive_entry_gid(ae));
+       assertEqualString("root", archive_entry_gname(ae));
+       assertEqualInt(0100644, archive_entry_mode(ae));
+       assertEqualInt(archive_entry_is_encrypted(ae), 0);
+       assertEqualIntA(a, archive_read_has_encrypted_entries(a), ARCHIVE_READ_FORMAT_ENCRYPTION_UNSUPPORTED);
+
+       /* Verify the end-of-archive. */
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+
+       /* Verify that the format detection worked. */
+       assertEqualInt(archive_filter_code(a, 0), ARCHIVE_FILTER_NONE);
+       assertEqualInt(archive_format(a), ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE);
+
+       assertEqualInt(ARCHIVE_OK, archive_read_close(a));
+       assertEqualInt(ARCHIVE_OK, archive_read_free(a));
+}
diff --git a/libarchive/test/test_read_format_tar_pax_timestamps.tar.uu b/libarchive/test/test_read_format_tar_pax_timestamps.tar.uu
new file mode 100644 (file)
index 0000000..089705c
--- /dev/null
@@ -0,0 +1,60 @@
+begin 644 test_read_format_tar_pax_timestamps.tar
+M4&%X2&5A9&5R+V5M<'1Y````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M`````````````#`P,#8T-"``,#`P,#`P(``P,#`P,#`@`#`P,#`P,#`P,#,W
+M(#`P,#`P,#`P,#`P(#`Q-#(P-0`@>`````````````````````````!N````
+M````````````````````````````````````````````````````````````
+M``````````````````````````````````````````!U<W1A<@`P,')O;W0`
+M````````````````````````````````````<F]O=```````````````````
+M```````````````````P,#`P,#`@`#`P,#`P,"``````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M```````````````````````S,2!M=&EM93TQ-S0X,#@Y-#8T+CDU,3DR.#0V
+M-V$*````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````;@``````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M`````````````````````````````````````````````&5M<'1Y````````
+M````````````````````````````````````````````````````````````
+M```````````````````````````````````````````````````````````P
+M,#`V-#0@`#`P,#`P,"``,#`P,#`P(``P,#`P,#`P,#`P,""```````````U<
+M@"`P,3$U-S4`(#``````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````=7-T87(`,#!R;V]T````````````````
+M`````````````````````')O;W0`````````````````````````````````
+M````,#`P,#`P(``P,#`P,#`@````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+H````````````````````````````````````````````````````````
+`
+end
diff --git a/libarchive/test/test_read_format_tar_timestamp_overflow.c b/libarchive/test/test_read_format_tar_timestamp_overflow.c
new file mode 100644 (file)
index 0000000..b900e38
--- /dev/null
@@ -0,0 +1,55 @@
+/*-
+ * Copyright (c) 2026 Tim Kientzle
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "test.h"
+
+/*
+ * Read a tar archive that has an oversized timestamp value.
+ * This should not cause an infinite loop.
+ */
+DEFINE_TEST(test_read_format_tar_timestamp_overflow)
+{
+       char name[] = "test_read_format_tar_timestamp_overflow.tar";
+       struct archive_entry *ae;
+       struct archive *a;
+
+       assert((a = archive_read_new()) != NULL);
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a));
+       extract_reference_file(name);
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, name, 10240));
+
+       /* Read first entry. */
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("tmp/f.txt", archive_entry_pathname(ae));
+       /* 15155542402 should be parsed correctly if time_t is 64-bit, 
+     * or handled gracefully if not. */
+    (void)archive_entry_mtime(ae);
+
+       /* Verify the end-of-archive. */
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+
+       assertEqualInt(ARCHIVE_OK, archive_read_close(a));
+       assertEqualInt(ARCHIVE_OK, archive_read_free(a));
+}
diff --git a/libarchive/test/test_read_format_tar_timestamp_overflow.tar.uu b/libarchive/test/test_read_format_tar_timestamp_overflow.tar.uu
new file mode 100644 (file)
index 0000000..b5520ff
--- /dev/null
@@ -0,0 +1,38 @@
+begin 600 test_read_format_tar_timestamp_overflow.tar
+M=&UP+V8N='AT````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M`````````````#`P,#`V-``````````````````````````````````P,#$V
+M`#$U,34U-30R-#`R`#`Q,C8Q-``@,```````````````````````````````
+M``````````````````````````````#_````````````````````````````
+M``````````````````````````````````````````!U<W1A<B`@`&UA<W1E
+M<@``````````````````````````````````;6%S=&5R````````````````
+M````````````````````````````````````````````````````````````
+M```````````````````````````````````!`````````````````````#0`
+M,#`P,3<U,``P,#`Q-S4P`#`P,#`P,#``````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+M````````````````````````````````````````````````````````````
+&````````
+`
+end