spiegel-keyman/linux/keyman-config/keyman_config/kmpmetadata.py
2023-06-07 18:58:06 +02:00

584 lines
22 KiB
Python
Executable file

#!/usr/bin/python3
import configparser
import json
import logging
import os
import sys
from enum import Enum
from json.decoder import JSONDecodeError
import magic
from keyman_config import secure_lookup
class KMFileTypes(Enum):
KM_ICON = 1
KM_SOURCE = 2
KM_OSK_SOURCE = 3
KM_KMX = 4
KM_OSK = 5
KM_TOUCH = 6
KM_FONT = 7
KM_DOC = 8
KM_META = 9
KM_IMAGE = 10
KM_TECKIT = 11
KM_CC = 12
KM_XML = 13
KM_UNKNOWN = 99
def print_info(info):
try:
print("---- Info ----")
if not info:
return
print("Name: ", secure_lookup(info, 'name', 'description'))
print("Copyright: ", secure_lookup(info, 'copyright', 'description'))
if 'version' in info:
print("Version: ", secure_lookup(info, 'version', 'description'))
if 'author' in info:
print("Author: ", secure_lookup(info, 'author', 'description'))
if secure_lookup(info, 'author', 'url'):
print("Author URL: ", secure_lookup(info, 'author', 'url'))
if 'website' in info:
print("Website description: ", secure_lookup(info, 'website', 'description'))
if secure_lookup(info, 'website', 'url'):
print("Website URL: ", secure_lookup(info, 'website', 'url'))
except Exception as e:
print(type(e)) # the exception instance
print(e.args) # arguments stored in .args
print(e) # __str__ allows args to be printed directly, pass
def print_system(system):
try:
print("---- System ----")
if not system:
return
if 'fileVersion' in system:
print("File Version: ", system['fileVersion'])
if 'keymanDeveloperVersion' in system:
print("Keyman Developer Version: ", system['keymanDeveloperVersion'])
except Exception as e:
print(type(e)) # the exception instance
print(e.args) # arguments stored in .args
print(e) # __str__ allows args to be printed directly, pass
def print_options(options):
try:
print("---- Options ----")
if not options:
return
if 'readmeFile' in options:
print("Readme File: ", options['readmeFile'])
if 'graphicFile' in options:
print("Graphic File: ", options['graphicFile'])
except Exception as e:
print(type(e)) # the exception instance
print(e.args) # arguments stored in .args
print(e) # __str__ allows args to be printed directly, pass
def print_keyboards(keyboards):
try:
print("---- Keyboards ----")
if not keyboards:
return
for kb in keyboards:
print("Keyboard Name: ", kb['name'])
print("Keyboard Id: ", kb['id'])
if 'version' in kb:
print("Keyboard Version: ", kb['version'])
if 'oskFont' in kb:
print("Keyboard On screen keyboard Font: ", kb['oskFont'])
if 'oskFont' in kb:
print("Keyboard Display Font: ", kb['displayFont'])
print("Languages")
for lang in kb['languages']:
print(" Name: ", lang['name'], "Id: ", lang['id'])
except Exception as e:
print(type(e)) # the exception instance
print(e.args) # arguments stored in .args
print(e) # __str__ allows args to be printed directly, pass
def determine_filetype(kblist, filename):
"""
Determine file type of a filename in a kmp from the extension
Args:
kblist (list): list of keyboard ids
filename (str): File name
Returns:
KMFileTypes: Enum of file type
KM_ICON: Keyboard icon
KM_SOURCE: Keyboard source
KM_OSK_SOURCE: Keyboard on-screen keyboard source
KM_KMX: Compiled keyboard
KM_OSK: Compiled on screen keyboard
KM_TOUCH: JS touch keyboard
KM_FONT: Font
KM_DOC: Documentation
KM_META: Metadata
KM_IMAGE: Image
KM_TECKIT: Files to use with teckit
KM_CC: Consistent changes tables
KM_XML: unspecified xml files
KM_UNKNOWN: Unknown
"""
name, ext = os.path.splitext(filename)
if not ext:
return KMFileTypes.KM_UNKNOWN
elif ext.lower() == ".ico":
return KMFileTypes.KM_ICON
elif ext.lower() == ".kmn":
return KMFileTypes.KM_SOURCE
elif ext.lower() == ".kvks":
return KMFileTypes.KM_OSK_SOURCE
elif ext.lower() == ".kmx":
return KMFileTypes.KM_KMX
elif ext.lower() == ".kvk":
return KMFileTypes.KM_OSK
elif ext.lower() in [".ttf", ".otf"]:
return KMFileTypes.KM_FONT
elif ext.lower() == ".js":
if kblist is None:
return KMFileTypes.KM_UNKNOWN
if name in kblist:
return KMFileTypes.KM_TOUCH
else:
return KMFileTypes.KM_DOC if name == "keyrenderer" else KMFileTypes.KM_UNKNOWN
elif ext.lower() in [
".txt",
".pdf",
".htm",
".html",
".doc",
".docx",
".css",
".chm",
"",
".md",
".odt",
".rtf",
".dot",
".mht",
".woff",
".php",
]:
return KMFileTypes.KM_DOC
elif ext.lower() in [".inf", ".json"]:
return KMFileTypes.KM_META
elif ext.lower() in [".png", ".jpeg", ".jpg", ".gif", ".bmp"]:
return KMFileTypes.KM_IMAGE
elif ext.lower() in [".tec", ".map"]:
return KMFileTypes.KM_TECKIT
elif ext.lower() == ".cct":
return KMFileTypes.KM_CC
elif ext.lower() == ".xml":
return KMFileTypes.KM_XML
else:
return KMFileTypes.KM_UNKNOWN
def print_files(files, extracted_dir):
try:
print("---- Files ----")
if not files:
return
for kbfile in files:
print("* File name: ", kbfile['name'])
print(" Description: ", kbfile['description'])
print(" Type: ", kbfile['type'])
file = os.path.join(extracted_dir, kbfile['name'])
if os.path.isfile(file):
print(" File", file, "exists")
ms = magic.open(magic.MAGIC_NONE)
ms.load()
ftype = ms.file(file)
print(" Type: ", ftype)
ms.close()
else:
print(" File", file, "does not exist")
except Exception as e:
print(type(e)) # the exception instance
print(e.args) # arguments stored in .args
print(e) # __str__ allows args to be printed directly, pass
def get_fonts(files):
fonts = []
if not files:
return fonts
for kbfile in files:
if kbfile['type'] == KMFileTypes.KM_FONT:
fonts.append(kbfile)
return fonts
def parseinfdata(inffile, verbose=False):
"""
Parse the metadata in a kmp.inf file.
Args:
jsonfile (str): Path to kmp.inf
verbose (bool, default False): verbose output
Returns:
list[5]: info, system, options, keyboards, files
info (dict):
name (dict):
description (str): KMP name
copyright (dict):
description (str): KMP copyright
version (dict):
description (str): KMP version
author (dict):
description (str): KMP author
url (str): contact url for the author
system (dict): System info
fileVersion (str): Keyman file format version
keymanDeveloperVersion (str): Keyman Developer version that compiled keyboard
options (dict): Keyboard options
readmeFile (str): README for the keyboard
keyboards (list): Keyboards in the kmp
name (str): Keyboard name
id (str): Keyboard ID
version (str): Keyboard version
oskFont (str, optional): Recommended on screen keyboard font
displayFont (str, optional): Recommended display font
languages (list): Languages the keyboard is used for
name (str): Language name
id (str): Language ID
files (list): Files in the kmp
name (str): File name
description (str): File description
type (KMFileTypes): Keyman file type
"""
info = system = keyboards = files = options = None
extracted_dir = os.path.dirname(inffile)
if os.path.isfile(inffile):
config = configparser.ConfigParser()
config.optionxform = str
logging.debug("parseinfdata: reading file:%s dir:%s", inffile, extracted_dir)
with open(inffile, 'r', encoding='latin_1') as f:
config.read_file(f)
info = None
for section in config.sections():
if section == 'Info':
if not info:
info = {}
for item in config.items('Info'):
if item[0] in ['Name', 'name']:
info['name'] = {'description': item[1].split("\"")[1]}
elif item[0] in ['Copyright', 'copyright']:
info['copyright'] = {'description': item[1].split("\"")[1]}
elif item[0] == 'Version':
info['version'] = {'description': item[1].split("\"")[1]}
elif item[0] == 'Author':
info['author'] = {'description': item[1].split("\"")[1], 'url': item[1].split("\"")[3]}
elif item[0] == "WebSite":
info['website'] = {'description': item[1].split("\"")[1], 'url': item[1].split("\"")[3]}
else:
logging.warning("Unknown item in Info: %s", item[0])
if section == 'PackageInfo':
if not info:
info = {}
info['version'] = {'description': "1.0"}
for item in config.items('PackageInfo'):
if item[0] in ['Name', 'name']:
if item[1].split("\"")[1]:
info['name'] = {'description': item[1].split("\"")[1]}
else:
info['name'] = {'description': item[1].split("\"")[2]}
elif item[0] in ['Copyright', 'copyright']:
if item[1].split("\"")[1]:
info['copyright'] = {'description': item[1].split("\"")[1]}
else:
info['copyright'] = {'description': item[1].split("\"")[2]}
elif item[0] == 'Version':
info['version'] = {'description': item[1].split("\"")[1]}
elif item[0] == 'Author':
info['author'] = {'description': item[1].split("\"")[1], 'url': item[1].split("\"")[3]}
elif item[0] == "WebSite":
if item[1].split("\"")[1]:
info['website'] = {'description': item[1].split("\"")[1]}
else:
info['website'] = {'description': item[1].split("\"")[2]}
else:
logging.warning("Unknown item in Info: %s", item[0])
elif section == 'Package':
system = {}
if not options:
options = {}
for item in config.items('Package'):
if item[0] == 'Version':
# intentionally storing in system because that's where fileVersion lives in kmp.json
system['fileVersion'] = item[1]
elif item[0] == 'ReadMeFile':
options['readmeFile'] = item[1]
elif item[0] == 'GraphicFile':
options['graphicFile'] = item[1]
elif item[0] == 'DisableKeepFonts':
options['disableKeepFonts'] = item[1]
elif item[0] == 'BothVersionsIncluded':
options['bothVersionsIncluded'] = item[1]
elif item[0] != 'ExecuteProgram':
print("Unknown item in Package:", item[0])
system['keymanDeveloperVersion'] = ""
elif "Keyboard" in section:
keyboards = []
keyboard = {}
languages = []
for item in config.items(section):
if item[0] == 'Name':
keyboard['name'] = item[1]
elif item[0] == 'ID':
keyboard['id'] = item[1]
elif item[0] == 'Version':
keyboard['version'] = item[1]
elif item[0] == 'OSKFont':
keyboard['oskFont'] = item[1]
elif item[0] == 'DisplayFont':
keyboard['displayFont'] = item[1]
elif item[0] == 'RTL':
keyboard['RTL'] = item[1]
elif "Language" in item[0]:
# only split on first ','
langid, langname = item[1].split(",", 1)
languages.append({'name': langname, 'id': langid})
else:
logging.warning("Unknown item in keyboard: %s", item[0])
keyboard['languages'] = languages
keyboards.append(keyboard)
elif section == "Files":
files = []
for item in config.items(section):
splititem = item[1].split("\"")
kbfile = {'name': splititem[3], 'description': splititem[1],
'type': determine_filetype(None, splititem[3])}
files.append(kbfile)
elif section == "InstallFiles":
files = []
for item in config.items(section):
kbfile = {'name': item[0], 'description': item[1], 'type': determine_filetype(None, item[0])}
files.append(kbfile)
elif section == 'Install':
if not options:
options = {}
for item in config.items('Install'):
if item[0] == 'ReadmeFile':
options['readmeFile'] = item[1]
kblist = []
if not info:
info = {}
if 'version' not in info:
info['version'] = {'description': "1.0"}
# inf file may not have keyboards in legacy kmps so generate it if needed
if files and not keyboards:
id = "unknown"
keyboards = []
for kbfile in files:
if kbfile['type'] == KMFileTypes.KM_KMX:
id = os.path.basename(os.path.splitext(kbfile['name'])[0])
keyboards = [{
'name': id,
'id': id,
'version': secure_lookup(info, 'version', 'description')
}]
kblist = []
if files:
for k in keyboards:
kblist.append(k['id'])
for kbfile in files:
if kbfile['type'] == KMFileTypes.KM_UNKNOWN:
kbfile['type'] = determine_filetype(kblist, kbfile['name'])
logging.debug("finished parsing %s", inffile)
if verbose:
print_info(info)
print_system(system)
print_options(options)
print_keyboards(keyboards)
print_files(files, extracted_dir)
return info, system, options, keyboards, files
def parsemetadata(jsonfile, verbose=False):
"""
Parse the metadata in a kmp.json file.
Args:
jsonfile (str): Path to kmp.json
verbose (bool, default False): verbose output
Returns:
list[5]: info, system, options, keyboards, files
info (dict):
name (dict):
description (str): KMP name
copyright (dict):
description (str): KMP copyright
version (dict):
description (str): KMP version
author (dict):
description (str): KMP author
url (str): contact url for the author
system (dict): System info
fileVersion (str): Keyman file format version
keymanDeveloperVersion (str): Keyman Developer version that compiled keyboard
options (dict): Keyboard options
readmeFile (str): README for the keyboard
keyboards (list): Keyboards in the kmp
name (str): Keyboard name
id (str): Keyboard ID
version (str): Keyboard version
oskFont (str, optional): Recommended on screen keyboard font
displayFont (str, optional): Recommended display font
languages (list): Languages the keyboard is used for
name (str): Language name
id (str): Language ID
files (list): Files in the kmp
name (str): File name
description (str): File description
"""
info = system = keyboards = files = options = nonexistent = None
extracted_dir = os.path.dirname(jsonfile)
logging.debug("parsemetadata: reading file:%s dir:%s", jsonfile, extracted_dir)
if os.path.isfile(jsonfile):
with open(jsonfile, "r") as read_file:
try:
data = json.load(read_file)
except JSONDecodeError as e:
logging.error("parsemetadata: %s invalid: %s (line %d, col %d)",
jsonfile, e.msg, e.lineno, e.colno)
# Use empty details
data = {"nonexistent": "none"}
for x in data:
if x == 'files':
files = data[x]
elif x == 'info':
info = data[x]
elif x == 'keyboards':
keyboards = data[x]
elif x == 'nonexistent':
nonexistent = data[x]
elif x == 'options':
options = data[x]
elif x == 'system':
system = data[x]
kblist = []
if keyboards:
kblist.extend(k['id'] for k in keyboards)
if files:
for kbfile in files:
kbfile['type'] = determine_filetype(kblist, kbfile['name'])
if nonexistent is not None:
logging.warning("This should not happen")
if verbose:
print_info(info)
print_system(system)
if options:
print_options(options)
print_keyboards(keyboards)
print_files(files, extracted_dir)
return info, system, options, keyboards, files
def get_metadata(tmpdirname):
"""
Get metadata from kmp.json if it exists.
If it does not exist then will return get_and_convert_infdata
Args:
tmpdirname(str): temp directory to extract kmp
Returns:
list[5]: info, system, options, keyboards, files
see kmpmetadata.parsemetadata for details
"""
kmpjson = os.path.join(tmpdirname, "kmp.json")
if os.path.isfile(kmpjson):
return parsemetadata(kmpjson, False)
else:
return get_and_convert_infdata(tmpdirname)
def get_and_convert_infdata(tmpdirname):
"""
Get metadata from kmp.inf if it exists.
Convert it to kmp.json if possible
Args:
inputfile (str): path to kmp file
tmpdirname(str): temp directory to extract kmp
Returns:
list[5]: info, system, options, keyboards, files
see kmpmetadata.parseinfdata for details
"""
kmpinf = os.path.join(tmpdirname, "kmp.inf")
if os.path.isfile(kmpinf):
info, system, options, keyboards, files = parseinfdata(kmpinf, False)
j = infmetadata_to_json(info, system, options, keyboards, files)
kmpjson = os.path.join(tmpdirname, "kmp.json")
try:
with open(kmpjson, "w") as write_file:
print(j, file=write_file)
except Exception as e:
logging.warning('Exception %s writing metadata %s %s', type(e), kmpjson, e.args)
return info, system, options, keyboards, files
else:
return None, None, None, None, None
def infmetadata_to_json(info, system, options, keyboards, files):
jsonfiles = []
if files:
for entry in files:
jsonfiles.append({"name": entry["name"], "description": entry["description"]})
d = {"system": system, "options": options,
"info": info, "keyboards": keyboards, "files": jsonfiles}
return json.dumps(d, indent=2)
def main(argv):
if len(sys.argv) != 2:
logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>")
sys.exit(2)
inputfile = sys.argv[1]
if not os.path.isfile(inputfile):
logging.error("kmpmetadata.py Input file ", inputfile, " not found.")
logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>")
sys.exit(2)
name, ext = os.path.splitext(inputfile)
if ext == ".json":
parsemetadata(inputfile, True)
elif ext == ".inf":
info, system, options, keyboards, files = parseinfdata(inputfile, True)
jsontest = infmetadata_to_json(info, system, options, keyboards, files)
print(jsontest)
else:
logging.error("kmpmetadata.py Input file must be json or inf.")
logging.error("kmpmetadata.py <kmp.json> or <kmp.inf>")
sys.exit(2)
if __name__ == "__main__":
main(sys.argv[1:])