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