from argparse import ArgumentParser from os.path import isabs, join, abspath, dirname, exists, isdir, basename from os import listdir, makedirs, remove from typing import List import uuid from xml.sax.saxutils import escape from math import floor from base64 import b64encode from subprocess import run from yaml import load try: from yaml import CLoader as Loader except ImportError: from yaml import Loader try: import fontforge have_fontforge = True except ImportError: have_fontforge = False p = ArgumentParser() p.add_argument('input', help='Input config file', nargs='+', type=str) def get_path(path: str, input: str): if isabs(path): return path return abspath(join(input, path)) def get_input(input: str, input_dir: str, temp_dir: str) -> List[str]: dinput = get_path(input, input_dir) if not exists(dinput): raise FileNotFoundError(dinput) if isdir(dinput): files = listdir(dinput) output = [] for file in files: output.extend(get_input(join(input, file), input_dir, temp_dir)) return output linput = input.lower() if linput.endswith('.ttf') or linput.endswith('.otf'): return [dinput] if linput.endswith('.ttc'): if not have_fontforge: print('FontForge is not installed, skipping', dinput) return [] fonts = fontforge.fontsInFile(dinput) paths = [] for i in fonts: font_path = get_path(f"{i}.ttf", temp_dir) if not exists(font_path): font = fontforge.open(f"{dinput}({i})") font.generate(font_path) font.close() paths.append(font_path) return paths return [] def replace(m): return "-".join([m.group(1), m.group(2), '5' + m.group(3), '8' + m.group(5), m.group(5)]) def main(args=None): args = p.parse_args(args) for input in args.input: with open(input, 'r', encoding='UTF-8') as f: config = load(f, Loader=Loader) input_dir = dirname(input) temp_dir = 'tmp' if 'temp_dir' in config and config['temp_dir']: temp_dir = config['temp_dir'] temp_dir = get_path(temp_dir, input_dir) output = 'output.mobileconfig' if 'output' in config and config['output']: output = config['output'] output = get_path(output, input_dir) if 'cert_file' not in config or not config['cert_file']: raise ValueError('cert_file is required') cert_file = get_path(config['cert_file'], input_dir) if not exists(cert_file): raise FileNotFoundError(cert_file) if 'chain_file' not in config or not config['chain_file']: raise ValueError('chain_file is required') chain_file = get_path(config['chain_file'], input_dir) if not exists(chain_file): raise FileNotFoundError(chain_file) if 'privkey_file' not in config or not config['privkey_file']: raise ValueError('privkey_file is required') privkey_file = get_path(config['privkey_file'], input_dir) if not exists(privkey_file): raise FileNotFoundError(privkey_file) if 'input' not in config or not config['input']: raise ValueError('input is required') if not isinstance(config['input'], list): raise ValueError('input must be a list') input_files = [] makedirs(temp_dir, exist_ok=True) for input in config['input']: input_files.extend(get_input(input, input_dir, temp_dir)) input_files = {i for i in input_files} name = 'Custom Fonts' if 'name' in config and config['name']: name = config['name'] uuid_str = str(uuid.uuid4()) identifier = 'com.example-' + uuid_str if 'identifier' in config and config['identifier']: identifier = config['identifier'] description = 'Make the font available to iOS applications.' if 'description' in config and config['description']: description = config['description'] organization = 'Font Profile' if 'organization' in config and config['organization']: organization = config['organization'] temp_output = get_path(basename(output), temp_dir) with open(temp_output, 'w', encoding='UTF-8') as f: f.write(f""" PayloadUUID {uuid_str} PayloadVersion 1 PayloadIdentifier {identifier} PayloadDisplayName {escape(name)} PayloadOrganization {escape(organization)} PayloadDescription {escape(description)} PayloadContent """) for i in input_files: uuid2 = str(uuid.uuid4()) f.write(f""" PayloadVersion 1 PayloadType com.apple.font PayloadIdentifier {uuid2} PayloadUUID {uuid2} Font """) with open(i, 'rb') as inp: t = b'' t += inp.read(4096) while len(t) >= 3: read_len = floor(len(t) / 3) * 3 buf = t[:read_len] t = t[read_len:] f.write(b64encode(buf).decode()) t += inp.read(4096) if t: f.write(b64encode(t).decode()) f.write(f""" """) f.write(""" PayloadType Configuration PayloadRemovalDisallowed """) run(['openssl', 'smime', '-sign', '-in', temp_output, '-out', output, '-signer', cert_file, '-inkey', privkey_file, '-certfile', chain_file, '-outform', 'der', '-nodetach']) remove(temp_output) if __name__ == '__main__': main()