Repository: fonttools/fonttools

Issue:
TTX crashes when compiling recursive glyph component defintion
If the glyf table in a ttx file (nonsensically) declares a glyph to be a component of itself, ttx in Version 3.0 of fonttools recursively calls getCoordinates until the "maximum recursion depth exceeded while calling a Python object".  The tool ttx ought to report which glyph has the problem.  (I encountered this bug in a glyph which is not used to render text.)


Discussion:

Resolving patch:
diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py
--- a/Lib/fontTools/misc/py23.py
+++ b/Lib/fontTools/misc/py23.py
@@ -7,7 +7,7 @@
 __all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO',
 		'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr',
 		'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error',
-		'SimpleNamespace', 'zip']
+		'SimpleNamespace', 'zip', 'RecursionError']
 
 
 class Py23Error(NotImplementedError):
@@ -514,6 +514,12 @@ class redirect_stderr(_RedirectStream):
 		_stream = "stderr"
 
 
+try:
+	RecursionError = RecursionError
+except NameError:
+	RecursionError = RuntimeError
+
+
 if __name__ == "__main__":
 	import doctest, sys
 	sys.exit(doctest.testmod().failed)
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -841,7 +841,10 @@ def getCoordinates(self, glyfTable):
 			allEndPts = []
 			for compo in self.components:
 				g = glyfTable[compo.glyphName]
-				coordinates, endPts, flags = g.getCoordinates(glyfTable)
+				try:
+					coordinates, endPts, flags = g.getCoordinates(glyfTable)
+				except RecursionError:
+					raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName)
 				if hasattr(compo, "firstPt"):
 					# move according to two reference points
 					x1,y1 = allCoords[compo.firstPt]


Test patch:
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -1,7 +1,8 @@
 from __future__ import print_function, division, absolute_import
 from fontTools.misc.py23 import *
 from fontTools.misc.fixedTools import otRound
-from fontTools.ttLib import TTFont, newTable
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+from fontTools.ttLib import TTFont, newTable, TTLibError
 from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
 import sys
 import array
@@ -180,6 +181,13 @@ def strip_ttLibVersion(string):
 
 class glyfTableTest(unittest.TestCase):
 
+    def __init__(self, methodName):
+        unittest.TestCase.__init__(self, methodName)
+        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
+        # and fires deprecation warnings if a program uses the old name.
+        if not hasattr(self, "assertRaisesRegex"):
+            self.assertRaisesRegex = self.assertRaisesRegexp
+
     @classmethod
     def setUpClass(cls):
         with open(GLYF_BIN, 'rb') as f:
@@ -215,6 +223,23 @@ def test_fromXML(self):
         glyfData = glyfTable.compile(font)
         self.assertEqual(glyfData, self.glyfData)
 
+    def test_recursiveComponent(self):
+        glyphSet = {}
+        pen_dummy = TTGlyphPen(glyphSet)
+        glyph_dummy = pen_dummy.glyph()
+        glyphSet["A"] = glyph_dummy
+        glyphSet["B"] = glyph_dummy
+        pen_A = TTGlyphPen(glyphSet)
+        pen_A.addComponent("B", (1, 0, 0, 1, 0, 0))
+        pen_B = TTGlyphPen(glyphSet)
+        pen_B.addComponent("A", (1, 0, 0, 1, 0, 0))
+        glyph_A = pen_A.glyph()
+        glyph_B = pen_B.glyph()
+        glyphSet["A"] = glyph_A
+        glyphSet["B"] = glyph_B
+        with self.assertRaisesRegex(TTLibError, "glyph '.' contains a recursive component reference"):
+            glyph_A.getCoordinates(glyphSet)
+
 
 if __name__ == "__main__":
     import sys