xref: /trunk/test/odf-validate/idcheck.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
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