1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22# usage: python -m aootest.checkers.idcheck <outdir> <ids.json> (tests call check(outdir)) 23# ODF 1.2 Part 1 2.2.1 D.1 and D.2, and Part 3 2.2.1 F, which `jing -i` does not check: each XML stream is 24# parsed namespace-aware, and its xml:id values must be NCNames, unique within the stream. 25import sys, os, re, zipfile, json 26from xml.parsers import expat 27 28STREAMS = {"content.xml", "styles.xml", "meta.xml", "settings.xml", "manifest.xml"} 29XMLID = "http://www.w3.org/XML/1998/namespace id" 30# XML 1.0 5th ed. NameStartChar/NameChar without ':' 31_S = "A-Z_a-zÀ-ÖØ-öø-˿Ͱ-ͽͿ--⁰-Ⰰ-、-豈-﷏ﷰ-�\U00010000-\U000EFFFF" 32NCNAME = re.compile("[" + _S + "][" + _S + r"\-.0-9·̀-ͯ‿-⁀]*\Z") 33 34 35def check_stream(data): 36 """Returns [(line, col, message)].""" 37 errs, seen = [], {} 38 p = expat.ParserCreate(namespace_separator=" ") 39 40 def start(name, attrs): 41 if XMLID not in attrs: 42 return 43 # xml:id 1.0 sec. 4: normalize like an ID-typed attribute 44 v = " ".join(attrs[XMLID].split()) 45 pos = (p.CurrentLineNumber, p.CurrentColumnNumber) 46 if not NCNAME.match(v): 47 errs.append(pos + ('xml:id "%s" is not an NCName' % v,)) 48 elif v in seen: 49 errs.append(pos + ('xml:id "%s" duplicates line %d' % (v, seen[v]),)) 50 else: 51 seen[v] = pos[0] 52 p.StartElementHandler = start 53 try: 54 p.Parse(data, True) 55 except expat.ExpatError as e: 56 errs.append((e.lineno, e.offset, "not namespace-well-formed: " + expat.ErrorString(e.code))) 57 return errs 58 59 60def check(outdir): 61 """Returns the result that main() writes as <ids.json>.""" 62 errors, bad_zip, nstreams = [], [], 0 63 pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith(".")) 64 for pkg in pkgs: 65 try: 66 z = zipfile.ZipFile(os.path.join(outdir, pkg)) 67 except Exception: 68 bad_zip.append(pkg); continue 69 for name in z.namelist(): 70 base = name.rsplit("/", 1)[-1] 71 if base not in STREAMS and not (name.startswith("META-INF/") and "signatures" in base): 72 continue 73 nstreams += 1 74 for line, col, msg in check_stream(z.read(name)): 75 errors.append([pkg + "/" + name, line, col, msg]) 76 return {"packages": pkgs, "bad_zip": bad_zip, "nstreams": nstreams, "errors": errors} 77 78 79def main(argv=None): 80 outdir, idsjson = sys.argv[1:3] if argv is None else argv 81 res = check(outdir) 82 json.dump(res, open(idsjson, "w")) 83 errors, pkgs, bad_zip, nstreams = res["errors"], res["packages"], res["bad_zip"], res["nstreams"] 84 print(f"xml:id / namespace check: {len(errors)} errors in {len({e[0].split('/')[0] for e in errors})} of {len(pkgs)} packages, " 85 f"streams checked: {nstreams}, bad zips: {bad_zip}") 86 87 88if __name__ == "__main__": 89 main() 90