• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Various low level data validators."""
2
3from __future__ import absolute_import, unicode_literals
4import calendar
5from io import open
6import fs.base
7import fs.osfs
8
9try:
10    from collections.abc import Mapping  # python >= 3.3
11except ImportError:
12    from collections import Mapping
13
14from fontTools.misc.py23 import basestring
15from fontTools.ufoLib.utils import integerTypes, numberTypes
16
17
18# -------
19# Generic
20# -------
21
22def isDictEnough(value):
23    """
24    Some objects will likely come in that aren't
25    dicts but are dict-ish enough.
26    """
27    if isinstance(value, Mapping):
28        return True
29    for attr in ("keys", "values", "items"):
30        if not hasattr(value, attr):
31            return False
32    return True
33
34def genericTypeValidator(value, typ):
35	"""
36	Generic. (Added at version 2.)
37	"""
38	return isinstance(value, typ)
39
40def genericIntListValidator(values, validValues):
41	"""
42	Generic. (Added at version 2.)
43	"""
44	if not isinstance(values, (list, tuple)):
45		return False
46	valuesSet = set(values)
47	validValuesSet = set(validValues)
48	if valuesSet - validValuesSet:
49		return False
50	for value in values:
51		if not isinstance(value, integerTypes):
52			return False
53	return True
54
55def genericNonNegativeIntValidator(value):
56	"""
57	Generic. (Added at version 3.)
58	"""
59	if not isinstance(value, integerTypes):
60		return False
61	if value < 0:
62		return False
63	return True
64
65def genericNonNegativeNumberValidator(value):
66	"""
67	Generic. (Added at version 3.)
68	"""
69	if not isinstance(value, numberTypes):
70		return False
71	if value < 0:
72		return False
73	return True
74
75def genericDictValidator(value, prototype):
76	"""
77	Generic. (Added at version 3.)
78	"""
79	# not a dict
80	if not isinstance(value, Mapping):
81		return False
82	# missing required keys
83	for key, (typ, required) in prototype.items():
84		if not required:
85			continue
86		if key not in value:
87			return False
88	# unknown keys
89	for key in value.keys():
90		if key not in prototype:
91			return False
92	# incorrect types
93	for key, v in value.items():
94		prototypeType, required = prototype[key]
95		if v is None and not required:
96			continue
97		if not isinstance(v, prototypeType):
98			return False
99	return True
100
101# --------------
102# fontinfo.plist
103# --------------
104
105# Data Validators
106
107def fontInfoStyleMapStyleNameValidator(value):
108	"""
109	Version 2+.
110	"""
111	options = ["regular", "italic", "bold", "bold italic"]
112	return value in options
113
114def fontInfoOpenTypeGaspRangeRecordsValidator(value):
115	"""
116	Version 3+.
117	"""
118	if not isinstance(value, list):
119		return False
120	if len(value) == 0:
121		return True
122	validBehaviors = [0, 1, 2, 3]
123	dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True))
124	ppemOrder = []
125	for rangeRecord in value:
126		if not genericDictValidator(rangeRecord, dictPrototype):
127			return False
128		ppem = rangeRecord["rangeMaxPPEM"]
129		behavior = rangeRecord["rangeGaspBehavior"]
130		ppemValidity = genericNonNegativeIntValidator(ppem)
131		if not ppemValidity:
132			return False
133		behaviorValidity = genericIntListValidator(behavior, validBehaviors)
134		if not behaviorValidity:
135			return False
136		ppemOrder.append(ppem)
137	if ppemOrder != sorted(ppemOrder):
138		return False
139	return True
140
141def fontInfoOpenTypeHeadCreatedValidator(value):
142	"""
143	Version 2+.
144	"""
145	# format: 0000/00/00 00:00:00
146	if not isinstance(value, basestring):
147		return False
148	# basic formatting
149	if not len(value) == 19:
150		return False
151	if value.count(" ") != 1:
152		return False
153	date, time = value.split(" ")
154	if date.count("/") != 2:
155		return False
156	if time.count(":") != 2:
157		return False
158	# date
159	year, month, day = date.split("/")
160	if len(year) != 4:
161		return False
162	if len(month) != 2:
163		return False
164	if len(day) != 2:
165		return False
166	try:
167		year = int(year)
168		month = int(month)
169		day = int(day)
170	except ValueError:
171		return False
172	if month < 1 or month > 12:
173		return False
174	monthMaxDay = calendar.monthrange(year, month)[1]
175	if day < 1 or day > monthMaxDay:
176		return False
177	# time
178	hour, minute, second = time.split(":")
179	if len(hour) != 2:
180		return False
181	if len(minute) != 2:
182		return False
183	if len(second) != 2:
184		return False
185	try:
186		hour = int(hour)
187		minute = int(minute)
188		second = int(second)
189	except ValueError:
190		return False
191	if hour < 0 or hour > 23:
192		return False
193	if minute < 0 or minute > 59:
194		return False
195	if second < 0 or second > 59:
196		return False
197	# fallback
198	return True
199
200def fontInfoOpenTypeNameRecordsValidator(value):
201	"""
202	Version 3+.
203	"""
204	if not isinstance(value, list):
205		return False
206	dictPrototype = dict(nameID=(int, True), platformID=(int, True), encodingID=(int, True), languageID=(int, True), string=(basestring, True))
207	for nameRecord in value:
208		if not genericDictValidator(nameRecord, dictPrototype):
209			return False
210	return True
211
212def fontInfoOpenTypeOS2WeightClassValidator(value):
213	"""
214	Version 2+.
215	"""
216	if not isinstance(value, integerTypes):
217		return False
218	if value < 0:
219		return False
220	return True
221
222def fontInfoOpenTypeOS2WidthClassValidator(value):
223	"""
224	Version 2+.
225	"""
226	if not isinstance(value, integerTypes):
227		return False
228	if value < 1:
229		return False
230	if value > 9:
231		return False
232	return True
233
234def fontInfoVersion2OpenTypeOS2PanoseValidator(values):
235	"""
236	Version 2.
237	"""
238	if not isinstance(values, (list, tuple)):
239		return False
240	if len(values) != 10:
241		return False
242	for value in values:
243		if not isinstance(value, integerTypes):
244			return False
245	# XXX further validation?
246	return True
247
248def fontInfoVersion3OpenTypeOS2PanoseValidator(values):
249	"""
250	Version 3+.
251	"""
252	if not isinstance(values, (list, tuple)):
253		return False
254	if len(values) != 10:
255		return False
256	for value in values:
257		if not isinstance(value, integerTypes):
258			return False
259		if value < 0:
260			return False
261	# XXX further validation?
262	return True
263
264def fontInfoOpenTypeOS2FamilyClassValidator(values):
265	"""
266	Version 2+.
267	"""
268	if not isinstance(values, (list, tuple)):
269		return False
270	if len(values) != 2:
271		return False
272	for value in values:
273		if not isinstance(value, integerTypes):
274			return False
275	classID, subclassID = values
276	if classID < 0 or classID > 14:
277		return False
278	if subclassID < 0 or subclassID > 15:
279		return False
280	return True
281
282def fontInfoPostscriptBluesValidator(values):
283	"""
284	Version 2+.
285	"""
286	if not isinstance(values, (list, tuple)):
287		return False
288	if len(values) > 14:
289		return False
290	if len(values) % 2:
291		return False
292	for value in values:
293		if not isinstance(value, numberTypes):
294			return False
295	return True
296
297def fontInfoPostscriptOtherBluesValidator(values):
298	"""
299	Version 2+.
300	"""
301	if not isinstance(values, (list, tuple)):
302		return False
303	if len(values) > 10:
304		return False
305	if len(values) % 2:
306		return False
307	for value in values:
308		if not isinstance(value, numberTypes):
309			return False
310	return True
311
312def fontInfoPostscriptStemsValidator(values):
313	"""
314	Version 2+.
315	"""
316	if not isinstance(values, (list, tuple)):
317		return False
318	if len(values) > 12:
319		return False
320	for value in values:
321		if not isinstance(value, numberTypes):
322			return False
323	return True
324
325def fontInfoPostscriptWindowsCharacterSetValidator(value):
326	"""
327	Version 2+.
328	"""
329	validValues = list(range(1, 21))
330	if value not in validValues:
331		return False
332	return True
333
334def fontInfoWOFFMetadataUniqueIDValidator(value):
335	"""
336	Version 3+.
337	"""
338	dictPrototype = dict(id=(basestring, True))
339	if not genericDictValidator(value, dictPrototype):
340		return False
341	return True
342
343def fontInfoWOFFMetadataVendorValidator(value):
344	"""
345	Version 3+.
346	"""
347	dictPrototype = {"name" : (basestring, True), "url" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
348	if not genericDictValidator(value, dictPrototype):
349		return False
350	if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
351		return False
352	return True
353
354def fontInfoWOFFMetadataCreditsValidator(value):
355	"""
356	Version 3+.
357	"""
358	dictPrototype = dict(credits=(list, True))
359	if not genericDictValidator(value, dictPrototype):
360		return False
361	if not len(value["credits"]):
362		return False
363	dictPrototype = {"name" : (basestring, True), "url" : (basestring, False), "role" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
364	for credit in value["credits"]:
365		if not genericDictValidator(credit, dictPrototype):
366			return False
367		if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
368			return False
369	return True
370
371def fontInfoWOFFMetadataDescriptionValidator(value):
372	"""
373	Version 3+.
374	"""
375	dictPrototype = dict(url=(basestring, False), text=(list, True))
376	if not genericDictValidator(value, dictPrototype):
377		return False
378	for text in value["text"]:
379		if not fontInfoWOFFMetadataTextValue(text):
380			return False
381	return True
382
383def fontInfoWOFFMetadataLicenseValidator(value):
384	"""
385	Version 3+.
386	"""
387	dictPrototype = dict(url=(basestring, False), text=(list, False), id=(basestring, False))
388	if not genericDictValidator(value, dictPrototype):
389		return False
390	if "text" in value:
391		for text in value["text"]:
392			if not fontInfoWOFFMetadataTextValue(text):
393				return False
394	return True
395
396def fontInfoWOFFMetadataTrademarkValidator(value):
397	"""
398	Version 3+.
399	"""
400	dictPrototype = dict(text=(list, True))
401	if not genericDictValidator(value, dictPrototype):
402		return False
403	for text in value["text"]:
404		if not fontInfoWOFFMetadataTextValue(text):
405			return False
406	return True
407
408def fontInfoWOFFMetadataCopyrightValidator(value):
409	"""
410	Version 3+.
411	"""
412	dictPrototype = dict(text=(list, True))
413	if not genericDictValidator(value, dictPrototype):
414		return False
415	for text in value["text"]:
416		if not fontInfoWOFFMetadataTextValue(text):
417			return False
418	return True
419
420def fontInfoWOFFMetadataLicenseeValidator(value):
421	"""
422	Version 3+.
423	"""
424	dictPrototype = {"name" : (basestring, True), "dir" : (basestring, False), "class" : (basestring, False)}
425	if not genericDictValidator(value, dictPrototype):
426		return False
427	if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
428		return False
429	return True
430
431def fontInfoWOFFMetadataTextValue(value):
432	"""
433	Version 3+.
434	"""
435	dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
436	if not genericDictValidator(value, dictPrototype):
437		return False
438	if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
439		return False
440	return True
441
442def fontInfoWOFFMetadataExtensionsValidator(value):
443	"""
444	Version 3+.
445	"""
446	if not isinstance(value, list):
447		return False
448	if not value:
449		return False
450	for extension in value:
451		if not fontInfoWOFFMetadataExtensionValidator(extension):
452			return False
453	return True
454
455def fontInfoWOFFMetadataExtensionValidator(value):
456	"""
457	Version 3+.
458	"""
459	dictPrototype = dict(names=(list, False), items=(list, True), id=(basestring, False))
460	if not genericDictValidator(value, dictPrototype):
461		return False
462	if "names" in value:
463		for name in value["names"]:
464			if not fontInfoWOFFMetadataExtensionNameValidator(name):
465				return False
466	for item in value["items"]:
467		if not fontInfoWOFFMetadataExtensionItemValidator(item):
468			return False
469	return True
470
471def fontInfoWOFFMetadataExtensionItemValidator(value):
472	"""
473	Version 3+.
474	"""
475	dictPrototype = dict(id=(basestring, False), names=(list, True), values=(list, True))
476	if not genericDictValidator(value, dictPrototype):
477		return False
478	for name in value["names"]:
479		if not fontInfoWOFFMetadataExtensionNameValidator(name):
480			return False
481	for val in value["values"]:
482		if not fontInfoWOFFMetadataExtensionValueValidator(val):
483			return False
484	return True
485
486def fontInfoWOFFMetadataExtensionNameValidator(value):
487	"""
488	Version 3+.
489	"""
490	dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
491	if not genericDictValidator(value, dictPrototype):
492		return False
493	if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
494		return False
495	return True
496
497def fontInfoWOFFMetadataExtensionValueValidator(value):
498	"""
499	Version 3+.
500	"""
501	dictPrototype = {"text" : (basestring, True), "language" : (basestring, False), "dir" : (basestring, False), "class" : (basestring, False)}
502	if not genericDictValidator(value, dictPrototype):
503		return False
504	if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
505		return False
506	return True
507
508# ----------
509# Guidelines
510# ----------
511
512def guidelinesValidator(value, identifiers=None):
513	"""
514	Version 3+.
515	"""
516	if not isinstance(value, list):
517		return False
518	if identifiers is None:
519		identifiers = set()
520	for guide in value:
521		if not guidelineValidator(guide):
522			return False
523		identifier = guide.get("identifier")
524		if identifier is not None:
525			if identifier in identifiers:
526				return False
527			identifiers.add(identifier)
528	return True
529
530_guidelineDictPrototype = dict(
531	x=((int, float), False), y=((int, float), False), angle=((int, float), False),
532	name=(basestring, False), color=(basestring, False), identifier=(basestring, False)
533)
534
535def guidelineValidator(value):
536	"""
537	Version 3+.
538	"""
539	if not genericDictValidator(value, _guidelineDictPrototype):
540		return False
541	x = value.get("x")
542	y = value.get("y")
543	angle = value.get("angle")
544	# x or y must be present
545	if x is None and y is None:
546		return False
547	# if x or y are None, angle must not be present
548	if x is None or y is None:
549		if angle is not None:
550			return False
551	# if x and y are defined, angle must be defined
552	if x is not None and y is not None and angle is None:
553		return False
554	# angle must be between 0 and 360
555	if angle is not None:
556		if angle < 0:
557			return False
558		if angle > 360:
559			return False
560	# identifier must be 1 or more characters
561	identifier = value.get("identifier")
562	if identifier is not None and not identifierValidator(identifier):
563		return False
564	# color must follow the proper format
565	color = value.get("color")
566	if color is not None and not colorValidator(color):
567		return False
568	return True
569
570# -------
571# Anchors
572# -------
573
574def anchorsValidator(value, identifiers=None):
575	"""
576	Version 3+.
577	"""
578	if not isinstance(value, list):
579		return False
580	if identifiers is None:
581		identifiers = set()
582	for anchor in value:
583		if not anchorValidator(anchor):
584			return False
585		identifier = anchor.get("identifier")
586		if identifier is not None:
587			if identifier in identifiers:
588				return False
589			identifiers.add(identifier)
590	return True
591
592_anchorDictPrototype = dict(
593	x=((int, float), False), y=((int, float), False),
594	name=(basestring, False), color=(basestring, False),
595	identifier=(basestring, False)
596)
597
598def anchorValidator(value):
599	"""
600	Version 3+.
601	"""
602	if not genericDictValidator(value, _anchorDictPrototype):
603		return False
604	x = value.get("x")
605	y = value.get("y")
606	# x and y must be present
607	if x is None or y is None:
608		return False
609	# identifier must be 1 or more characters
610	identifier = value.get("identifier")
611	if identifier is not None and not identifierValidator(identifier):
612		return False
613	# color must follow the proper format
614	color = value.get("color")
615	if color is not None and not colorValidator(color):
616		return False
617	return True
618
619# ----------
620# Identifier
621# ----------
622
623def identifierValidator(value):
624	"""
625	Version 3+.
626
627	>>> identifierValidator("a")
628	True
629	>>> identifierValidator("")
630	False
631	>>> identifierValidator("a" * 101)
632	False
633	"""
634	validCharactersMin = 0x20
635	validCharactersMax = 0x7E
636	if not isinstance(value, basestring):
637		return False
638	if not value:
639		return False
640	if len(value) > 100:
641		return False
642	for c in value:
643		c = ord(c)
644		if c < validCharactersMin or c > validCharactersMax:
645			return False
646	return True
647
648# -----
649# Color
650# -----
651
652def colorValidator(value):
653	"""
654	Version 3+.
655
656	>>> colorValidator("0,0,0,0")
657	True
658	>>> colorValidator(".5,.5,.5,.5")
659	True
660	>>> colorValidator("0.5,0.5,0.5,0.5")
661	True
662	>>> colorValidator("1,1,1,1")
663	True
664
665	>>> colorValidator("2,0,0,0")
666	False
667	>>> colorValidator("0,2,0,0")
668	False
669	>>> colorValidator("0,0,2,0")
670	False
671	>>> colorValidator("0,0,0,2")
672	False
673
674	>>> colorValidator("1r,1,1,1")
675	False
676	>>> colorValidator("1,1g,1,1")
677	False
678	>>> colorValidator("1,1,1b,1")
679	False
680	>>> colorValidator("1,1,1,1a")
681	False
682
683	>>> colorValidator("1 1 1 1")
684	False
685	>>> colorValidator("1 1,1,1")
686	False
687	>>> colorValidator("1,1 1,1")
688	False
689	>>> colorValidator("1,1,1 1")
690	False
691
692	>>> colorValidator("1, 1, 1, 1")
693	True
694	"""
695	if not isinstance(value, basestring):
696		return False
697	parts = value.split(",")
698	if len(parts) != 4:
699		return False
700	for part in parts:
701		part = part.strip()
702		converted = False
703		try:
704			part = int(part)
705			converted = True
706		except ValueError:
707			pass
708		if not converted:
709			try:
710				part = float(part)
711				converted = True
712			except ValueError:
713				pass
714		if not converted:
715			return False
716		if part < 0:
717			return False
718		if part > 1:
719			return False
720	return True
721
722# -----
723# image
724# -----
725
726pngSignature = b"\x89PNG\r\n\x1a\n"
727
728_imageDictPrototype = dict(
729	fileName=(basestring, True),
730	xScale=((int, float), False), xyScale=((int, float), False),
731	yxScale=((int, float), False), yScale=((int, float), False),
732	xOffset=((int, float), False), yOffset=((int, float), False),
733	color=(basestring, False)
734)
735
736def imageValidator(value):
737	"""
738	Version 3+.
739	"""
740	if not genericDictValidator(value, _imageDictPrototype):
741		return False
742	# fileName must be one or more characters
743	if not value["fileName"]:
744		return False
745	# color must follow the proper format
746	color = value.get("color")
747	if color is not None and not colorValidator(color):
748		return False
749	return True
750
751def pngValidator(path=None, data=None, fileObj=None):
752	"""
753	Version 3+.
754
755	This checks the signature of the image data.
756	"""
757	assert path is not None or data is not None or fileObj is not None
758	if path is not None:
759		with open(path, "rb") as f:
760			signature = f.read(8)
761	elif data is not None:
762		signature = data[:8]
763	elif fileObj is not None:
764		pos = fileObj.tell()
765		signature = fileObj.read(8)
766		fileObj.seek(pos)
767	if signature != pngSignature:
768		return False, "Image does not begin with the PNG signature."
769	return True, None
770
771# -------------------
772# layercontents.plist
773# -------------------
774
775def layerContentsValidator(value, ufoPathOrFileSystem):
776	"""
777	Check the validity of layercontents.plist.
778	Version 3+.
779	"""
780	if isinstance(ufoPathOrFileSystem, fs.base.FS):
781		fileSystem = ufoPathOrFileSystem
782	else:
783		fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)
784
785	bogusFileMessage = "layercontents.plist in not in the correct format."
786	# file isn't in the right format
787	if not isinstance(value, list):
788		return False, bogusFileMessage
789	# work through each entry
790	usedLayerNames = set()
791	usedDirectories = set()
792	contents = {}
793	for entry in value:
794		# layer entry in the incorrect format
795		if not isinstance(entry, list):
796			return False, bogusFileMessage
797		if not len(entry) == 2:
798			return False, bogusFileMessage
799		for i in entry:
800			if not isinstance(i, basestring):
801				return False, bogusFileMessage
802		layerName, directoryName = entry
803		# check directory naming
804		if directoryName != "glyphs":
805			if not directoryName.startswith("glyphs."):
806				return False, "Invalid directory name (%s) in layercontents.plist." % directoryName
807		if len(layerName) == 0:
808			return False, "Empty layer name in layercontents.plist."
809		# directory doesn't exist
810		if not fileSystem.exists(directoryName):
811			return False, "A glyphset does not exist at %s." % directoryName
812		# default layer name
813		if layerName == "public.default" and directoryName != "glyphs":
814			return False, "The name public.default is being used by a layer that is not the default."
815		# check usage
816		if layerName in usedLayerNames:
817			return False, "The layer name %s is used by more than one layer." % layerName
818		usedLayerNames.add(layerName)
819		if directoryName in usedDirectories:
820			return False, "The directory %s is used by more than one layer." % directoryName
821		usedDirectories.add(directoryName)
822		# store
823		contents[layerName] = directoryName
824	# missing default layer
825	foundDefault = "glyphs" in contents.values()
826	if not foundDefault:
827		return False, "The required default glyph set is not in the UFO."
828	return True, None
829
830# ------------
831# groups.plist
832# ------------
833
834def groupsValidator(value):
835	"""
836	Check the validity of the groups.
837	Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
838
839	>>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
840	>>> groupsValidator(groups)
841	(True, None)
842
843	>>> groups = {"" : ["A"]}
844	>>> valid, msg = groupsValidator(groups)
845	>>> valid
846	False
847	>>> print(msg)
848	A group has an empty name.
849
850	>>> groups = {"public.awesome" : ["A"]}
851	>>> groupsValidator(groups)
852	(True, None)
853
854	>>> groups = {"public.kern1." : ["A"]}
855	>>> valid, msg = groupsValidator(groups)
856	>>> valid
857	False
858	>>> print(msg)
859	The group data contains a kerning group with an incomplete name.
860	>>> groups = {"public.kern2." : ["A"]}
861	>>> valid, msg = groupsValidator(groups)
862	>>> valid
863	False
864	>>> print(msg)
865	The group data contains a kerning group with an incomplete name.
866
867	>>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
868	>>> groupsValidator(groups)
869	(True, None)
870
871	>>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
872	>>> valid, msg = groupsValidator(groups)
873	>>> valid
874	False
875	>>> print(msg)
876	The glyph "A" occurs in too many kerning groups.
877	"""
878	bogusFormatMessage = "The group data is not in the correct format."
879	if not isDictEnough(value):
880		return False, bogusFormatMessage
881	firstSideMapping = {}
882	secondSideMapping = {}
883	for groupName, glyphList in value.items():
884		if not isinstance(groupName, (basestring)):
885			return False, bogusFormatMessage
886		if not isinstance(glyphList, (list, tuple)):
887			return False, bogusFormatMessage
888		if not groupName:
889			return False, "A group has an empty name."
890		if groupName.startswith("public."):
891			if not groupName.startswith("public.kern1.") and not groupName.startswith("public.kern2."):
892				# unknown pubic.* name. silently skip.
893				continue
894			else:
895				if len("public.kernN.") == len(groupName):
896					return False, "The group data contains a kerning group with an incomplete name."
897			if groupName.startswith("public.kern1."):
898				d = firstSideMapping
899			else:
900				d = secondSideMapping
901			for glyphName in glyphList:
902				if not isinstance(glyphName, basestring):
903					return False, "The group data %s contains an invalid member." % groupName
904				if glyphName in d:
905					return False, "The glyph \"%s\" occurs in too many kerning groups." % glyphName
906				d[glyphName] = groupName
907	return True, None
908
909# -------------
910# kerning.plist
911# -------------
912
913def kerningValidator(data):
914	"""
915	Check the validity of the kerning data structure.
916	Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
917
918	>>> kerning = {"A" : {"B" : 100}}
919	>>> kerningValidator(kerning)
920	(True, None)
921
922	>>> kerning = {"A" : ["B"]}
923	>>> valid, msg = kerningValidator(kerning)
924	>>> valid
925	False
926	>>> print(msg)
927	The kerning data is not in the correct format.
928
929	>>> kerning = {"A" : {"B" : "100"}}
930	>>> valid, msg = kerningValidator(kerning)
931	>>> valid
932	False
933	>>> print(msg)
934	The kerning data is not in the correct format.
935	"""
936	bogusFormatMessage = "The kerning data is not in the correct format."
937	if not isinstance(data, Mapping):
938		return False, bogusFormatMessage
939	for first, secondDict in data.items():
940		if not isinstance(first, basestring):
941			return False, bogusFormatMessage
942		elif not isinstance(secondDict, Mapping):
943			return False, bogusFormatMessage
944		for second, value in secondDict.items():
945			if not isinstance(second, basestring):
946				return False, bogusFormatMessage
947			elif not isinstance(value, numberTypes):
948				return False, bogusFormatMessage
949	return True, None
950
951# -------------
952# lib.plist/lib
953# -------------
954
955_bogusLibFormatMessage = "The lib data is not in the correct format: %s"
956
957def fontLibValidator(value):
958	"""
959	Check the validity of the lib.
960	Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
961
962	>>> lib = {"foo" : "bar"}
963	>>> fontLibValidator(lib)
964	(True, None)
965
966	>>> lib = {"public.awesome" : "hello"}
967	>>> fontLibValidator(lib)
968	(True, None)
969
970	>>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
971	>>> fontLibValidator(lib)
972	(True, None)
973
974	>>> lib = "hello"
975	>>> valid, msg = fontLibValidator(lib)
976	>>> valid
977	False
978	>>> print(msg)  # doctest: +ELLIPSIS
979	The lib data is not in the correct format: expected a dictionary, ...
980
981	>>> lib = {1: "hello"}
982	>>> valid, msg = fontLibValidator(lib)
983	>>> valid
984	False
985	>>> print(msg)
986	The lib key is not properly formatted: expected basestring, found int: 1
987
988	>>> lib = {"public.glyphOrder" : "hello"}
989	>>> valid, msg = fontLibValidator(lib)
990	>>> valid
991	False
992	>>> print(msg)  # doctest: +ELLIPSIS
993	public.glyphOrder is not properly formatted: expected list or tuple,...
994
995	>>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
996	>>> valid, msg = fontLibValidator(lib)
997	>>> valid
998	False
999	>>> print(msg)  # doctest: +ELLIPSIS
1000	public.glyphOrder is not properly formatted: expected basestring,...
1001	"""
1002	if not isDictEnough(value):
1003		reason = "expected a dictionary, found %s" % type(value).__name__
1004		return False, _bogusLibFormatMessage % reason
1005	for key, value in value.items():
1006		if not isinstance(key, basestring):
1007			return False, (
1008				"The lib key is not properly formatted: expected basestring, found %s: %r" %
1009				(type(key).__name__, key))
1010		# public.glyphOrder
1011		if key == "public.glyphOrder":
1012			bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
1013			if not isinstance(value, (list, tuple)):
1014				reason = "expected list or tuple, found %s" % type(value).__name__
1015				return False, bogusGlyphOrderMessage % reason
1016			for glyphName in value:
1017				if not isinstance(glyphName, basestring):
1018					reason = "expected basestring, found %s" % type(glyphName).__name__
1019					return False, bogusGlyphOrderMessage % reason
1020	return True, None
1021
1022# --------
1023# GLIF lib
1024# --------
1025
1026def glyphLibValidator(value):
1027	"""
1028	Check the validity of the lib.
1029	Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
1030
1031	>>> lib = {"foo" : "bar"}
1032	>>> glyphLibValidator(lib)
1033	(True, None)
1034
1035	>>> lib = {"public.awesome" : "hello"}
1036	>>> glyphLibValidator(lib)
1037	(True, None)
1038
1039	>>> lib = {"public.markColor" : "1,0,0,0.5"}
1040	>>> glyphLibValidator(lib)
1041	(True, None)
1042
1043	>>> lib = {"public.markColor" : 1}
1044	>>> valid, msg = glyphLibValidator(lib)
1045	>>> valid
1046	False
1047	>>> print(msg)
1048	public.markColor is not properly formatted.
1049	"""
1050	if not isDictEnough(value):
1051		reason = "expected a dictionary, found %s" % type(value).__name__
1052		return False, _bogusLibFormatMessage % reason
1053	for key, value in value.items():
1054		if not isinstance(key, basestring):
1055			reason = "key (%s) should be a string" % key
1056			return False, _bogusLibFormatMessage % reason
1057		# public.markColor
1058		if key == "public.markColor":
1059			if not colorValidator(value):
1060				return False, "public.markColor is not properly formatted."
1061	return True, None
1062
1063
1064if __name__ == "__main__":
1065	import doctest
1066	doctest.testmod()
1067