xref: /trunk/test/odf-validate/validate.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
1*0e16a31dSJim Jagielski#**************************************************************
2*0e16a31dSJim Jagielski#
3*0e16a31dSJim Jagielski#  Licensed to the Apache Software Foundation (ASF) under one
4*0e16a31dSJim Jagielski#  or more contributor license agreements.  See the NOTICE file
5*0e16a31dSJim Jagielski#  distributed with this work for additional information
6*0e16a31dSJim Jagielski#  regarding copyright ownership.  The ASF licenses this file
7*0e16a31dSJim Jagielski#  to you under the Apache License, Version 2.0 (the
8*0e16a31dSJim Jagielski#  "License"); you may not use this file except in compliance
9*0e16a31dSJim Jagielski#  with the License.  You may obtain a copy of the License at
10*0e16a31dSJim Jagielski#
11*0e16a31dSJim Jagielski#    http://www.apache.org/licenses/LICENSE-2.0
12*0e16a31dSJim Jagielski#
13*0e16a31dSJim Jagielski#  Unless required by applicable law or agreed to in writing,
14*0e16a31dSJim Jagielski#  software distributed under the License is distributed on an
15*0e16a31dSJim Jagielski#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16*0e16a31dSJim Jagielski#  KIND, either express or implied.  See the License for the
17*0e16a31dSJim Jagielski#  specific language governing permissions and limitations
18*0e16a31dSJim Jagielski#  under the License.
19*0e16a31dSJim Jagielski#
20*0e16a31dSJim Jagielski#**************************************************************
21*0e16a31dSJim Jagielski
22*0e16a31dSJim Jagielski# usage: validate.py <outdir> <val.json>
23*0e16a31dSJim Jagielski# Validates every package in <outdir> against ODF 1.2 as written, against ODF 1.3 and 1.4 with the version rewritten
24*0e16a31dSJim Jagielski# ("13", "14"), and against ODF 1.2 and 1.3 after removing foreign elements and attributes ("12x", "13x"; Part 1 2.2.2 B.1;
25*0e16a31dSJim Jagielski# manifest left as written apart from the version).
26*0e16a31dSJim Jagielskiimport sys, os, re, zipfile, subprocess, json, shutil, importlib.util
27*0e16a31dSJim Jagielski
28*0e16a31dSJim JagielskiD = os.path.dirname(os.path.abspath(__file__))
29*0e16a31dSJim JagielskiJING = ["java", "-jar", os.environ.get("ODFV_JING", "/Applications/XMLauthor/lib/jing.jar"), "-i"]
30*0e16a31dSJim JagielskiSCH = {"12": (D + "/schema/OpenDocument-v1.2-os-schema.rng", D + "/schema/OpenDocument-v1.2-os-manifest-schema.rng"),
31*0e16a31dSJim Jagielski       "13": (D + "/schema/OpenDocument-v1.3-schema.rng", D + "/schema/OpenDocument-v1.3-manifest-schema.rng"),
32*0e16a31dSJim Jagielski       "14": (D + "/schema/OpenDocument-v1.4-schema.rng", D + "/schema/OpenDocument-v1.4-manifest-schema.rng")}
33*0e16a31dSJim JagielskiSCH["12x"] = SCH["12"]
34*0e16a31dSJim JagielskiSCH["13x"] = SCH["13"]
35*0e16a31dSJim JagielskiVERSIONS = ("12", "14", "12x", "13", "13x")
36*0e16a31dSJim Jagielski_spec = importlib.util.spec_from_file_location("foreign", D + "/foreign.py")
37*0e16a31dSJim Jagielskiforeign = importlib.util.module_from_spec(_spec)
38*0e16a31dSJim Jagielski_spec.loader.exec_module(foreign)
39*0e16a31dSJim JagielskiSTREAMS = {"content.xml", "styles.xml", "meta.xml", "settings.xml"}
40*0e16a31dSJim Jagielski# ODF 1.2 Part 1 2.2.1 B.4: a content.xml rooted at MathML is validated against MathML, not ODF
41*0e16a31dSJim JagielskiERR = re.compile(r"^(.*?):(\d+):(\d+): (error|fatal|warning): (.*)$")
42*0e16a31dSJim Jagielski
43*0e16a31dSJim Jagielski
44*0e16a31dSJim Jagielskidef root_is_mathml(text):
45*0e16a31dSJim Jagielski    body = re.sub(r'<\?xml[^>]*\?>|<!DOCTYPE[^>]*>|<!--.*?-->', '', text[:4000], flags=re.S).lstrip()
46*0e16a31dSJim Jagielski    return bool(re.match(r'<(?:[\w.-]+:)?math[\s>/]', body))
47*0e16a31dSJim Jagielski
48*0e16a31dSJim Jagielski
49*0e16a31dSJim Jagielskidef main():
50*0e16a31dSJim Jagielski    outdir, valjson = sys.argv[1], sys.argv[2]
51*0e16a31dSJim Jagielski    work = os.path.abspath(valjson[:-5] + ".xml")
52*0e16a31dSJim Jagielski    if not work.startswith(os.path.join(D, "runs") + os.sep):
53*0e16a31dSJim Jagielski        sys.exit("validate.py: <val.json> must live under " + os.path.join(D, "runs"))
54*0e16a31dSJim Jagielski    shutil.rmtree(work, ignore_errors=True)
55*0e16a31dSJim Jagielski    files = {v: ([], []) for v in VERSIONS}
56*0e16a31dSJim Jagielski    mixed = {"12x": foreign.mixed_elements(SCH["12"][0]), "13x": foreign.mixed_elements(SCH["13"][0])}
57*0e16a31dSJim Jagielski    skipped, bad_zip = [], []
58*0e16a31dSJim Jagielski    pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith("."))
59*0e16a31dSJim Jagielski    for pkg in pkgs:
60*0e16a31dSJim Jagielski        try:
61*0e16a31dSJim Jagielski            z = zipfile.ZipFile(os.path.join(outdir, pkg))
62*0e16a31dSJim Jagielski        except Exception:
63*0e16a31dSJim Jagielski            bad_zip.append(pkg); continue
64*0e16a31dSJim Jagielski        for name in z.namelist():
65*0e16a31dSJim Jagielski            is_mani = name == "META-INF/manifest.xml"
66*0e16a31dSJim Jagielski            if name.rsplit("/", 1)[-1] not in STREAMS and not is_mani:
67*0e16a31dSJim Jagielski                continue
68*0e16a31dSJim Jagielski            text = z.read(name).decode("utf-8")
69*0e16a31dSJim Jagielski            if not is_mani and name.endswith("content.xml") and root_is_mathml(text):
70*0e16a31dSJim Jagielski                skipped.append(pkg + "/" + name); continue
71*0e16a31dSJim Jagielski            for v in VERSIONS:
72*0e16a31dSJim Jagielski                t = text
73*0e16a31dSJim Jagielski                if v in ("12x", "13x") and not is_mani:
74*0e16a31dSJim Jagielski                    t = foreign.strip_foreign(t, mixed[v])
75*0e16a31dSJim Jagielski                if v in ("13", "13x", "14"):
76*0e16a31dSJim Jagielski                    attr = "manifest:version" if is_mani else "office:version"
77*0e16a31dSJim Jagielski                    t = re.sub(attr + '="1.2"', attr + '="1.%s"' % v[1], t, count=1)
78*0e16a31dSJim Jagielski                p = os.path.join(work, v, pkg, name)
79*0e16a31dSJim Jagielski                os.makedirs(os.path.dirname(p), exist_ok=True)
80*0e16a31dSJim Jagielski                open(p, "w", encoding="utf-8").write(t)
81*0e16a31dSJim Jagielski                files[v][1 if is_mani else 0].append(p)
82*0e16a31dSJim Jagielski    errors = {}
83*0e16a31dSJim Jagielski    for v in VERSIONS:
84*0e16a31dSJim Jagielski        errs = []
85*0e16a31dSJim Jagielski        for schema, fl in ((SCH[v][0], files[v][0]), (SCH[v][1], files[v][1])):
86*0e16a31dSJim Jagielski            while fl:
87*0e16a31dSJim Jagielski                batch = fl[:400]
88*0e16a31dSJim Jagielski                r = subprocess.run(JING + [schema] + batch, capture_output=True, text=True)
89*0e16a31dSJim Jagielski                fl = fl[400:]
90*0e16a31dSJim Jagielski                for line in (r.stdout + r.stderr).splitlines():
91*0e16a31dSJim Jagielski                    m = ERR.match(line)
92*0e16a31dSJim Jagielski                    if m:
93*0e16a31dSJim Jagielski                        errs.append([os.path.relpath(m.group(1), os.path.join(work, v)), int(m.group(2)), int(m.group(3)), m.group(5)])
94*0e16a31dSJim Jagielski                        # a fatal error stops jing: resume with the files after the one that caused it
95*0e16a31dSJim Jagielski                        if m.group(4) == "fatal" and m.group(1) in batch:
96*0e16a31dSJim Jagielski                            fl = batch[batch.index(m.group(1)) + 1:] + fl
97*0e16a31dSJim Jagielski                    elif line.strip():
98*0e16a31dSJim Jagielski                        errs.append(["?", 0, 0, line.strip()])
99*0e16a31dSJim Jagielski        errors[v] = errs
100*0e16a31dSJim Jagielski    json.dump({"packages": pkgs, "bad_zip": bad_zip, "mathml_skipped": skipped,
101*0e16a31dSJim Jagielski               "nfiles": len(files["12"][0]) + len(files["12"][1]), "errors": errors}, open(valjson, "w"))
102*0e16a31dSJim Jagielski    for v in VERSIONS:
103*0e16a31dSJim Jagielski        print(f"ODF {v[0]}.{v[1]}{' extended, foreign content removed' if v.endswith('x') else ''}: {len(errors[v])} errors in {len({e[0].split('/')[0] for e in errors[v]})} of {len(pkgs)} packages")
104*0e16a31dSJim Jagielski    print(f"files validated: {len(files['12'][0]) + len(files['12'][1])}, MathML skipped: {len(skipped)}, bad zips: {bad_zip}")
105*0e16a31dSJim Jagielski    unparsed = [e for v in errors for e in errors[v] if e[0] == "?"]
106*0e16a31dSJim Jagielski    if unparsed:
107*0e16a31dSJim Jagielski        print("UNPARSED jing output:", *[e[3] for e in unparsed[:5]], sep="\n  ")
108*0e16a31dSJim Jagielski
109*0e16a31dSJim Jagielski
110*0e16a31dSJim Jagielskimain()
111