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