validators.py 30 KB

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