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# usage: python -m aootest.checkers.validate <outdir> <val.json> (needs AOO_JING and AOO_SCHEMA_DIR; tests call check()) 23*6e223505SJim Jagielski# Validates every package in <outdir> against ODF 1.2 as written, against ODF 1.3 and 1.4 with an office:version or 24*6e223505SJim Jagielski# manifest:version of 1.2 or 1.3 rewritten to the target ("13", "14"; any other version stays and is reported), and against 25*6e223505SJim Jagielski# ODF 1.2, 1.3 and 1.4 after removing foreign elements and attributes ("12x", "13x", "14x"; Part 1 2.2.2 B.1; manifest left 26*6e223505SJim Jagielski# as written apart from the version). 272cb6d6f1SJim Jagielskiimport sys, os, re, zipfile, subprocess, json 282cb6d6f1SJim Jagielski 292cb6d6f1SJim Jagielskifrom aootest.checkers import foreign 302cb6d6f1SJim Jagielski 312cb6d6f1SJim JagielskiSCHEMA_FILES = {"12": ("OpenDocument-v1.2-os-schema.rng", "OpenDocument-v1.2-os-manifest-schema.rng"), 322cb6d6f1SJim Jagielski "13": ("OpenDocument-v1.3-schema.rng", "OpenDocument-v1.3-manifest-schema.rng"), 332cb6d6f1SJim Jagielski "14": ("OpenDocument-v1.4-schema.rng", "OpenDocument-v1.4-manifest-schema.rng")} 34*6e223505SJim JagielskiVERSIONS = ("12", "14", "12x", "13", "13x", "14x") 352cb6d6f1SJim JagielskiSTREAMS = {"content.xml", "styles.xml", "meta.xml", "settings.xml"} 362cb6d6f1SJim Jagielski# ODF 1.2 Part 1 2.2.1 B.4: a content.xml rooted at MathML is validated against MathML, not ODF 372cb6d6f1SJim JagielskiERR = re.compile(r"^(.*?):(\d+):(\d+): (error|fatal|warning): (.*)$") 382cb6d6f1SJim Jagielski 392cb6d6f1SJim Jagielski 402cb6d6f1SJim Jagielskidef root_is_mathml(text): 412cb6d6f1SJim Jagielski body = re.sub(r'<\?xml[^>]*\?>|<!DOCTYPE[^>]*>|<!--.*?-->', '', text[:4000], flags=re.S).lstrip() 422cb6d6f1SJim Jagielski return bool(re.match(r'<(?:[\w.-]+:)?math[\s>/]', body)) 432cb6d6f1SJim Jagielski 442cb6d6f1SJim Jagielski 452cb6d6f1SJim Jagielskidef schemas(schema_dir): 462cb6d6f1SJim Jagielski sch = {v: tuple(os.path.join(schema_dir, f) for f in fs) for v, fs in SCHEMA_FILES.items()} 47*6e223505SJim Jagielski sch["12x"], sch["13x"], sch["14x"] = sch["12"], sch["13"], sch["14"] 482cb6d6f1SJim Jagielski return sch 492cb6d6f1SJim Jagielski 502cb6d6f1SJim Jagielski 512cb6d6f1SJim Jagielskidef check(outdir, workdir, jing, schema_dir): 522cb6d6f1SJim Jagielski """Returns the result that main() writes as <val.json>. `jing` is the command prefix (java, -jar, jar, -i); 532cb6d6f1SJim Jagielski `workdir` must be empty or absent: the checker writes its working copies there and never deletes anything.""" 542cb6d6f1SJim Jagielski work = os.path.abspath(workdir) 552cb6d6f1SJim Jagielski if os.path.isdir(work) and os.listdir(work): 562cb6d6f1SJim Jagielski raise ValueError("validate: workdir is not empty: " + work) 572cb6d6f1SJim Jagielski SCH = schemas(schema_dir) 582cb6d6f1SJim Jagielski files = {v: ([], []) for v in VERSIONS} 59*6e223505SJim Jagielski mixed = {v: foreign.mixed_elements(SCH[v][0]) for v in ("12x", "13x", "14x")} 602cb6d6f1SJim Jagielski skipped, bad_zip = [], [] 612cb6d6f1SJim Jagielski pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith(".")) 622cb6d6f1SJim Jagielski for pkg in pkgs: 632cb6d6f1SJim Jagielski try: 642cb6d6f1SJim Jagielski z = zipfile.ZipFile(os.path.join(outdir, pkg)) 652cb6d6f1SJim Jagielski except Exception: 662cb6d6f1SJim Jagielski bad_zip.append(pkg); continue 672cb6d6f1SJim Jagielski for name in z.namelist(): 682cb6d6f1SJim Jagielski is_mani = name == "META-INF/manifest.xml" 692cb6d6f1SJim Jagielski if name.rsplit("/", 1)[-1] not in STREAMS and not is_mani: 702cb6d6f1SJim Jagielski continue 712cb6d6f1SJim Jagielski text = z.read(name).decode("utf-8") 722cb6d6f1SJim Jagielski if not is_mani and name.endswith("content.xml") and root_is_mathml(text): 732cb6d6f1SJim Jagielski skipped.append(pkg + "/" + name); continue 742cb6d6f1SJim Jagielski for v in VERSIONS: 752cb6d6f1SJim Jagielski t = text 76*6e223505SJim Jagielski if v in mixed and not is_mani: 772cb6d6f1SJim Jagielski t = foreign.strip_foreign(t, mixed[v]) 78*6e223505SJim Jagielski if v in ("13", "13x", "14", "14x"): 792cb6d6f1SJim Jagielski attr = "manifest:version" if is_mani else "office:version" 80*6e223505SJim Jagielski t = re.sub(attr + '="1\\.[23]"', attr + '="1.%s"' % v[1], t, count=1) 812cb6d6f1SJim Jagielski p = os.path.join(work, v, pkg, name) 822cb6d6f1SJim Jagielski os.makedirs(os.path.dirname(p), exist_ok=True) 832cb6d6f1SJim Jagielski open(p, "w", encoding="utf-8").write(t) 842cb6d6f1SJim Jagielski files[v][1 if is_mani else 0].append(p) 852cb6d6f1SJim Jagielski errors = {} 862cb6d6f1SJim Jagielski for v in VERSIONS: 872cb6d6f1SJim Jagielski errs = [] 882cb6d6f1SJim Jagielski for schema, fl in ((SCH[v][0], files[v][0]), (SCH[v][1], files[v][1])): 892cb6d6f1SJim Jagielski while fl: 902cb6d6f1SJim Jagielski batch = fl[:400] 912cb6d6f1SJim Jagielski r = subprocess.run(jing + [schema] + batch, capture_output=True, text=True) 922cb6d6f1SJim Jagielski fl = fl[400:] 932cb6d6f1SJim Jagielski for line in (r.stdout + r.stderr).splitlines(): 942cb6d6f1SJim Jagielski m = ERR.match(line) 952cb6d6f1SJim Jagielski if m: 962cb6d6f1SJim 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)]) 972cb6d6f1SJim Jagielski # a fatal error stops jing: resume with the files after the one that caused it 982cb6d6f1SJim Jagielski if m.group(4) == "fatal" and m.group(1) in batch: 992cb6d6f1SJim Jagielski fl = batch[batch.index(m.group(1)) + 1:] + fl 1002cb6d6f1SJim Jagielski elif line.strip(): 1012cb6d6f1SJim Jagielski errs.append(["?", 0, 0, line.strip()]) 1022cb6d6f1SJim Jagielski errors[v] = errs 1032cb6d6f1SJim Jagielski return {"packages": pkgs, "bad_zip": bad_zip, "mathml_skipped": skipped, 1042cb6d6f1SJim Jagielski "nfiles": len(files["12"][0]) + len(files["12"][1]), "errors": errors} 1052cb6d6f1SJim Jagielski 1062cb6d6f1SJim Jagielski 1072cb6d6f1SJim Jagielskidef main(argv=None): 1082cb6d6f1SJim Jagielski outdir, valjson = sys.argv[1:3] if argv is None else argv 1092cb6d6f1SJim Jagielski res = check(outdir, valjson[:-5] + ".xml", ["java", "-jar", os.environ["AOO_JING"], "-i"], os.environ["AOO_SCHEMA_DIR"]) 1102cb6d6f1SJim Jagielski json.dump(res, open(valjson, "w")) 1112cb6d6f1SJim Jagielski errors, pkgs, skipped, bad_zip = res["errors"], res["packages"], res["mathml_skipped"], res["bad_zip"] 1122cb6d6f1SJim Jagielski for v in VERSIONS: 1132cb6d6f1SJim 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") 1142cb6d6f1SJim Jagielski print(f"files validated: {res['nfiles']}, MathML skipped: {len(skipped)}, bad zips: {bad_zip}") 1152cb6d6f1SJim Jagielski unparsed = [e for v in errors for e in errors[v] if e[0] == "?"] 1162cb6d6f1SJim Jagielski if unparsed: 1172cb6d6f1SJim Jagielski print("UNPARSED jing output:", *[e[3] for e in unparsed[:5]], sep="\n ") 1182cb6d6f1SJim Jagielski 1192cb6d6f1SJim Jagielski 1202cb6d6f1SJim Jagielskiif __name__ == "__main__": 1212cb6d6f1SJim Jagielski main() 122