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