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