Merge pull request #3029 from daltonmaag/wrap-glif-xml-errors
[glifLib] Wrap XML library exceptions with glifLib types when parsing glifs
This commit is contained in:
commit
5d0432a813
@ -1,3 +1,6 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
class UFOLibError(Exception):
|
class UFOLibError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -7,7 +10,12 @@ class UnsupportedUFOFormat(UFOLibError):
|
|||||||
|
|
||||||
|
|
||||||
class GlifLibError(UFOLibError):
|
class GlifLibError(UFOLibError):
|
||||||
pass
|
def _add_note(self, note: str) -> None:
|
||||||
|
# Loose backport of PEP 678 until we only support Python 3.11+, used for
|
||||||
|
# adding additional context to errors.
|
||||||
|
# TODO: Replace with https://docs.python.org/3.11/library/exceptions.html#BaseException.add_note
|
||||||
|
(message, *rest) = self.args
|
||||||
|
self.args = ((message + "\n" + note), *rest)
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedGLIFFormat(GlifLibError):
|
class UnsupportedGLIFFormat(GlifLibError):
|
||||||
|
@ -416,17 +416,33 @@ class GlyphSet(_UFOBaseIO):
|
|||||||
if validate is None:
|
if validate is None:
|
||||||
validate = self._validateRead
|
validate = self._validateRead
|
||||||
text = self.getGLIF(glyphName)
|
text = self.getGLIF(glyphName)
|
||||||
tree = _glifTreeFromString(text)
|
try:
|
||||||
formatVersions = GLIFFormatVersion.supported_versions(
|
tree = _glifTreeFromString(text)
|
||||||
self.ufoFormatVersionTuple
|
formatVersions = GLIFFormatVersion.supported_versions(
|
||||||
)
|
self.ufoFormatVersionTuple
|
||||||
_readGlyphFromTree(
|
)
|
||||||
tree,
|
_readGlyphFromTree(
|
||||||
glyphObject,
|
tree,
|
||||||
pointPen,
|
glyphObject,
|
||||||
formatVersions=formatVersions,
|
pointPen,
|
||||||
validate=validate,
|
formatVersions=formatVersions,
|
||||||
)
|
validate=validate,
|
||||||
|
)
|
||||||
|
except GlifLibError as glifLibError:
|
||||||
|
# Re-raise with a note that gives extra context, describing where
|
||||||
|
# the error occurred.
|
||||||
|
fileName = self.contents[glyphName]
|
||||||
|
try:
|
||||||
|
glifLocation = f"'{self.fs.getsyspath(fileName)}'"
|
||||||
|
except fs.errors.NoSysPath:
|
||||||
|
# Network or in-memory FS may not map to a local path, so use
|
||||||
|
# the best string representation we have.
|
||||||
|
glifLocation = f"'{fileName}' from '{str(self.fs)}'"
|
||||||
|
|
||||||
|
glifLibError._add_note(
|
||||||
|
f"The issue is in glyph '{glyphName}', located in {glifLocation}."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
def writeGlyph(
|
def writeGlyph(
|
||||||
self,
|
self,
|
||||||
@ -1082,10 +1098,14 @@ def _glifTreeFromFile(aFile):
|
|||||||
|
|
||||||
def _glifTreeFromString(aString):
|
def _glifTreeFromString(aString):
|
||||||
data = tobytes(aString, encoding="utf-8")
|
data = tobytes(aString, encoding="utf-8")
|
||||||
if etree._have_lxml:
|
try:
|
||||||
root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True))
|
if etree._have_lxml:
|
||||||
else:
|
root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True))
|
||||||
root = etree.fromstring(data)
|
else:
|
||||||
|
root = etree.fromstring(data)
|
||||||
|
except Exception as etree_exception:
|
||||||
|
raise GlifLibError("GLIF contains invalid XML.") from etree_exception
|
||||||
|
|
||||||
if root.tag != "glyph":
|
if root.tag != "glyph":
|
||||||
raise GlifLibError("The GLIF is not properly formatted.")
|
raise GlifLibError("The GLIF is not properly formatted.")
|
||||||
if root.text and root.text.strip() != "":
|
if root.text and root.text.strip() != "":
|
||||||
|
@ -3,6 +3,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
from io import open
|
from io import open
|
||||||
from .testSupport import getDemoFontGlyphSetPath
|
from .testSupport import getDemoFontGlyphSetPath
|
||||||
from fontTools.ufoLib.glifLib import (
|
from fontTools.ufoLib.glifLib import (
|
||||||
@ -134,6 +135,34 @@ class GlyphSetTests(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
self.assertEqual(g.unicodes, unicodes[glyphName])
|
self.assertEqual(g.unicodes, unicodes[glyphName])
|
||||||
|
|
||||||
|
def testReadGlyphInvalidXml(self):
|
||||||
|
"""Test that calling readGlyph() to read a .glif with invalid XML raises
|
||||||
|
a library error, instead of an exception from the XML dependency that is
|
||||||
|
used internally. In addition, check that the raised exception describes
|
||||||
|
the glyph by name and gives the location of the broken .glif file."""
|
||||||
|
|
||||||
|
# Create a glyph set with three empty glyphs.
|
||||||
|
glyph_set = GlyphSet(self.dstDir)
|
||||||
|
glyph_set.writeGlyph("a", _Glyph())
|
||||||
|
glyph_set.writeGlyph("b", _Glyph())
|
||||||
|
glyph_set.writeGlyph("c", _Glyph())
|
||||||
|
|
||||||
|
# Corrupt the XML of /c.
|
||||||
|
invalid_xml = b"<abc></def>"
|
||||||
|
Path(self.dstDir, glyph_set.contents["c"]).write_bytes(invalid_xml)
|
||||||
|
|
||||||
|
# Confirm that reading /a and /b is fine...
|
||||||
|
glyph_set.readGlyph("a", _Glyph())
|
||||||
|
glyph_set.readGlyph("b", _Glyph())
|
||||||
|
|
||||||
|
# ...but that reading /c raises a descriptive library error.
|
||||||
|
expected_message = (
|
||||||
|
r"GLIF contains invalid XML\.\n"
|
||||||
|
r"The issue is in glyph 'c', located in '.*c\.glif.*\."
|
||||||
|
)
|
||||||
|
with pytest.raises(GlifLibError, match=expected_message):
|
||||||
|
glyph_set.readGlyph("c", _Glyph())
|
||||||
|
|
||||||
|
|
||||||
class FileNameTest:
|
class FileNameTest:
|
||||||
def test_default_file_name_scheme(self):
|
def test_default_file_name_scheme(self):
|
||||||
@ -228,6 +257,17 @@ class ReadWriteFuncTest:
|
|||||||
assert g.width == 1290
|
assert g.width == 1290
|
||||||
assert g.unicodes == [0x0041]
|
assert g.unicodes == [0x0041]
|
||||||
|
|
||||||
|
def test_read_invalid_xml(self):
|
||||||
|
"""Test that calling readGlyphFromString() with invalid XML raises a
|
||||||
|
library error, instead of an exception from the XML dependency that is
|
||||||
|
used internally."""
|
||||||
|
|
||||||
|
invalid_xml = b"<abc></def>"
|
||||||
|
empty_glyph = _Glyph()
|
||||||
|
|
||||||
|
with pytest.raises(GlifLibError, match="GLIF contains invalid XML"):
|
||||||
|
readGlyphFromString(invalid_xml, empty_glyph)
|
||||||
|
|
||||||
def test_read_unsupported_format_version(self, caplog):
|
def test_read_unsupported_format_version(self, caplog):
|
||||||
s = """<?xml version='1.0' encoding='utf-8'?>
|
s = """<?xml version='1.0' encoding='utf-8'?>
|
||||||
<glyph name="A" format="0" formatMinor="0">
|
<glyph name="A" format="0" formatMinor="0">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user