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