xref: /trunk/test/odf-validate/fixtures/t15/check.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
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: runpy.sh fixtures/t15/check.py <port> <outdir> <mode>   (mode ext | strict | v11: the office profile's save version)
23# Task 15 fixes: protection keys (SHA-256 over UTF-8 with the digest attribute), foreign office:meta content in strict mode,
24# drawooo:display in Writer and Calc, a missing number:transliteration-style. Prints PASS/FAIL lines.
25import sys, os, re, zipfile, hashlib, base64, traceback, uno
26import xml.etree.ElementTree as ET
27from unohelp import connect, desktop, pv
28from com.sun.star.awt import Size, Point
29
30port, out, mode = int(sys.argv[1]), os.path.abspath(sys.argv[2]), sys.argv[3]
31os.makedirs(out, exist_ok=True)
32desk = desktop(connect(port))
33P, WRONG = "Pässwörd中1", "Passwort"
34DSIG256 = "http://www.w3.org/2000/09/xmldsig#sha256"
35ENC256 = "http://www.w3.org/2001/04/xmlenc#sha256"
36DSIG1 = "http://www.w3.org/2000/09/xmldsig#sha1"
37DRAWOOO = "http://openoffice.org/2010/draw"
38new_form = mode in ("ext", "strict")
39
40
41def sha1(enc):
42    return hashlib.sha1(P.encode(enc)).digest()
43
44
45H_LE, H_BE, H_U8 = sha1("utf-16-le"), sha1("utf-16-be"), sha1("utf-8")
46H_256 = hashlib.sha256(P.encode("utf-8")).digest()
47b64 = lambda b: base64.b64encode(b).decode()
48
49
50def check(name, ok, detail=""):
51    print(("PASS " + name) if ok else ("FAIL %s %s" % (name, detail)), flush=True)
52
53
54def new(kind):
55    return desk.loadComponentFromURL("private:factory/" + kind, "_blank", 0, (pv("Hidden", True),))
56
57
58def load(path):
59    return desk.loadComponentFromURL(uno.systemPathToFileUrl(path), "_blank", 0, (pv("Hidden", True),))
60
61
62def store(doc, name, flt):
63    p = os.path.join(out, name)
64    doc.storeToURL(uno.systemPathToFileUrl(p), (pv("FilterName", flt),))
65    return p
66
67
68def member(path, name):
69    with zipfile.ZipFile(path) as z:
70        return z.read(name).decode("utf-8")
71
72
73def rewrite(src, dst, edits):
74    """copy a package, applying {member: fn(text) -> text}; mimetype stays first and stored"""
75    with zipfile.ZipFile(src) as zi, zipfile.ZipFile(dst, "w") as zo:
76        for i in zi.infolist():
77            data = zi.read(i.filename)
78            if i.filename in edits:
79                data = edits[i.filename](data.decode("utf-8")).encode("utf-8")
80            zo.writestr(i, data, compress_type=zipfile.ZIP_STORED if i.filename == "mimetype" else zipfile.ZIP_DEFLATED)
81    return dst
82
83
84def attrs(xml, tag, attr):
85    """values of attribute attr (qualified name) on every <tag ...> start tag"""
86    return [m.group(1) for t in re.findall(r"<%s\b[^>]*>" % re.escape(tag), xml)
87            for m in [re.search(r'\b%s="([^"]*)"' % re.escape(attr), t)] if m]
88
89
90def setkey(key, digest):
91    def f(xml):
92        xml = re.sub(r' table:protection-key-digest-algorithm="[^"]*"', "", xml)
93        xml = re.sub(r'(table:protection-key=")[^"]*"', lambda m: m.group(1) + b64(key) + '"' +
94                     (' table:protection-key-digest-algorithm="%s"' % digest if digest else ""), xml)
95        return xml
96    return f
97
98
99def guarded(fn):
100    try:
101        fn()
102    except Exception:
103        check(fn.__name__, False, traceback.format_exc().strip().splitlines()[-1])
104
105
106# ---- Calc protection keys
107def calc_keys():
108    doc = new("scalc")
109    doc.Sheets.getByIndex(0).protect(P)
110    doc.protect(P)
111    base = store(doc, "calc-new.ods", "calc8")
112    doc.close(True)
113    x = member(base, "content.xml")
114    sheet_key = attrs(x, "table:table", "table:protection-key")
115    body_key = attrs(x, "office:spreadsheet", "table:protection-key")
116    sheet_dig = attrs(x, "table:table", "table:protection-key-digest-algorithm")
117    body_dig = attrs(x, "office:spreadsheet", "table:protection-key-digest-algorithm")
118    want = H_256 if new_form else H_LE
119    check("calc-new key", sheet_key == [b64(want)] and body_key == [b64(want)], (sheet_key, body_key))
120    check("calc-new digest attribute", (sheet_dig == [DSIG256] and body_dig == [DSIG256]) if new_form else (not sheet_dig and not body_dig), (sheet_dig, body_dig))
121
122    def verifies(path, label, ok_expected):
123        d = load(path)
124        sh = d.Sheets.getByIndex(0)
125        was = sh.isProtected()
126        try:
127            sh.unprotect(WRONG)
128        except Exception:
129            pass
130        wrong_kept = sh.isProtected()
131        try:
132            sh.unprotect(P)
133        except Exception:
134            pass
135        right_open = not sh.isProtected()
136        d.close(True)
137        check("calc %s" % label, was and wrong_kept and (right_open == ok_expected), (was, wrong_kept, right_open))
138
139    verifies(base, "new file verifies", True)
140    variants = [("legacy sha1 utf16le", H_LE, None, True), ("sha1 utf16be", H_BE, None, True),
141                ("sha1 utf8 no attribute", H_U8, None, True), ("sha1 utf8 xmldsig#sha1", H_U8, DSIG1, True),
142                ("sha256 xmldsig", H_256, DSIG256, True), ("sha256 xmlenc", H_256, ENC256, True),
143                ("sha256 no attribute", H_256, None, True), ("unknown algorithm", os.urandom(20), "urn:x-test:alg", False)]
144    for i, (label, key, dig, ok) in enumerate(variants):
145        p = rewrite(base, os.path.join(out, "calc-var%d.ods" % i), {"content.xml": setkey(key, dig)})
146        verifies(p, label, ok)
147
148    # ext and ODF 1.1 copy an unchanged sheet's stream verbatim (strict re-exports it), so the input attribute survives
149    for label, key, dig, edited in (
150            ("legacy", H_LE, None, False), ("sha256 without attribute", H_256, None, False), ("sha256 xmlenc", H_256, ENC256, False)):
151        src = rewrite(base, os.path.join(out, "calc-resave-src.ods"), {"content.xml": setkey(key, dig)})
152        d = load(src)
153        p = store(d, "calc-resave.ods", "calc8")
154        d.close(True)
155        if key == H_LE:
156            want_dig = None
157        else:
158            want_dig = DSIG256 if mode == "strict" else dig
159        x = member(p, "content.xml")
160        got = attrs(x, "table:table", "table:protection-key")
161        gdig = attrs(x, "table:table", "table:protection-key-digest-algorithm")
162        check("calc re-save %s" % label, got == [b64(key)] and gdig == ([want_dig] if want_dig else []), (got, gdig))
163
164
165    # a password changed after loading must reach the file
166    P2 = "other\u00e9"
167    src = rewrite(base, os.path.join(out, "calc-change-src.ods"), {"content.xml": setkey(H_LE, None)})
168    d = load(src)
169    sh = d.Sheets.getByIndex(0)
170    sh.unprotect(P)
171    sh.protect(P2)
172    p = store(d, "calc-change.ods", "calc8")
173    d.close(True)
174    x = member(p, "content.xml")
175    got = attrs(x, "table:table", "table:protection-key")
176    want = hashlib.sha256(P2.encode("utf-8")).digest() if new_form else hashlib.sha1(P2.encode("utf-16-le")).digest()
177    check("calc changed password is written", got == [b64(want)], got)
178
179
180# ---- Writer section protection keys
181def writer_keys():
182    def make(key, name):
183        doc = new("swriter")
184        sec = doc.createInstance("com.sun.star.text.TextSection")
185        cur = doc.Text.createTextCursor()
186        doc.Text.insertTextContent(cur, sec, False)
187        sec.setPropertyValue("ProtectionKey", uno.ByteSequence(key))
188        sec.setPropertyValue("IsProtected", True)
189        p = store(doc, name, "writer8")
190        doc.close(True)
191        return p
192
193    for label, key, want_dig in (("sha256", H_256, DSIG256 if new_form else None), ("legacy sha1", H_LE, None)):
194        p = make(key, "writer-%s.odt" % label.split()[-1])
195        x = member(p, "content.xml")
196        got = attrs(x, "text:section", "text:protection-key")
197        gdig = attrs(x, "text:section", "text:protection-key-digest-algorithm")
198        check("writer %s key" % label, got == [b64(key)], got)
199        check("writer %s digest attribute" % label, gdig == ([want_dig] if want_dig else []), gdig)
200    base = make(H_256, "writer-base.odt")
201    src = rewrite(base, os.path.join(out, "writer-xmlenc.odt"), {"content.xml": lambda x: re.sub(
202        r'(text:protection-key-digest-algorithm=")[^"]*"', lambda m: m.group(1) + ENC256 + '"', x)})
203    d = load(src)
204    sec = d.TextSections.getByIndex(0)
205    got = bytes(sec.getPropertyValue("ProtectionKey").value) if hasattr(sec.getPropertyValue("ProtectionKey"), "value") else bytes(sec.getPropertyValue("ProtectionKey"))
206    p = store(d, "writer-xmlenc-resave.odt", "writer8")
207    d.close(True)
208    check("writer xmlenc key loaded", got == H_256, got.hex())
209    gdig = attrs(member(p, "content.xml"), "text:section", "text:protection-key-digest-algorithm")
210    check("writer xmlenc re-saved as xmldsig", gdig == ([DSIG256] if new_form else []), gdig)
211
212
213# ---- foreign content in office:meta
214def meta_foreign():
215    doc = new("swriter")
216    doc.Text.insertString(doc.Text.End, "meta", False)
217    base = store(doc, "meta-base.odt", "writer8")
218    doc.close(True)
219
220    def inject(m):
221        m = m.replace("<office:meta>", '<office:meta xmlns:foo="urn:example:foreign" foo:metaattr="m">', 1)
222        # a foreign sibling that rebinds dc: must not change how the following dc: element resolves
223        return m.replace("</office:meta>", '<foo:bar foo:attr="1">x</foo:bar><x:ext xmlns:x="urn:example:x" xmlns:dc="urn:example:other"/><dc:subject>trapsubject</dc:subject></office:meta>', 1)
224    src = rewrite(base, os.path.join(out, "meta-in.odt"), {"meta.xml": inject})
225    check("meta input carries the foreign content", "foo:bar" in member(src, "meta.xml"))
226    d = load(src)
227    p = store(d, "meta-out.odt", "writer8")
228    d.close(True)
229    m = member(p, "meta.xml")
230    ET.fromstring(m)
231    foreign = "urn:example:foreign" in m or "foo:" in m
232    check("meta foreign content %s" % ("dropped in strict" if mode == "strict" else "kept"), foreign == (mode != "strict"), m[:400])
233    check("meta ODF element after a rebinding foreign sibling kept", "trapsubject" in m, m[:600])
234    check("meta ODF content kept", "<meta:generator>" in m or "<meta:creation-date>" in m or "<dc:" in m or "<meta:" in m, m[:200])
235
236
237# ---- drawooo:display in Writer and Calc
238def display_ns():
239    def shapes(doc, page):
240        for i, (vis, prn) in enumerate(((True, False), (False, True), (False, False))):
241            s = doc.createInstance("com.sun.star.drawing.RectangleShape")
242            page.add(s)
243            s.Position = Point(1000, 1000 + i * 2500)
244            s.Size = Size(4000, 2000)
245            s.setPropertyValue("Visible", vis)
246            s.setPropertyValue("Printable", prn)
247    for kind, flt, ext in (("swriter", "writer8", "odt"), ("scalc", "calc8", "ods")):
248        doc = new(kind)
249        page = doc.DrawPage if kind == "swriter" else doc.Sheets.getByIndex(0).DrawPage
250        shapes(doc, page)
251        p = store(doc, "display-%s.%s" % (kind, ext), flt)
252        doc.close(True)
253        root = ET.fromstring(member(p, "content.xml"))
254        pref = bare = 0
255        for e in root.iter():
256            for a in e.attrib:
257                pref += a == "{%s}display" % DRAWOOO
258                bare += a == "display"
259        want = 3 if mode == "ext" else 0
260        check("%s drawooo:display count (%s)" % (kind, mode), pref == want, pref)
261        check("%s no unprefixed display" % kind, bare == 0, bare)
262
263
264# ---- missing number:transliteration-style
265def translit():
266    doc = new("scalc")
267    sh = doc.Sheets.getByIndex(0)
268    nf = doc.NumberFormats
269    loc = uno.createUnoStruct("com.sun.star.lang.Locale")
270    loc.Language, loc.Country = "ar", "SA"
271    key = nf.queryKey("[NatNum1]0", loc, False)
272    if key == -1:
273        key = nf.addNew("[NatNum1]0", loc)
274    c = sh.getCellByPosition(0, 0)
275    c.Value = 123
276    c.NumberFormat = key
277    base = store(doc, "translit-base.ods", "calc8")
278    doc.close(True)
279    digits = "١٢٣"
280    for label, fn in (("with style", lambda x: x), ("without style", lambda x: re.sub(r' number:transliteration-style="[^"]*"', "", x))):
281        src = rewrite(base, os.path.join(out, "translit-%s.ods" % label.replace(" ", "-")), {"content.xml": fn, "styles.xml": fn})
282        d = load(src)
283        s = d.Sheets.getByIndex(0).getCellByPosition(0, 0).String
284        d.close(True)
285        check("transliteration %s shows Arabic-Indic digits" % label, s == digits, s)
286
287
288for fn in (calc_keys, writer_keys, meta_foreign, display_ns, translit):
289    guarded(fn)
290