xref: /trunk/test/aoo-test/src/aootest/controls.py (revision 6e223505681eb073927a3bbffc2acd512d92ba3f)
12cb6d6f1SJim Jagielski################################################################
22cb6d6f1SJim Jagielski#
32cb6d6f1SJim Jagielski#  Licensed to the Apache Software Foundation (ASF) under one
42cb6d6f1SJim Jagielski#  or more contributor license agreements.  See the NOTICE file
52cb6d6f1SJim Jagielski#  distributed with this work for additional information
62cb6d6f1SJim Jagielski#  regarding copyright ownership.  The ASF licenses this file
72cb6d6f1SJim Jagielski#  to you under the Apache License, Version 2.0 (the
82cb6d6f1SJim Jagielski#  "License"); you may not use this file except in compliance
92cb6d6f1SJim Jagielski#  with the License.  You may obtain a copy of the License at
102cb6d6f1SJim Jagielski#
112cb6d6f1SJim Jagielski#    http://www.apache.org/licenses/LICENSE-2.0
122cb6d6f1SJim Jagielski#
132cb6d6f1SJim Jagielski#  Unless required by applicable law or agreed to in writing,
142cb6d6f1SJim Jagielski#  software distributed under the License is distributed on an
152cb6d6f1SJim Jagielski#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
162cb6d6f1SJim Jagielski#  KIND, either express or implied.  See the License for the
172cb6d6f1SJim Jagielski#  specific language governing permissions and limitations
182cb6d6f1SJim Jagielski#  under the License.
192cb6d6f1SJim Jagielski#
202cb6d6f1SJim Jagielski################################################################
212cb6d6f1SJim Jagielski
222cb6d6f1SJim Jagielski"""Negative-control documents for the checkers: ports of make-controls.py, make-pkg-controls.py and make-doctype-controls.py."""
232cb6d6f1SJim Jagielskiimport io, json, os, zipfile, warnings
242cb6d6f1SJim Jagielski
252cb6d6f1SJim Jagielski
262cb6d6f1SJim Jagielskidef _prepare(path):
272cb6d6f1SJim Jagielski    """A control directory must be empty or absent: nothing is ever deleted here."""
282cb6d6f1SJim Jagielski    if path is None:
292cb6d6f1SJim Jagielski        return
302cb6d6f1SJim Jagielski    if os.path.isdir(path) and os.listdir(path):
312cb6d6f1SJim Jagielski        raise ValueError("control directory is not empty: " + path)
322cb6d6f1SJim Jagielski    os.makedirs(path, exist_ok=True)
332cb6d6f1SJim Jagielski
342cb6d6f1SJim Jagielski
352cb6d6f1SJim Jagielskidef _target(path, name):
362cb6d6f1SJim Jagielski    """A file to write, or a throwaway buffer when only the expectations are wanted."""
372cb6d6f1SJim Jagielski    return io.BytesIO() if path is None else os.path.join(path, name)
382cb6d6f1SJim Jagielski
392cb6d6f1SJim Jagielski
402cb6d6f1SJim Jagielskidef _write(path, name, text):
412cb6d6f1SJim Jagielski    if path is not None:
422cb6d6f1SJim Jagielski        open(os.path.join(path, name), "w").write(text)
432cb6d6f1SJim Jagielski
442cb6d6f1SJim Jagielski
452cb6d6f1SJim Jagielskidef make_odf_controls(outdir):
462cb6d6f1SJim Jagielski    """Writes the negative-control packages for validate.check() and idcheck.check() into `outdir` (empty or absent)."""
472cb6d6f1SJim Jagielski    C = os.fspath(outdir)
482cb6d6f1SJim Jagielski    MANI = ('<?xml version="1.0" encoding="UTF-8"?>\n<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">'
492cb6d6f1SJim Jagielski            '<manifest:file-entry manifest:full-path="/" manifest:version="1.2" manifest:media-type="application/vnd.oasis.opendocument.text"/>'
502cb6d6f1SJim Jagielski            '<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/></manifest:manifest>\n')
512cb6d6f1SJim Jagielski    MATH = ('<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE math:math PUBLIC "-//OpenOffice.org//DTD Modified W3C MathML 1.01//EN" "math.dtd">\n'
522cb6d6f1SJim Jagielski            '<math:math xmlns:math="http://www.w3.org/1998/Math/MathML"><math:semantics><math:mi>x</math:mi></math:semantics></math:math>\n')
532cb6d6f1SJim Jagielski
542cb6d6f1SJim Jagielski
552cb6d6f1SJim Jagielski    def mk(name, ver, body, extra=None):
562cb6d6f1SJim Jagielski        content = ('<?xml version="1.0" encoding="UTF-8"?>\n<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '
572cb6d6f1SJim Jagielski                   'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="%s"><office:body><office:text>%s</office:text></office:body></office:document-content>\n') % (ver, body)
582cb6d6f1SJim Jagielski        with zipfile.ZipFile(_target(C, name + ".odt"), "w") as z:
592cb6d6f1SJim Jagielski            z.writestr(zipfile.ZipInfo("mimetype"), "application/vnd.oasis.opendocument.text", compress_type=zipfile.ZIP_STORED)
602cb6d6f1SJim Jagielski            z.writestr("content.xml", content, compress_type=zipfile.ZIP_DEFLATED)
612cb6d6f1SJim Jagielski            z.writestr("META-INF/manifest.xml", MANI, compress_type=zipfile.ZIP_DEFLATED)
622cb6d6f1SJim Jagielski            for n, t in (extra or {}).items():
632cb6d6f1SJim Jagielski                z.writestr(n, t, compress_type=zipfile.ZIP_DEFLATED)
642cb6d6f1SJim Jagielski
652cb6d6f1SJim Jagielski
662cb6d6f1SJim Jagielski    OFFNS = ('xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" '
672cb6d6f1SJim Jagielski             'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" '
682cb6d6f1SJim Jagielski             'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" '
69*6e223505SJim Jagielski             'xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"')
702cb6d6f1SJim Jagielski
712cb6d6f1SJim Jagielski
722cb6d6f1SJim Jagielski    def mkdoc(name, ext, mime, streams):
732cb6d6f1SJim Jagielski        """A package whose streams are given whole: {"content.xml": (root element, body)}, written with office:version 1.2."""
742cb6d6f1SJim Jagielski        mani = MANI.replace("application/vnd.oasis.opendocument.text", mime)
752cb6d6f1SJim Jagielski        with zipfile.ZipFile(_target(C, name + "." + ext), "w") as z:
762cb6d6f1SJim Jagielski            z.writestr(zipfile.ZipInfo("mimetype"), mime, compress_type=zipfile.ZIP_STORED)
772cb6d6f1SJim Jagielski            for n, (root, body) in streams.items():
782cb6d6f1SJim Jagielski                z.writestr(n, '<?xml version="1.0" encoding="UTF-8"?>\n<%s %s office:version="1.2">%s</%s>\n' % (root, OFFNS, body, root), compress_type=zipfile.ZIP_DEFLATED)
792cb6d6f1SJim Jagielski            z.writestr("META-INF/manifest.xml", mani, compress_type=zipfile.ZIP_DEFLATED)
802cb6d6f1SJim Jagielski
812cb6d6f1SJim Jagielski
822cb6d6f1SJim Jagielski    _prepare(C)
832cb6d6f1SJim Jagielski    mk("good", "1.2", "<text:p>hi</text:p>")
842cb6d6f1SJim Jagielski    mk("bad", "1.2", "<text:bogus/>")
852cb6d6f1SJim Jagielski    mk("wrongver", "1.1", "<text:p>hi</text:p>")
862cb6d6f1SJim Jagielski    # text:drop-down is new in ODF 1.3 (OFFICE-3881): invalid in 1.2, valid in 1.3 and 1.4
872cb6d6f1SJim Jagielski    mk("new13", "1.2", '<text:p><text:drop-down text:name="d"><text:label text:value="a"/></text:drop-down></text:p>')
882cb6d6f1SJim Jagielski    # ODF 1.2 Part 1 2.2.1 B.4: an embedded object whose content.xml is MathML
892cb6d6f1SJim Jagielski    mk("mathobj", "1.2", "<text:p>hi</text:p>", {"Object 1/content.xml": MATH})
902cb6d6f1SJim Jagielski    # ODF 1.2 Part 1 2.2.1 D.1/D.2: checked by idcheck.py, not by the schema
912cb6d6f1SJim Jagielski    mk("goodid", "1.2", '<text:p xml:id="a1">x</text:p><text:p xml:id=" b-2.c ">y</text:p>')
922cb6d6f1SJim Jagielski    mk("dupid", "1.2", '<text:p xml:id="a1">x</text:p><text:p xml:id=" a1 ">y</text:p>')
932cb6d6f1SJim Jagielski    mk("badid", "1.2", '<text:p xml:id="1abc">x</text:p>')
942cb6d6f1SJim Jagielski    mk("badid2", "1.2", '<text:p xml:id="a b">x</text:p>')
952cb6d6f1SJim Jagielski    mk("badns", "1.2", "<text:p><zz:span/></text:p>")
962cb6d6f1SJim Jagielski    # Part 3 2.2.1 F: signature files are held to the same rules (validate.py does not look at them)
972cb6d6f1SJim Jagielski    mk("badsig", "1.2", "<text:p>hi</text:p>", {"META-INF/documentsignatures.xml": '<a><b xml:id="x"/><b xml:id="x"/></a>'})
982cb6d6f1SJim Jagielski    # Part 1 2.2.2 B.1: valid as an extended document only (foreign attribute, foreign element in a paragraph, foreign element elsewhere)
992cb6d6f1SJim Jagielski    mk("extforeign", "1.2", '<text:p xmlns:foo="urn:foo" foo:x="1">a<foo:y>b</foo:y>c</text:p><foo:w xmlns:foo="urn:foo">z<text:p>q</text:p></foo:w>')
1002cb6d6f1SJim Jagielski    TEXT = "application/vnd.oasis.opendocument.text"
1012cb6d6f1SJim Jagielski    # meta:creator-initials in office:annotation is new in ODF 1.3; baseinit is the same note without it
1022cb6d6f1SJim Jagielski    ANN = '<office:body><office:text><text:p><office:annotation><dc:creator>A</dc:creator>%s<text:p>n</text:p></office:annotation>x</text:p></office:text></office:body>'
1032cb6d6f1SJim Jagielski    for n, ini in (("baseinit", ""), ("newinit", "<meta:creator-initials>AB</meta:creator-initials>")):
1042cb6d6f1SJim Jagielski        mkdoc(n, "odt", TEXT, {"content.xml": ("office:document-content", ANN % ini)})
1052cb6d6f1SJim Jagielski    # chart:coordinate-region (inner plot position) is new in 1.3; baseregion is the same chart without it
1062cb6d6f1SJim Jagielski    CHART = "application/vnd.oasis.opendocument.chart"
1072cb6d6f1SJim Jagielski    for n, region in (("baseregion", ""), ("newregion", '<chart:coordinate-region svg:x="1cm" svg:y="1cm" svg:width="5cm" svg:height="4cm"/>')):
1082cb6d6f1SJim Jagielski        mkdoc(n, "odc", CHART, {"content.xml": ("office:document-content", '<office:body><office:chart><chart:chart chart:class="chart:bar"><chart:plot-area>%s</chart:plot-area></chart:chart></office:chart></office:body>' % region)})
1092cb6d6f1SJim Jagielski    # 1.3 makes style:scale-to, style:scale-to-pages and the pair scale-to-X/scale-to-Y mutually exclusive (OFFICE-3857); 1.2 has no scale-to-X/Y
1102cb6d6f1SJim Jagielski    for n, attrs in (("scaleone", 'style:scale-to="80%"'), ("scalexy", 'style:scale-to-X="2" style:scale-to-Y="3"'), ("scaleboth", 'style:scale-to-X="2" style:scale-to-Y="3" style:scale-to="80%"')):
1112cb6d6f1SJim Jagielski        mkdoc(n, "odt", TEXT, {"content.xml": ("office:document-content", "<office:body><office:text><text:p>hi</text:p></office:text></office:body>"),
1122cb6d6f1SJim Jagielski                              "styles.xml": ("office:document-styles", '<office:automatic-styles><style:page-layout style:name="pm1"><style:page-layout-properties %s/></style:page-layout></office:automatic-styles>' % attrs)})
113*6e223505SJim Jagielski    # 1.4 (OFFICE-4142): draw:decorative on style:graphic-properties is new, invalid in 1.2 and 1.3; basedeco is the same style without it
114*6e223505SJim Jagielski    for n, attrs in (("basedeco", ""), ("newdeco", ' draw:decorative="true"')):
115*6e223505SJim Jagielski        mkdoc(n, "odt", TEXT, {"content.xml": ("office:document-content", "<office:body><office:text><text:p>hi</text:p></office:text></office:body>"),
116*6e223505SJim Jagielski                              "styles.xml": ("office:document-styles", '<office:automatic-styles><style:style style:name="gr1" style:family="graphic"><style:graphic-properties%s/></style:style></office:automatic-styles>' % attrs)})
117*6e223505SJim Jagielski    # 1.4 (OFFICE-4110): draw:handle needs the new position-x/-y or polar-pole pair; the old combined attribute alone is valid in 1.2 and 1.3 only.
118*6e223505SJim Jagielski    # The pair itself is invalid in 1.2 and 1.3.
119*6e223505SJim Jagielski    for n, attrs in (("handleold", 'draw:handle-position="0 0"'), ("handlenew", 'draw:handle-position-x="0" draw:handle-position-y="0"'),
120*6e223505SJim Jagielski                     ("handlepolar", 'draw:handle-polar-pole-x="0" draw:handle-polar-pole-y="0"')):
121*6e223505SJim Jagielski        mkdoc(n, "odt", TEXT, {"content.xml": ("office:document-content", '<office:body><office:text><text:p><draw:custom-shape><draw:enhanced-geometry><draw:handle %s/></draw:enhanced-geometry></draw:custom-shape></text:p></office:text></office:body>' % attrs)})
1222cb6d6f1SJim Jagielski
1232cb6d6f1SJim Jagielski
1242cb6d6f1SJim Jagielskidef make_pkg_controls(outdir):
1252cb6d6f1SJim Jagielski    """Writes pkgcheck's negative-control packages and .expected.json into `outdir` (empty or absent), or only builds the table when
1262cb6d6f1SJim Jagielski    `outdir` is None. Returns {package: sorted [(level, rule)]}: the findings pkgcheck.check() must report, exactly."""
1272cb6d6f1SJim Jagielski    C = None if outdir is None else os.fspath(outdir)
1282cb6d6f1SJim Jagielski    MIME = "application/vnd.oasis.opendocument.text"
1292cb6d6f1SJim Jagielski    S, Z = zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED
1302cb6d6f1SJim Jagielski    SIG = ('<?xml version="1.0" encoding="UTF-8"?>\n<dsig:document-signatures xmlns:dsig="urn:oasis:names:tc:opendocument:xmlns:digitalsignature:1.0" '
1312cb6d6f1SJim Jagielski           'dsig:version="%s" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:Signature Id="s1"/></dsig:document-signatures>\n')
1322cb6d6f1SJim Jagielski    ENC = ('<manifest:encryption-data manifest:checksum-type="SHA1/1K" manifest:checksum="AAAA">'
1332cb6d6f1SJim Jagielski           '<manifest:algorithm manifest:algorithm-name="Blowfish CFB" manifest:initialisation-vector="AAAA"/>'
1342cb6d6f1SJim Jagielski           '<manifest:key-derivation manifest:key-derivation-name="PBKDF2" manifest:key-size="16" manifest:iteration-count="1024" manifest:salt="AAAA"/>'
1352cb6d6f1SJim Jagielski           '</manifest:encryption-data>')
1362cb6d6f1SJim Jagielski
1372cb6d6f1SJim Jagielski
1382cb6d6f1SJim Jagielski    def entry(path, mt="text/xml", inner="", **attrs):
1392cb6d6f1SJim Jagielski        a = "".join(' manifest:%s="%s"' % kv for kv in attrs.items())
1402cb6d6f1SJim Jagielski        return '<manifest:file-entry manifest:full-path="%s" manifest:media-type="%s"%s>%s</manifest:file-entry>' % (path, mt, a, inner)
1412cb6d6f1SJim Jagielski
1422cb6d6f1SJim Jagielski
1432cb6d6f1SJim Jagielski    def manifest(*entries, root="manifest:manifest"):
1442cb6d6f1SJim Jagielski        return ('<?xml version="1.0" encoding="UTF-8"?>\n<%s xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">%s</%s>\n'
1452cb6d6f1SJim Jagielski                % (root, "".join(entries), root))
1462cb6d6f1SJim Jagielski
1472cb6d6f1SJim Jagielski
1482cb6d6f1SJim Jagielski    ROOT = entry("/", MIME, version="1.2")
1492cb6d6f1SJim Jagielski    CONTENT = entry("content.xml")
1502cb6d6f1SJim Jagielski    GOOD_MANI = manifest(ROOT, CONTENT)
1512cb6d6f1SJim Jagielski    EXPECTED = {}
1522cb6d6f1SJim Jagielski
1532cb6d6f1SJim Jagielski
1542cb6d6f1SJim Jagielski    def build(name, files, expect=()):
1552cb6d6f1SJim Jagielski        """files: (zip name, data, method[, extra]) in physical order."""
1562cb6d6f1SJim Jagielski        with zipfile.ZipFile(_target(C, name + ".odt"), "w") as z, warnings.catch_warnings():
1572cb6d6f1SJim Jagielski            warnings.simplefilter("ignore")
1582cb6d6f1SJim Jagielski            for f in files:
1592cb6d6f1SJim Jagielski                zi = zipfile.ZipInfo(f[0])
1602cb6d6f1SJim Jagielski                if len(f) > 3:
1612cb6d6f1SJim Jagielski                    zi.extra = f[3]
1622cb6d6f1SJim Jagielski                z.writestr(zi, f[1], compress_type=f[2])
1632cb6d6f1SJim Jagielski        EXPECTED[name + ".odt"] = sorted(expect)
1642cb6d6f1SJim Jagielski
1652cb6d6f1SJim Jagielski
1662cb6d6f1SJim Jagielski    def std(mani=GOOD_MANI, mime=MIME, extra=()):
1672cb6d6f1SJim Jagielski        return [("mimetype", mime, S), ("content.xml", "<c/>", Z)] + list(extra) + [("META-INF/manifest.xml", mani, Z)]
1682cb6d6f1SJim Jagielski
1692cb6d6f1SJim Jagielski
1702cb6d6f1SJim Jagielski    _prepare(C)
1712cb6d6f1SJim Jagielski    build("good", std())
1722cb6d6f1SJim Jagielski    sub = [("Object 1/content.xml", "<c/>", Z), ("Pictures/a.png", "x", S), ("secret.xml", "0123456789", S),
1732cb6d6f1SJim Jagielski           ("META-INF/documentsignatures.xml", SIG % "1.2", Z)]
1742cb6d6f1SJim Jagielski    build("good-full", std(manifest(ROOT, CONTENT, entry("Object 1/", MIME, version="1.2"), entry("Object 1/content.xml"),
1752cb6d6f1SJim Jagielski                                    entry("Pictures/a.png", "image/png"), entry("secret.xml", inner=ENC, size="10")), extra=sub))
1762cb6d6f1SJim Jagielski    build("mime-deflated", [("mimetype", MIME, Z), ("content.xml", "<c/>", Z), ("META-INF/manifest.xml", GOOD_MANI, Z)], [("shall", "mimetype-compressed")])
1772cb6d6f1SJim Jagielski    build("mime-extra", [("mimetype", MIME, S, b"\xfe\xca\x00\x00"), ("content.xml", "<c/>", Z), ("META-INF/manifest.xml", GOOD_MANI, Z)], [("shall", "mimetype-extra")])
1782cb6d6f1SJim Jagielski    build("mime-notfirst", [("content.xml", "<c/>", Z), ("mimetype", MIME, S), ("META-INF/manifest.xml", GOOD_MANI, Z)], [("shall", "mimetype-first")])
1792cb6d6f1SJim Jagielski    build("mime-mismatch", std(mime="application/x-bogus"), [("shall", "mimetype-mismatch")])
1802cb6d6f1SJim Jagielski    build("mime-nonascii", std(mime=MIME + "é"), [("shall", "mimetype-ascii")])
1812cb6d6f1SJim Jagielski    build("mime-missing", std()[1:], [("should", "mimetype-missing"), ("shall", "mimetype-missing")])
1822cb6d6f1SJim Jagielski    build("mime-missing-noroot", std(manifest(CONTENT))[1:], [("should", "mimetype-missing")])
1832cb6d6f1SJim Jagielski    build("no-root-entry", std(manifest(CONTENT)), [("shall", "manifest-root-entry")])
1842cb6d6f1SJim Jagielski    build("uncovered", std(extra=[("extra.xml", "<e/>", Z)]), [("shall", "manifest-coverage")])
1852cb6d6f1SJim Jagielski    build("dup-entry", std(manifest(ROOT, CONTENT, CONTENT)), [("shall", "manifest-duplicate")])
1862cb6d6f1SJim Jagielski    build("self-entry", std(manifest(ROOT, CONTENT, entry("mimetype", "text/plain"))), [("shall", "manifest-self")])
1872cb6d6f1SJim Jagielski    build("dangling", std(manifest(ROOT, CONTENT, entry("gone.xml"))), [("should", "manifest-dangling")])
1882cb6d6f1SJim Jagielski    build("dir-entry-manifest", std(manifest(ROOT, CONTENT, entry("Configurations2/", "application/vnd.sun.xml.ui.configuration"))), [("should", "manifest-dir-entry")])
1892cb6d6f1SJim Jagielski    build("stray-meta-inf", std(extra=[("META-INF/foo.xml", "<f/>", Z)]), [("shall", "meta-inf-file")])
1902cb6d6f1SJim Jagielski    build("no-manifest", [("mimetype", MIME, S), ("content.xml", "<c/>", Z)], [("shall", "manifest-missing")])
1912cb6d6f1SJim Jagielski    build("manifest-malformed", std("<manifest:manifest"), [("shall", "manifest-malformed")])
1922cb6d6f1SJim Jagielski    build("manifest-wrongroot", std(manifest(root="manifest:other")), [("shall", "manifest-root")])
1932cb6d6f1SJim Jagielski    build("zip-bzip2", std(extra=[("b.xml", "<b/>", zipfile.ZIP_BZIP2)], mani=manifest(ROOT, CONTENT, entry("b.xml"))), [("shall", "zip-method")])
1942cb6d6f1SJim Jagielski    build("zip-dup-name", std(extra=[("content.xml", "<d/>", Z)]), [("shall", "zip-duplicate")])
1952cb6d6f1SJim Jagielski    build("zip-dir-entry", std(extra=[("Pictures/", "", S)]), [("should", "zip-dir-entry")])
1962cb6d6f1SJim Jagielski    build("enc-nosize", std(manifest(ROOT, CONTENT, entry("secret.xml", inner=ENC)), extra=[("secret.xml", "0123456789", S)]), [("shall", "encrypted-size")])
1972cb6d6f1SJim Jagielski    build("enc-deflated", std(manifest(ROOT, CONTENT, entry("secret.xml", inner=ENC, size="10")), extra=[("secret.xml", "0123456789", Z)]), [("shall", "encrypted-method")])
1982cb6d6f1SJim Jagielski    build("sig-malformed", std(extra=[("META-INF/documentsignatures.xml", "<dsig", Z)]), [("shall", "signature-malformed")])
1992cb6d6f1SJim Jagielski    build("sig-wrongroot", std(extra=[("META-INF/documentsignatures.xml", "<a/>", Z)]), [("shall", "signature-root")])
2002cb6d6f1SJim Jagielski    build("sig-badversion", std(extra=[("META-INF/documentsignatures.xml", SIG % "1.1", Z)]), [("shall", "signature-schema")])
2012cb6d6f1SJim Jagielski    if C:
2022cb6d6f1SJim Jagielski        json.dump(EXPECTED, open(os.path.join(C, ".expected.json"), "w"), indent=1)
2032cb6d6f1SJim Jagielski    return EXPECTED
2042cb6d6f1SJim Jagielski
2052cb6d6f1SJim Jagielski
2062cb6d6f1SJim Jagielskidef make_doctype_controls(outdir):
2072cb6d6f1SJim Jagielski    """Writes doctypecheck's negative-control documents and .expected.json into `outdir` (empty or absent), or only builds the table
2082cb6d6f1SJim Jagielski    when `outdir` is None. Returns {document: sorted [(level, rule)]}."""
2092cb6d6f1SJim Jagielski    C = None if outdir is None else os.fspath(outdir)
2102cb6d6f1SJim Jagielski    P = "application/vnd.oasis.opendocument."
2112cb6d6f1SJim Jagielski    NS = ('xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML"')
2122cb6d6f1SJim Jagielski    S, Z = zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED
2132cb6d6f1SJim Jagielski    EXPECTED = {}
2142cb6d6f1SJim Jagielski
2152cb6d6f1SJim Jagielski
2162cb6d6f1SJim Jagielski    def content(child="text", root="office:document-content", extra=""):
2172cb6d6f1SJim Jagielski        body = "<office:body>%s</office:body>" % ("<office:%s/>" % child if child else "")
2182cb6d6f1SJim Jagielski        return '<?xml version="1.0" encoding="UTF-8"?>\n<%s %s office:version="1.2"%s>%s</%s>\n' % (root, NS, extra, body, root)
2192cb6d6f1SJim Jagielski
2202cb6d6f1SJim Jagielski
2212cb6d6f1SJim Jagielski    MATHDOC = '<?xml version="1.0" encoding="UTF-8"?>\n<math:math %s><math:mi>x</math:mi></math:math>\n' % NS
2222cb6d6f1SJim Jagielski
2232cb6d6f1SJim Jagielski
2242cb6d6f1SJim Jagielski    def pkg(name, mime, body, expect=(), files=None, manifest=""):
2252cb6d6f1SJim Jagielski        """files: replaces the default content.xml; manifest: extra file-entry elements."""
2262cb6d6f1SJim Jagielski        with zipfile.ZipFile(_target(C, name + ".odt"), "w") as z:
2272cb6d6f1SJim Jagielski            if mime is not None:
2282cb6d6f1SJim Jagielski                z.writestr(zipfile.ZipInfo("mimetype"), mime, compress_type=S)
2292cb6d6f1SJim Jagielski            for n, d in (files if files is not None else {"content.xml": body}).items():
2302cb6d6f1SJim Jagielski                z.writestr(n, d, compress_type=Z)
2312cb6d6f1SJim Jagielski            z.writestr("META-INF/manifest.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<manifest:manifest xmlns:manifest='
2322cb6d6f1SJim Jagielski                       '"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">%s</manifest:manifest>\n' % manifest, compress_type=Z)
2332cb6d6f1SJim Jagielski        EXPECTED[name + ".odt"] = sorted(expect)
2342cb6d6f1SJim Jagielski
2352cb6d6f1SJim Jagielski
2362cb6d6f1SJim Jagielski    def flat(name, mime, child="text", expect=()):
2372cb6d6f1SJim Jagielski        attr = "" if mime is None else ' office:mimetype="%s"' % mime
2382cb6d6f1SJim Jagielski        _write(C, name + ".fodt", content(child, "office:document", attr))
2392cb6d6f1SJim Jagielski        EXPECTED[name + ".fodt"] = sorted(expect)
2402cb6d6f1SJim Jagielski
2412cb6d6f1SJim Jagielski
2422cb6d6f1SJim Jagielski    _prepare(C)
2432cb6d6f1SJim Jagielski    for t, ms, child in (("text", ["text", "text-template", "text-master"], "text"), ("spreadsheet", ["spreadsheet", "spreadsheet-template"], "spreadsheet"),
2442cb6d6f1SJim Jagielski                         ("drawing", ["graphics", "graphics-template"], "drawing"), ("presentation", ["presentation", "presentation-template"], "presentation"),
2452cb6d6f1SJim Jagielski                         ("chart", ["chart", "chart-template"], "chart"), ("image", ["image", "image-template"], "image"), ("database", ["base"], "database")):
2462cb6d6f1SJim Jagielski        for m in ms:
2472cb6d6f1SJim Jagielski            pkg("good-" + m, P + m, content(child))
2482cb6d6f1SJim Jagielski    pkg("good-formula", P + "formula", MATHDOC)
2492cb6d6f1SJim Jagielski    sub = lambda mt, c: {"content.xml": content(), "Object 1/content.xml": c}
2502cb6d6f1SJim Jagielski    ent = lambda mt: '<manifest:file-entry manifest:full-path="Object 1/" manifest:media-type="%s"/>' % mt
2512cb6d6f1SJim Jagielski    pkg("good-embedded", P + "text", None, files=sub(P + "chart", content("chart")), manifest=ent(P + "chart"))
2522cb6d6f1SJim Jagielski    pkg("good-embedded-math", P + "text", None, files=sub("", MATHDOC), manifest=ent(P + "formula"))
2532cb6d6f1SJim Jagielski    flat("good-flat", P + "text")
2542cb6d6f1SJim Jagielski    for m, ru in (("application/x-bogus", "doctype-unknown-mimetype"), (P + "text\n", "doctype-unknown-mimetype"),
2552cb6d6f1SJim Jagielski                  ("application.vnd.oasis.opendocument.graphics", "doctype-unknown-mimetype"),
2562cb6d6f1SJim Jagielski                  (P + "text-web", "doctype-unlisted-mimetype"), (P + "formula-template", "doctype-unlisted-mimetype")):
2572cb6d6f1SJim Jagielski        pkg("bad-mime-" + str(len(EXPECTED)), m, content(), [("shall", ru)])
2582cb6d6f1SJim Jagielski    pkg("no-mimetype", None, content(), [("shall", "doctype-no-mimetype")])
2592cb6d6f1SJim Jagielski    pkg("body-mismatch", P + "text", content("spreadsheet"), [("shall", "doctype-body")])
2602cb6d6f1SJim Jagielski    pkg("body-empty", P + "text", content(None), [("shall", "doctype-body")])
2612cb6d6f1SJim Jagielski    pkg("root-styles", P + "text", content(root="office:document-styles"), [("shall", "doctype-root")])
2622cb6d6f1SJim Jagielski    pkg("formula-wrong-root", P + "formula", content(), [("shall", "doctype-root")])
2632cb6d6f1SJim Jagielski    pkg("math-in-text", P + "text", MATHDOC, [("shall", "doctype-root")])
2642cb6d6f1SJim Jagielski    pkg("only-styles", P + "text", None, [("shall", "doctype-no-body")], files={"styles.xml": content(None, "office:document-styles")})
2652cb6d6f1SJim Jagielski    pkg("only-meta", P + "text", None, [("shall", "doctype-no-content"), ("shall", "doctype-no-body")], files={"meta.xml": "<a/>"})
2662cb6d6f1SJim Jagielski    pkg("malformed", P + "text", "<office:document-content", [("shall", "doctype-malformed")])
2672cb6d6f1SJim Jagielski    pkg("sub-body", P + "text", None, [("should", "sub-doctype-body")], files=sub(P + "chart", content("spreadsheet")), manifest=ent(P + "chart"))
2682cb6d6f1SJim Jagielski    pkg("sub-root", P + "text", None, [("should", "sub-doctype-root")], files=sub(P + "formula", content()), manifest=ent(P + "formula"))
2692cb6d6f1SJim Jagielski    flat("flat-no-mimetype", None, expect=[("shall", "doctype-no-mimetype")])
2702cb6d6f1SJim Jagielski    flat("flat-unknown", "application/x-bogus", expect=[("shall", "doctype-unknown-mimetype")])
2712cb6d6f1SJim Jagielski    flat("flat-formula-mime", P + "formula", expect=[("shall", "doctype-unknown-mimetype")])
2722cb6d6f1SJim Jagielski    flat("flat-body", P + "text", "presentation", [("shall", "doctype-body")])
2732cb6d6f1SJim Jagielski    if C:
2742cb6d6f1SJim Jagielski        json.dump(EXPECTED, open(os.path.join(C, ".expected.json"), "w"), indent=1)
2752cb6d6f1SJim Jagielski    return EXPECTED
276