"""Various low level data validators.""" import calendar from io import open import fs.base import fs.osfs from collections.abc import Mapping from fontTools.ufoLib.utils import numberTypes # ------- # Generic # ------- def isDictEnough(value): """ Some objects will likely come in that aren't dicts but are dict-ish enough. """ if isinstance(value, Mapping): return True for attr in ("keys", "values", "items"): if not hasattr(value, attr): return False return True def genericTypeValidator(value, typ): """ Generic. (Added at version 2.) """ return isinstance(value, typ) def genericIntListValidator(values, validValues): """ Generic. (Added at version 2.) """ if not isinstance(values, (list, tuple)): return False valuesSet = set(values) validValuesSet = set(validValues) if valuesSet - validValuesSet: return False for value in values: if not isinstance(value, int): return False return True def genericNonNegativeIntValidator(value): """ Generic. (Added at version 3.) """ if not isinstance(value, int): return False if value < 0: return False return True def genericNonNegativeNumberValidator(value): """ Generic. (Added at version 3.) """ if not isinstance(value, numberTypes): return False if value < 0: return False return True def genericDictValidator(value, prototype): """ Generic. (Added at version 3.) """ # not a dict if not isinstance(value, Mapping): return False # missing required keys for key, (typ, required) in prototype.items(): if not required: continue if key not in value: return False # unknown keys for key in value.keys(): if key not in prototype: return False # incorrect types for key, v in value.items(): prototypeType, required = prototype[key] if v is None and not required: continue if not isinstance(v, prototypeType): return False return True # -------------- # fontinfo.plist # -------------- # Data Validators def fontInfoStyleMapStyleNameValidator(value): """ Version 2+. """ options = ["regular", "italic", "bold", "bold italic"] return value in options def fontInfoOpenTypeGaspRangeRecordsValidator(value): """ Version 3+. """ if not isinstance(value, list): return False if len(value) == 0: return True validBehaviors = [0, 1, 2, 3] dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True)) ppemOrder = [] for rangeRecord in value: if not genericDictValidator(rangeRecord, dictPrototype): return False ppem = rangeRecord["rangeMaxPPEM"] behavior = rangeRecord["rangeGaspBehavior"] ppemValidity = genericNonNegativeIntValidator(ppem) if not ppemValidity: return False behaviorValidity = genericIntListValidator(behavior, validBehaviors) if not behaviorValidity: return False ppemOrder.append(ppem) if ppemOrder != sorted(ppemOrder): return False return True def fontInfoOpenTypeHeadCreatedValidator(value): """ Version 2+. """ # format: 0000/00/00 00:00:00 if not isinstance(value, str): return False # basic formatting if not len(value) == 19: return False if value.count(" ") != 1: return False date, time = value.split(" ") if date.count("/") != 2: return False if time.count(":") != 2: return False # date year, month, day = date.split("/") if len(year) != 4: return False if len(month) != 2: return False if len(day) != 2: return False try: year = int(year) month = int(month) day = int(day) except ValueError: return False if month < 1 or month > 12: return False monthMaxDay = calendar.monthrange(year, month)[1] if day < 1 or day > monthMaxDay: return False # time hour, minute, second = time.split(":") if len(hour) != 2: return False if len(minute) != 2: return False if len(second) != 2: return False try: hour = int(hour) minute = int(minute) second = int(second) except ValueError: return False if hour < 0 or hour > 23: return False if minute < 0 or minute > 59: return False if second < 0 or second > 59: return False # fallback return True def fontInfoOpenTypeNameRecordsValidator(value): """ Version 3+. """ if not isinstance(value, list): return False dictPrototype = dict(nameID=(int, True), platformID=(int, True), encodingID=(int, True), languageID=(int, True), string=(str, True)) for nameRecord in value: if not genericDictValidator(nameRecord, dictPrototype): return False return True def fontInfoOpenTypeOS2WeightClassValidator(value): """ Version 2+. """ if not isinstance(value, int): return False if value < 0: return False return True def fontInfoOpenTypeOS2WidthClassValidator(value): """ Version 2+. """ if not isinstance(value, int): return False if value < 1: return False if value > 9: return False return True def fontInfoVersion2OpenTypeOS2PanoseValidator(values): """ Version 2. """ if not isinstance(values, (list, tuple)): return False if len(values) != 10: return False for value in values: if not isinstance(value, int): return False # XXX further validation? return True def fontInfoVersion3OpenTypeOS2PanoseValidator(values): """ Version 3+. """ if not isinstance(values, (list, tuple)): return False if len(values) != 10: return False for value in values: if not isinstance(value, int): return False if value < 0: return False # XXX further validation? return True def fontInfoOpenTypeOS2FamilyClassValidator(values): """ Version 2+. """ if not isinstance(values, (list, tuple)): return False if len(values) != 2: return False for value in values: if not isinstance(value, int): return False classID, subclassID = values if classID < 0 or classID > 14: return False if subclassID < 0 or subclassID > 15: return False return True def fontInfoPostscriptBluesValidator(values): """ Version 2+. """ if not isinstance(values, (list, tuple)): return False if len(values) > 14: return False if len(values) % 2: return False for value in values: if not isinstance(value, numberTypes): return False return True def fontInfoPostscriptOtherBluesValidator(values): """ Version 2+. """ if not isinstance(values, (list, tuple)): return False if len(values) > 10: return False if len(values) % 2: return False for value in values: if not isinstance(value, numberTypes): return False return True def fontInfoPostscriptStemsValidator(values): """ Version 2+. """ if not isinstance(values, (list, tuple)): return False if len(values) > 12: return False for value in values: if not isinstance(value, numberTypes): return False return True def fontInfoPostscriptWindowsCharacterSetValidator(value): """ Version 2+. """ validValues = list(range(1, 21)) if value not in validValues: return False return True def fontInfoWOFFMetadataUniqueIDValidator(value): """ Version 3+. """ dictPrototype = dict(id=(str, True)) if not genericDictValidator(value, dictPrototype): return False return True def fontInfoWOFFMetadataVendorValidator(value): """ Version 3+. """ dictPrototype = {"name" : (str, True), "url" : (str, False), "dir" : (str, False), "class" : (str, False)} if not genericDictValidator(value, dictPrototype): return False if "dir" in value and value.get("dir") not in ("ltr", "rtl"): return False return True def fontInfoWOFFMetadataCreditsValidator(value): """ Version 3+. """ dictPrototype = dict(credits=(list, True)) if not genericDictValidator(value, dictPrototype): return False if not len(value["credits"]): return False dictPrototype = {"name" : (str, True), "url" : (str, False), "role" : (str, False), "dir" : (str, False), "class" : (str, False)} for credit in value["credits"]: if not genericDictValidator(credit, dictPrototype): return False if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"): return False return True def fontInfoWOFFMetadataDescriptionValidator(value): """ Version 3+. """ dictPrototype = dict(url=(str, False), text=(list, True)) if not genericDictValidator(value, dictPrototype): return False for text in value["text"]: if not fontInfoWOFFMetadataTextValue(text): return False return True def fontInfoWOFFMetadataLicenseValidator(value): """ Version 3+. """ dictPrototype = dict(url=(str, False), text=(list, False), id=(str, False)) if not genericDictValidator(value, dictPrototype): return False if "text" in value: for text in value["text"]: if not fontInfoWOFFMetadataTextValue(text): return False return True def fontInfoWOFFMetadataTrademarkValidator(value): """ Version 3+. """ dictPrototype = dict(text=(list, True)) if not genericDictValidator(value, dictPrototype): return False for text in value["text"]: if not fontInfoWOFFMetadataTextValue(text): return False return True def fontInfoWOFFMetadataCopyrightValidator(value): """ Version 3+. """ dictPrototype = dict(text=(list, True)) if not genericDictValidator(value, dictPrototype): return False for text in value["text"]: if not fontInfoWOFFMetadataTextValue(text): return False return True def fontInfoWOFFMetadataLicenseeValidator(value): """ Version 3+. """ dictPrototype = {"name" : (str, True), "dir" : (str, False), "class" : (str, False)} if not genericDictValidator(value, dictPrototype): return False if "dir" in value and value.get("dir") not in ("ltr", "rtl"): return False return True def fontInfoWOFFMetadataTextValue(value): """ Version 3+. """ dictPrototype = {"text" : (str, True), "language" : (str, False), "dir" : (str, False), "class" : (str, False)} if not genericDictValidator(value, dictPrototype): return False if "dir" in value and value.get("dir") not in ("ltr", "rtl"): return False return True def fontInfoWOFFMetadataExtensionsValidator(value): """ Version 3+. """ if not isinstance(value, list): return False if not value: return False for extension in value: if not fontInfoWOFFMetadataExtensionValidator(extension): return False return True def fontInfoWOFFMetadataExtensionValidator(value): """ Version 3+. """ dictPrototype = dict(names=(list, False), items=(list, True), id=(str, False)) if not genericDictValidator(value, dictPrototype): return False if "names" in value: for name in value["names"]: if not fontInfoWOFFMetadataExtensionNameValidator(name): return False for item in value["items"]: if not fontInfoWOFFMetadataExtensionItemValidator(item): return False return True def fontInfoWOFFMetadataExtensionItemValidator(value): """ Version 3+. """ dictPrototype = dict(id=(str, False), names=(list, True), values=(list, True)) if not genericDictValidator(value, dictPrototype): return False for name in value["names"]: if not fontInfoWOFFMetadataExtensionNameValidator(name): return False for val in value["values"]: if not fontInfoWOFFMetadataExtensionValueValidator(val): return False return True def fontInfoWOFFMetadataExtensionNameValidator(value): """ Version 3+. """ dictPrototype = {"text" : (str, True), "language" : (str, False), "dir" : (str, False), "class" : (str, False)} if not genericDictValidator(value, dictPrototype): return False if "dir" in value and value.get("dir") not in ("ltr", "rtl"): return False return True def fontInfoWOFFMetadataExtensionValueValidator(value): """ Version 3+. """ dictPrototype = {"text" : (str, True), "language" : (str, False), "dir" : (str, False), "class" : (str, False)} if not genericDictValidator(value, dictPrototype): return False if "dir" in value and value.get("dir") not in ("ltr", "rtl"): return False return True # ---------- # Guidelines # ---------- def guidelinesValidator(value, identifiers=None): """ Version 3+. """ if not isinstance(value, list): return False if identifiers is None: identifiers = set() for guide in value: if not guidelineValidator(guide): return False identifier = guide.get("identifier") if identifier is not None: if identifier in identifiers: return False identifiers.add(identifier) return True _guidelineDictPrototype = dict( x=((int, float), False), y=((int, float), False), angle=((int, float), False), name=(str, False), color=(str, False), identifier=(str, False) ) def guidelineValidator(value): """ Version 3+. """ if not genericDictValidator(value, _guidelineDictPrototype): return False x = value.get("x") y = value.get("y") angle = value.get("angle") # x or y must be present if x is None and y is None: return False # if x or y are None, angle must not be present if x is None or y is None: if angle is not None: return False # if x and y are defined, angle must be defined if x is not None and y is not None and angle is None: return False # angle must be between 0 and 360 if angle is not None: if angle < 0: return False if angle > 360: return False # identifier must be 1 or more characters identifier = value.get("identifier") if identifier is not None and not identifierValidator(identifier): return False # color must follow the proper format color = value.get("color") if color is not None and not colorValidator(color): return False return True # ------- # Anchors # ------- def anchorsValidator(value, identifiers=None): """ Version 3+. """ if not isinstance(value, list): return False if identifiers is None: identifiers = set() for anchor in value: if not anchorValidator(anchor): return False identifier = anchor.get("identifier") if identifier is not None: if identifier in identifiers: return False identifiers.add(identifier) return True _anchorDictPrototype = dict( x=((int, float), False), y=((int, float), False), name=(str, False), color=(str, False), identifier=(str, False) ) def anchorValidator(value): """ Version 3+. """ if not genericDictValidator(value, _anchorDictPrototype): return False x = value.get("x") y = value.get("y") # x and y must be present if x is None or y is None: return False # identifier must be 1 or more characters identifier = value.get("identifier") if identifier is not None and not identifierValidator(identifier): return False # color must follow the proper format color = value.get("color") if color is not None and not colorValidator(color): return False return True # ---------- # Identifier # ---------- def identifierValidator(value): """ Version 3+. >>> identifierValidator("a") True >>> identifierValidator("") False >>> identifierValidator("a" * 101) False """ validCharactersMin = 0x20 validCharactersMax = 0x7E if not isinstance(value, str): return False if not value: return False if len(value) > 100: return False for c in value: c = ord(c) if c < validCharactersMin or c > validCharactersMax: return False return True # ----- # Color # ----- def colorValidator(value): """ Version 3+. >>> colorValidator("0,0,0,0") True >>> colorValidator(".5,.5,.5,.5") True >>> colorValidator("0.5,0.5,0.5,0.5") True >>> colorValidator("1,1,1,1") True >>> colorValidator("2,0,0,0") False >>> colorValidator("0,2,0,0") False >>> colorValidator("0,0,2,0") False >>> colorValidator("0,0,0,2") False >>> colorValidator("1r,1,1,1") False >>> colorValidator("1,1g,1,1") False >>> colorValidator("1,1,1b,1") False >>> colorValidator("1,1,1,1a") False >>> colorValidator("1 1 1 1") False >>> colorValidator("1 1,1,1") False >>> colorValidator("1,1 1,1") False >>> colorValidator("1,1,1 1") False >>> colorValidator("1, 1, 1, 1") True """ if not isinstance(value, str): return False parts = value.split(",") if len(parts) != 4: return False for part in parts: part = part.strip() converted = False try: part = int(part) converted = True except ValueError: pass if not converted: try: part = float(part) converted = True except ValueError: pass if not converted: return False if part < 0: return False if part > 1: return False return True # ----- # image # ----- pngSignature = b"\x89PNG\r\n\x1a\n" _imageDictPrototype = dict( fileName=(str, True), xScale=((int, float), False), xyScale=((int, float), False), yxScale=((int, float), False), yScale=((int, float), False), xOffset=((int, float), False), yOffset=((int, float), False), color=(str, False) ) def imageValidator(value): """ Version 3+. """ if not genericDictValidator(value, _imageDictPrototype): return False # fileName must be one or more characters if not value["fileName"]: return False # color must follow the proper format color = value.get("color") if color is not None and not colorValidator(color): return False return True def pngValidator(path=None, data=None, fileObj=None): """ Version 3+. This checks the signature of the image data. """ assert path is not None or data is not None or fileObj is not None if path is not None: with open(path, "rb") as f: signature = f.read(8) elif data is not None: signature = data[:8] elif fileObj is not None: pos = fileObj.tell() signature = fileObj.read(8) fileObj.seek(pos) if signature != pngSignature: return False, "Image does not begin with the PNG signature." return True, None # ------------------- # layercontents.plist # ------------------- def layerContentsValidator(value, ufoPathOrFileSystem): """ Check the validity of layercontents.plist. Version 3+. """ if isinstance(ufoPathOrFileSystem, fs.base.FS): fileSystem = ufoPathOrFileSystem else: fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem) bogusFileMessage = "layercontents.plist in not in the correct format." # file isn't in the right format if not isinstance(value, list): return False, bogusFileMessage # work through each entry usedLayerNames = set() usedDirectories = set() contents = {} for entry in value: # layer entry in the incorrect format if not isinstance(entry, list): return False, bogusFileMessage if not len(entry) == 2: return False, bogusFileMessage for i in entry: if not isinstance(i, str): return False, bogusFileMessage layerName, directoryName = entry # check directory naming if directoryName != "glyphs": if not directoryName.startswith("glyphs."): return False, "Invalid directory name (%s) in layercontents.plist." % directoryName if len(layerName) == 0: return False, "Empty layer name in layercontents.plist." # directory doesn't exist if not fileSystem.exists(directoryName): return False, "A glyphset does not exist at %s." % directoryName # default layer name if layerName == "public.default" and directoryName != "glyphs": return False, "The name public.default is being used by a layer that is not the default." # check usage if layerName in usedLayerNames: return False, "The layer name %s is used by more than one layer." % layerName usedLayerNames.add(layerName) if directoryName in usedDirectories: return False, "The directory %s is used by more than one layer." % directoryName usedDirectories.add(directoryName) # store contents[layerName] = directoryName # missing default layer foundDefault = "glyphs" in contents.values() if not foundDefault: return False, "The required default glyph set is not in the UFO." return True, None # ------------ # groups.plist # ------------ def groupsValidator(value): """ Check the validity of the groups. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2). >>> groups = {"A" : ["A", "A"], "A2" : ["A"]} >>> groupsValidator(groups) (True, None) >>> groups = {"" : ["A"]} >>> valid, msg = groupsValidator(groups) >>> valid False >>> print(msg) A group has an empty name. >>> groups = {"public.awesome" : ["A"]} >>> groupsValidator(groups) (True, None) >>> groups = {"public.kern1." : ["A"]} >>> valid, msg = groupsValidator(groups) >>> valid False >>> print(msg) The group data contains a kerning group with an incomplete name. >>> groups = {"public.kern2." : ["A"]} >>> valid, msg = groupsValidator(groups) >>> valid False >>> print(msg) The group data contains a kerning group with an incomplete name. >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]} >>> groupsValidator(groups) (True, None) >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]} >>> valid, msg = groupsValidator(groups) >>> valid False >>> print(msg) The glyph "A" occurs in too many kerning groups. """ bogusFormatMessage = "The group data is not in the correct format." if not isDictEnough(value): return False, bogusFormatMessage firstSideMapping = {} secondSideMapping = {} for groupName, glyphList in value.items(): if not isinstance(groupName, (str)): return False, bogusFormatMessage if not isinstance(glyphList, (list, tuple)): return False, bogusFormatMessage if not groupName: return False, "A group has an empty name." if groupName.startswith("public."): if not groupName.startswith("public.kern1.") and not groupName.startswith("public.kern2."): # unknown public.* name. silently skip. continue else: if len("public.kernN.") == len(groupName): return False, "The group data contains a kerning group with an incomplete name." if groupName.startswith("public.kern1."): d = firstSideMapping else: d = secondSideMapping for glyphName in glyphList: if not isinstance(glyphName, str): return False, "The group data %s contains an invalid member." % groupName if glyphName in d: return False, "The glyph \"%s\" occurs in too many kerning groups." % glyphName d[glyphName] = groupName return True, None # ------------- # kerning.plist # ------------- def kerningValidator(data): """ Check the validity of the kerning data structure. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2). >>> kerning = {"A" : {"B" : 100}} >>> kerningValidator(kerning) (True, None) >>> kerning = {"A" : ["B"]} >>> valid, msg = kerningValidator(kerning) >>> valid False >>> print(msg) The kerning data is not in the correct format. >>> kerning = {"A" : {"B" : "100"}} >>> valid, msg = kerningValidator(kerning) >>> valid False >>> print(msg) The kerning data is not in the correct format. """ bogusFormatMessage = "The kerning data is not in the correct format." if not isinstance(data, Mapping): return False, bogusFormatMessage for first, secondDict in data.items(): if not isinstance(first, str): return False, bogusFormatMessage elif not isinstance(secondDict, Mapping): return False, bogusFormatMessage for second, value in secondDict.items(): if not isinstance(second, str): return False, bogusFormatMessage elif not isinstance(value, numberTypes): return False, bogusFormatMessage return True, None # ------------- # lib.plist/lib # ------------- _bogusLibFormatMessage = "The lib data is not in the correct format: %s" def fontLibValidator(value): """ Check the validity of the lib. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2). >>> lib = {"foo" : "bar"} >>> fontLibValidator(lib) (True, None) >>> lib = {"public.awesome" : "hello"} >>> fontLibValidator(lib) (True, None) >>> lib = {"public.glyphOrder" : ["A", "C", "B"]} >>> fontLibValidator(lib) (True, None) >>> lib = "hello" >>> valid, msg = fontLibValidator(lib) >>> valid False >>> print(msg) # doctest: +ELLIPSIS The lib data is not in the correct format: expected a dictionary, ... >>> lib = {1: "hello"} >>> valid, msg = fontLibValidator(lib) >>> valid False >>> print(msg) The lib key is not properly formatted: expected str, found int: 1 >>> lib = {"public.glyphOrder" : "hello"} >>> valid, msg = fontLibValidator(lib) >>> valid False >>> print(msg) # doctest: +ELLIPSIS public.glyphOrder is not properly formatted: expected list or tuple,... >>> lib = {"public.glyphOrder" : ["A", 1, "B"]} >>> valid, msg = fontLibValidator(lib) >>> valid False >>> print(msg) # doctest: +ELLIPSIS public.glyphOrder is not properly formatted: expected str,... """ if not isDictEnough(value): reason = "expected a dictionary, found %s" % type(value).__name__ return False, _bogusLibFormatMessage % reason for key, value in value.items(): if not isinstance(key, str): return False, ( "The lib key is not properly formatted: expected str, found %s: %r" % (type(key).__name__, key)) # public.glyphOrder if key == "public.glyphOrder": bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s" if not isinstance(value, (list, tuple)): reason = "expected list or tuple, found %s" % type(value).__name__ return False, bogusGlyphOrderMessage % reason for glyphName in value: if not isinstance(glyphName, str): reason = "expected str, found %s" % type(glyphName).__name__ return False, bogusGlyphOrderMessage % reason return True, None # -------- # GLIF lib # -------- def glyphLibValidator(value): """ Check the validity of the lib. Version 3+ (though it's backwards compatible with UFO 1 and UFO 2). >>> lib = {"foo" : "bar"} >>> glyphLibValidator(lib) (True, None) >>> lib = {"public.awesome" : "hello"} >>> glyphLibValidator(lib) (True, None) >>> lib = {"public.markColor" : "1,0,0,0.5"} >>> glyphLibValidator(lib) (True, None) >>> lib = {"public.markColor" : 1} >>> valid, msg = glyphLibValidator(lib) >>> valid False >>> print(msg) public.markColor is not properly formatted. """ if not isDictEnough(value): reason = "expected a dictionary, found %s" % type(value).__name__ return False, _bogusLibFormatMessage % reason for key, value in value.items(): if not isinstance(key, str): reason = "key (%s) should be a string" % key return False, _bogusLibFormatMessage % reason # public.markColor if key == "public.markColor": if not colorValidator(value): return False, "public.markColor is not properly formatted." return True, None if __name__ == "__main__": import doctest doctest.testmod()