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: idcheck.py <outdir> <ids.json> 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 28D = os.path.dirname(os.path.abspath(__file__)) 29STREAMS = {"content.xml", "styles.xml", "meta.xml", "settings.xml", "manifest.xml"} 30XMLID = "http://www.w3.org/XML/1998/namespace id" 31# XML 1.0 5th ed. NameStartChar/NameChar without ':' 32_S = "A-Z_a-zÀ-ÖØ-öø-˿Ͱ-ͽͿ--⁰-Ⰰ-、-豈-﷏ﷰ-�\U00010000-\U000EFFFF" 33NCNAME = re.compile("[" + _S + "][" + _S + r"\-.0-9·̀-ͯ‿-⁀]*\Z") 34 35 36def check_stream(data): 37 """Returns [(line, col, message)].""" 38 errs, seen = [], {} 39 p = expat.ParserCreate(namespace_separator=" ") 40 41 def start(name, attrs): 42 if XMLID not in attrs: 43 return 44 # xml:id 1.0 sec. 4: normalize like an ID-typed attribute 45 v = " ".join(attrs[XMLID].split()) 46 pos = (p.CurrentLineNumber, p.CurrentColumnNumber) 47 if not NCNAME.match(v): 48 errs.append(pos + ('xml:id "%s" is not an NCName' % v,)) 49 elif v in seen: 50 errs.append(pos + ('xml:id "%s" duplicates line %d' % (v, seen[v]),)) 51 else: 52 seen[v] = pos[0] 53 p.StartElementHandler = start 54 try: 55 p.Parse(data, True) 56 except expat.ExpatError as e: 57 errs.append((e.lineno, e.offset, "not namespace-well-formed: " + expat.ErrorString(e.code))) 58 return errs 59 60 61def main(): 62 outdir, idsjson = sys.argv[1], sys.argv[2] 63 if not os.path.abspath(idsjson).startswith(os.path.join(D, "runs") + os.sep): 64 sys.exit("idcheck.py: <ids.json> must live under " + os.path.join(D, "runs")) 65 errors, bad_zip, nstreams = [], [], 0 66 pkgs = sorted(p for p in os.listdir(outdir) if not p.startswith(".")) 67 for pkg in pkgs: 68 try: 69 z = zipfile.ZipFile(os.path.join(outdir, pkg)) 70 except Exception: 71 bad_zip.append(pkg); continue 72 for name in z.namelist(): 73 base = name.rsplit("/", 1)[-1] 74 if base not in STREAMS and not (name.startswith("META-INF/") and "signatures" in base): 75 continue 76 nstreams += 1 77 for line, col, msg in check_stream(z.read(name)): 78 errors.append([pkg + "/" + name, line, col, msg]) 79 json.dump({"packages": pkgs, "bad_zip": bad_zip, "nstreams": nstreams, "errors": errors}, open(idsjson, "w")) 80 print(f"xml:id / namespace check: {len(errors)} errors in {len({e[0].split('/')[0] for e in errors})} of {len(pkgs)} packages, " 81 f"streams checked: {nstreams}, bad zips: {bad_zip}") 82 83 84main() 85