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