diff options
author | kientzle <kientzle@FreeBSD.org> | 2009-04-27 18:27:54 +0000 |
---|---|---|
committer | kientzle <kientzle@FreeBSD.org> | 2009-04-27 18:27:54 +0000 |
commit | 8958ccc8b0d4ae23fed5df59edf3791d898978f1 (patch) | |
tree | 1e1f0a336858e45c992e396f0dd4b9197a9c3969 | |
parent | 0dffd8719168deb07b1f0cc8db36e3948f969cbb (diff) | |
download | FreeBSD-src-8958ccc8b0d4ae23fed5df59edf3791d898978f1.zip FreeBSD-src-8958ccc8b0d4ae23fed5df59edf3791d898978f1.tar.gz |
Merge r1053,r1055,r1056,r1057,r1065 from libarchive.googlecode.com:
* Fix parsing of POSIX.1e ACLs from Solaris tar archives
* Test the above
* Preserve the order of POSIX.1e ACL entries
* Update tests whose results depended on the order of ACL entries
* Identify NFSv4 ACLs in Solaris tar archives and warn that
they're not yet supported. (In particular, don't try to parse
them as POSIX.1e ACLs.)
Thanks to: Edward Napierala sent me some Solaris 10 tar archives to test
-rw-r--r-- | lib/libarchive/archive_entry.c | 115 | ||||
-rw-r--r-- | lib/libarchive/archive_read_support_format_tar.c | 47 | ||||
-rw-r--r-- | lib/libarchive/test/Makefile | 1 | ||||
-rw-r--r-- | lib/libarchive/test/test_acl_pax.c | 10 | ||||
-rw-r--r-- | lib/libarchive/test/test_compat_solaris_tar_acl.c | 128 | ||||
-rw-r--r-- | lib/libarchive/test/test_compat_solaris_tar_acl.tar.uu | 61 |
6 files changed, 314 insertions, 48 deletions
diff --git a/lib/libarchive/archive_entry.c b/lib/libarchive/archive_entry.c index cf61067..16166aa 100644 --- a/lib/libarchive/archive_entry.c +++ b/lib/libarchive/archive_entry.c @@ -115,6 +115,7 @@ static int acl_special(struct archive_entry *entry, static struct ae_acl *acl_new_entry(struct archive_entry *entry, int type, int permset, int tag, int id); static int isint_w(const wchar_t *start, const wchar_t *end, int *result); +static int ismode_w(const wchar_t *start, const wchar_t *end, int *result); static void next_field_w(const wchar_t **wp, const wchar_t **start, const wchar_t **end, wchar_t *sep); static int prefix_w(const wchar_t *start, const wchar_t *end, @@ -1238,7 +1239,7 @@ static struct ae_acl * acl_new_entry(struct archive_entry *entry, int type, int permset, int tag, int id) { - struct ae_acl *ap; + struct ae_acl *ap, *aq; if (type != ARCHIVE_ENTRY_ACL_TYPE_ACCESS && type != ARCHIVE_ENTRY_ACL_TYPE_DEFAULT) @@ -1251,20 +1252,26 @@ acl_new_entry(struct archive_entry *entry, /* XXX TODO: More sanity-checks on the arguments XXX */ /* If there's a matching entry already in the list, overwrite it. */ - for (ap = entry->acl_head; ap != NULL; ap = ap->next) { + ap = entry->acl_head; + aq = NULL; + while (ap != NULL) { if (ap->type == type && ap->tag == tag && ap->id == id) { ap->permset = permset; return (ap); } + aq = ap; + ap = ap->next; } - /* Add a new entry to the list. */ + /* Add a new entry to the end of the list. */ ap = (struct ae_acl *)malloc(sizeof(*ap)); if (ap == NULL) return (NULL); memset(ap, 0, sizeof(*ap)); - ap->next = entry->acl_head; - entry->acl_head = ap; + if (aq == NULL) + entry->acl_head = ap; + else + aq->next = ap; ap->type = type; ap->tag = tag; ap->id = id; @@ -1586,11 +1593,10 @@ __archive_entry_acl_parse_w(struct archive_entry *entry, struct { const wchar_t *start; const wchar_t *end; - } field[4]; + } field[4], name; int fields; int type, tag, permset, id; - const wchar_t *p; wchar_t sep; while (text != NULL && *text != L'\0') { @@ -1609,9 +1615,6 @@ __archive_entry_acl_parse_w(struct archive_entry *entry, ++fields; } while (sep == L':'); - if (fields < 3) - return (ARCHIVE_WARN); - /* Check for a numeric ID in field 1 or 3. */ id = -1; isint_w(field[1].start, field[1].end, &id); @@ -1619,27 +1622,6 @@ __archive_entry_acl_parse_w(struct archive_entry *entry, if (id == -1 && fields > 3) isint_w(field[3].start, field[3].end, &id); - /* Parse the permissions from field 2. */ - permset = 0; - p = field[2].start; - while (p < field[2].end) { - switch (*p++) { - case 'r': case 'R': - permset |= ARCHIVE_ENTRY_ACL_READ; - break; - case 'w': case 'W': - permset |= ARCHIVE_ENTRY_ACL_WRITE; - break; - case 'x': case 'X': - permset |= ARCHIVE_ENTRY_ACL_EXECUTE; - break; - case '-': - break; - default: - return (ARCHIVE_WARN); - } - } - /* * Solaris extension: "defaultuser::rwx" is the * default ACL corresponding to "user::rwx", etc. @@ -1651,22 +1633,47 @@ __archive_entry_acl_parse_w(struct archive_entry *entry, } else type = default_type; + name.start = name.end = NULL; if (prefix_w(field[0].start, field[0].end, L"user")) { - if (id != -1 || field[1].start < field[1].end) + if (!ismode_w(field[2].start, field[2].end, &permset)) + return (ARCHIVE_WARN); + if (id != -1 || field[1].start < field[1].end) { tag = ARCHIVE_ENTRY_ACL_USER; - else + name = field[1]; + } else tag = ARCHIVE_ENTRY_ACL_USER_OBJ; } else if (prefix_w(field[0].start, field[0].end, L"group")) { - if (id != -1 || field[1].start < field[1].end) + if (!ismode_w(field[2].start, field[2].end, &permset)) + return (ARCHIVE_WARN); + if (id != -1 || field[1].start < field[1].end) { tag = ARCHIVE_ENTRY_ACL_GROUP; - else + name = field[1]; + } else tag = ARCHIVE_ENTRY_ACL_GROUP_OBJ; } else if (prefix_w(field[0].start, field[0].end, L"other")) { - if (id != -1 || field[1].start < field[1].end) + if (fields == 2 + && field[1].start < field[1].end + && ismode_w(field[1].start, field[2].end, &permset)) { + /* This is Solaris-style "other:rwx" */ + } else if (fields == 3 + && field[1].start == field[1].end + && field[2].start < field[2].end + && ismode_w(field[2].start, field[2].end, &permset)) { + /* This is FreeBSD-style "other::rwx" */ + } else return (ARCHIVE_WARN); tag = ARCHIVE_ENTRY_ACL_OTHER; } else if (prefix_w(field[0].start, field[0].end, L"mask")) { - if (id != -1 || field[1].start < field[1].end) + if (fields == 2 + && field[1].start < field[1].end + && ismode_w(field[1].start, field[1].end, &permset)) { + /* This is Solaris-style "mask:rwx" */ + } else if (fields == 3 + && field[1].start == field[1].end + && field[2].start < field[2].end + && ismode_w(field[2].start, field[2].end, &permset)) { + /* This is FreeBSD-style "mask::rwx" */ + } else return (ARCHIVE_WARN); tag = ARCHIVE_ENTRY_ACL_MASK; } else @@ -1674,7 +1681,7 @@ __archive_entry_acl_parse_w(struct archive_entry *entry, /* Add entry to the internal list. */ archive_entry_acl_add_entry_w_len(entry, type, permset, - tag, id, field[1].start, field[1].end - field[1].start); + tag, id, name.start, name.end - name.start); } return (ARCHIVE_OK); } @@ -1798,6 +1805,38 @@ isint_w(const wchar_t *start, const wchar_t *end, int *result) } /* + * Parse a string as a mode field. Returns true if + * the string is non-empty and consists only of mode characters, + * false otherwise. + */ +static int +ismode_w(const wchar_t *start, const wchar_t *end, int *permset) +{ + const wchar_t *p; + + p = start; + *permset = 0; + while (p < end) { + switch (*p++) { + case 'r': case 'R': + *permset |= ARCHIVE_ENTRY_ACL_READ; + break; + case 'w': case 'W': + *permset |= ARCHIVE_ENTRY_ACL_WRITE; + break; + case 'x': case 'X': + *permset |= ARCHIVE_ENTRY_ACL_EXECUTE; + break; + case '-': + break; + default: + return (0); + } + } + return (1); +} + +/* * Match "[:whitespace:]*(.*)[:whitespace:]*[:,\n]". *wp is updated * to point to just after the separator. *start points to the first * character of the matched text and *end just after the last diff --git a/lib/libarchive/archive_read_support_format_tar.c b/lib/libarchive/archive_read_support_format_tar.c index a2d08ea..57f420f 100644 --- a/lib/libarchive/archive_read_support_format_tar.c +++ b/lib/libarchive/archive_read_support_format_tar.c @@ -732,6 +732,7 @@ header_Solaris_ACL(struct archive_read *a, struct tar *tar, const struct archive_entry_header_ustar *header; size_t size; int err; + int64_t type; char *acl, *p; wchar_t *wp; @@ -744,24 +745,57 @@ header_Solaris_ACL(struct archive_read *a, struct tar *tar, err = read_body_to_string(a, tar, &(tar->acl_text), h); if (err != ARCHIVE_OK) return (err); + /* Recursively read next header */ err = tar_read_header(a, tar, entry); if ((err != ARCHIVE_OK) && (err != ARCHIVE_WARN)) return (err); - /* Skip leading octal number. */ - /* XXX TODO: Parse the octal number and sanity-check it. */ + /* TODO: Examine the first characters to see if this + * is an AIX ACL descriptor. We'll likely never support + * them, but it would be polite to recognize and warn when + * we do see them. */ + + /* Leading octal number indicates ACL type and number of entries. */ p = acl = tar->acl_text.s; - while (*p != '\0' && p < acl + size) + type = 0; + while (*p != '\0' && p < acl + size) { + if (*p < '0' || *p > '7') { + archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, + "Malformed Solaris ACL attribute (invalid digit)"); + return(ARCHIVE_WARN); + } + type <<= 3; + type += *p - '0'; + if (type > 077777777) { + archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, + "Malformed Solaris ACL attribute (count too large)"); + return (ARCHIVE_WARN); + } p++; + } + switch (type & ~0777777) { + case 01000000: + /* POSIX.1e ACL */ + break; + case 03000000: + archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, + "Solaris NFSv4 ACLs not supported"); + return (ARCHIVE_WARN); + default: + archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, + "Malformed Solaris ACL attribute (unsupported type %o)", + (int)type); + return (ARCHIVE_WARN); + } p++; if (p >= acl + size) { archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, - "Malformed Solaris ACL attribute"); + "Malformed Solaris ACL attribute (body overflow)"); return(ARCHIVE_WARN); } - /* Skip leading octal number. */ + /* ACL text is null-terminated; find the end. */ size -= (p - acl); acl = p; @@ -771,6 +805,9 @@ header_Solaris_ACL(struct archive_read *a, struct tar *tar, wp = utf8_decode(tar, acl, p - acl); err = __archive_entry_acl_parse_w(entry, wp, ARCHIVE_ENTRY_ACL_TYPE_ACCESS); + if (err != ARCHIVE_OK) + archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, + "Malformed Solaris ACL attribute (unparsable)"); return (err); } diff --git a/lib/libarchive/test/Makefile b/lib/libarchive/test/Makefile index d266d82..b6aeff1 100644 --- a/lib/libarchive/test/Makefile +++ b/lib/libarchive/test/Makefile @@ -16,6 +16,7 @@ TESTS= \ test_compat_bzip2.c \ test_compat_gtar.c \ test_compat_gzip.c \ + test_compat_solaris_tar_acl.c \ test_compat_tar_hardlink.c \ test_compat_xz.c \ test_compat_zip.c \ diff --git a/lib/libarchive/test/test_acl_pax.c b/lib/libarchive/test/test_acl_pax.c index 4a37199..5898a66 100644 --- a/lib/libarchive/test/test_acl_pax.c +++ b/lib/libarchive/test/test_acl_pax.c @@ -151,10 +151,10 @@ static unsigned char reference[] = { 0,0,0,0,0,0,0,0,0,0,'1','1','3',' ','S','C','H','I','L','Y','.','a','c','l', '.','a','c','c','e','s','s','=','u','s','e','r',':',':','r','-','x',',','g', 'r','o','u','p',':',':','r','-','-',',','o','t','h','e','r',':',':','-','w', -'x',',','g','r','o','u','p',':','g','r','o','u','p','7','8',':','r','w','x', -':','7','8',',','u','s','e','r',':','u','s','e','r','7','8',':','-','-','-', -':','7','8',',','u','s','e','r',':','u','s','e','r','7','7',':','r','-','-', -':','7','7',10,'1','6',' ','S','C','H','I','L','Y','.','d','e','v','=','0', +'x',',','u','s','e','r',':','u','s','e','r','7','7',':','r','-','-',':','7', +'7',',','u','s','e','r',':','u','s','e','r','7','8',':','-','-','-',':','7', +'8',',','g','r','o','u','p',':','g','r','o','u','p','7','8',':','r','w','x', +':','7','8',10,'1','6',' ','S','C','H','I','L','Y','.','d','e','v','=','0', 10,'1','6',' ','S','C','H','I','L','Y','.','i','n','o','=','0',10,'1','8', ' ','S','C','H','I','L','Y','.','n','l','i','n','k','=','0',10,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -464,7 +464,7 @@ DEFINE_TEST(test_acl_pax) /* Assert that the generated data matches the built-in reference data.*/ failure("Generated pax archive does not match reference; check 'testout' and 'reference' files."); - assert(0 == memcmp(buff, reference, sizeof(reference))); + assertEqualMem(buff, reference, sizeof(reference)); failure("Generated pax archive does not match reference; check 'testout' and 'reference' files."); assertEqualInt((int)used, sizeof(reference)); diff --git a/lib/libarchive/test/test_compat_solaris_tar_acl.c b/lib/libarchive/test/test_compat_solaris_tar_acl.c new file mode 100644 index 0000000..ec3955e --- /dev/null +++ b/lib/libarchive/test/test_compat_solaris_tar_acl.c @@ -0,0 +1,128 @@ +/*- + * Copyright (c) 2003-2009 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" +__FBSDID("$FreeBSD$"); + +/* + * Exercise support for reading Solaris-style ACL data + * from tar archives. + * + * This should work on all systems, regardless of whether local + * filesystems support ACLs or not. + */ + +DEFINE_TEST(test_compat_solaris_tar_acl) +{ + struct archive *a; + struct archive_entry *ae; + const char *reference1 = "test_compat_solaris_tar_acl.tar"; + int type, permset, tag, qual; + const char *name; + + /* Sample file generated on Solaris 10 */ + extract_reference_file(reference1); + assert(NULL != (a = archive_read_new())); + assertA(0 == archive_read_support_format_all(a)); + assertA(0 == archive_read_support_compression_all(a)); + assertA(0 == archive_read_open_filename(a, reference1, 512)); + + /* Archive has 1 entry with some ACLs set on it. */ + assertA(0 == archive_read_next_header(a, &ae)); + failure("Basic ACLs should set mode to 0640, not %04o", + archive_entry_mode(ae)&0777); + assertEqualInt((archive_entry_mode(ae) & 0777), 0640); + assertEqualInt(7, archive_entry_acl_reset(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS)); + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(006, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_USER_OBJ, tag); + assertEqualInt(-1, qual); + assert(name == NULL); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(004, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_GROUP_OBJ, tag); + assertEqualInt(-1, qual); + assert(name == NULL); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(000, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_OTHER, tag); + assertEqualInt(-1, qual); + assert(name == NULL); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(001, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag); + assertEqualInt(71, qual); + assertEqualString(name, "lp"); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(004, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag); + assertEqualInt(666, qual); + assertEqualString(name, "666"); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(007, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag); + assertEqualInt(1000, qual); + assertEqualString(name, "trasz"); + + assertEqualInt(ARCHIVE_OK, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ACCESS, type); + assertEqualInt(004, permset); + assertEqualInt(ARCHIVE_ENTRY_ACL_MASK, tag); + assertEqualInt(-1, qual); + assertEqualString(name, NULL); + + assertEqualInt(ARCHIVE_EOF, archive_entry_acl_next(ae, + ARCHIVE_ENTRY_ACL_TYPE_ACCESS, + &type, &permset, &tag, &qual, &name)); + + /* Close the archive. */ + assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a)); + assertEqualInt(ARCHIVE_OK, archive_read_finish(a)); +} diff --git a/lib/libarchive/test/test_compat_solaris_tar_acl.tar.uu b/lib/libarchive/test/test_compat_solaris_tar_acl.tar.uu new file mode 100644 index 0000000..229b335 --- /dev/null +++ b/lib/libarchive/test/test_compat_solaris_tar_acl.tar.uu @@ -0,0 +1,61 @@ +$FreeBSD$ +begin 644 test_acl_solaris.tar +M9FEL92UW:71H+7!O<VEX+6%C;',````````````````````````````````` +M```````````````````````````````````````````````````````````` +M`````````````#`P,#`V-#0`,#`P,3<U,``P,#`P,#`P`#`P,#`P,#`P,30T +M`#$Q,3<T-C`T,34W`#`P,34Q-S8`00`````````````````````````````` +M```````````````````````````````````````````````````````````` +M``````````````````````````````````````````!U<W1A<@`P,'1R87-Z +M````````````````````````````````````<F]O=``````````````````` +M```````````````````P,#`P,C$P`#`P,#`P,3`````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````Q,#`P,#`W`'5S97(Z.G)W+2QU<V5R.FQP.BTM +M>#HW,2QU<V5R.C8V-CIR+2TZ-C8V+'5S97(Z=')A<WHZ<G=X.C$P,#`L9W)O +M=7`Z.G(M+2QM87-K.G(M+2QO=&AE<CIR+2T``````````3````````/-@``` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M````````````````````````````!%&8`````````&L`````,3`P,#`P-P!U +M<V5R.CIR=RTL=7-E<CIL<#HM+7@Z-S$L=7-E<CHV-C8Z<BTM.C8V-BQU<V5R +M.G1R87-Z.G)W>#HQ,#`P+&=R;W5P.CIR+2TL;6%S:SIR+69I;&4M=VET:"UP +M;W-I>"UA8VQS```````````````````````````````````````````````` +M```````````````````````````````````````````````````````````P +M,#`P-C0T`#`P,#$W-3``,#`P,#`P,``P,#`P,#`P,#`P,``Q,3$W-#8P-#$U +M-P`P,#$U,30T`#`````````````````````````````````````````````` +M```````````````````````````````````````````````````````````` +M````````````````````````````=7-T87(`,#!T<F%S>@`````````````` +M`````````````````````')O;W0````````````````````````````````` +M````,#`P,#(Q,``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 |