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