def load_manifest_dict(manifest, manifest_kind, default_project_type='native'): """ Load data from a manifest dictionary :param manifest: a dictionary of settings :param manifest_kind: 'package.json' or 'appinfo.json' :return: a tuple of (models.Project options dictionary, the media map, the dependencies dictionary) """ project = {} dependencies = {} if manifest_kind == APPINFO_MANIFEST: project['app_short_name'] = manifest['shortName'] project['app_long_name'] = manifest['longName'] project['app_company_name'] = manifest['companyName'] project['app_version_label'] = manifest['versionLabel'] project['app_keys'] = dict_to_pretty_json(manifest.get('appKeys', {})) project['sdk_version'] = manifest.get('sdkVersion', '2') project['app_modern_multi_js'] = manifest.get('enableMultiJS', False) elif manifest_kind == PACKAGE_MANIFEST: project['app_short_name'] = manifest['name'] project['app_company_name'] = manifest['author'] project['semver'] = manifest['version'] project['app_long_name'] = manifest['pebble']['displayName'] if settings.NPM_MANIFEST_SUPPORT: project['app_keys'] = dict_to_pretty_json(manifest['pebble'].get( 'messageKeys', [])) else: project['app_keys'] = dict_to_pretty_json(manifest['pebble'].get( 'messageKeys', {})) if isinstance(json.loads(project['app_keys']), list): raise InvalidProjectArchiveException( "Auto-assigned (array) messageKeys are not yet supported.") project['keywords'] = manifest.get('keywords', []) dependencies = manifest.get('dependencies', {}) manifest = manifest['pebble'] project['app_modern_multi_js'] = manifest.get('enableMultiJS', True) project['sdk_version'] = manifest.get('sdkVersion', '3') else: raise InvalidProjectArchiveException( _('Invalid manifest kind: %s') % manifest_kind[-12:]) project['app_uuid'] = manifest['uuid'] project['app_is_watchface'] = manifest.get('watchapp', {}).get('watchface', False) project['app_is_hidden'] = manifest.get('watchapp', {}).get('hiddenApp', False) project['app_is_shown_on_communication'] = manifest.get( 'watchapp', {}).get('onlyShownOnCommunication', False) project['app_capabilities'] = ','.join(manifest.get('capabilities', [])) if 'targetPlatforms' in manifest: project['app_platforms'] = ','.join(manifest['targetPlatforms']) if 'resources' in manifest and 'media' in manifest['resources']: media_map = manifest['resources']['media'] else: media_map = {} project['project_type'] = manifest.get('projectType', default_project_type) return project, media_map, dependencies
def make_valid_filename(zip_entry): entry_filename = zip_entry.filename if entry_filename[:dir_end] != base_dir: return False entry_filename = entry_filename[dir_end:] if entry_filename == '': return False if not os.path.normpath('/SENTINEL_DO_NOT_ACTUALLY_USE_THIS_NAME/%s' % entry_filename).startswith('/SENTINEL_DO_NOT_ACTUALLY_USE_THIS_NAME/'): raise SuspiciousOperation("Invalid zip file contents.") if zip_entry.file_size > 5242880: # 5 MB raise InvalidProjectArchiveException("Excessively large compressed file.") return entry_filename
def do_import_archive(project_id, archive, delete_project=False): project = Project.objects.get(pk=project_id) try: with tempfile.NamedTemporaryFile(suffix='.zip') as archive_file: archive_file.write(archive) archive_file.flush() with zipfile.ZipFile(str(archive_file.name), 'r') as z: contents = z.infolist() # Requirements: # - Find the folder containing the project. This may or may not be at the root level. # - Read in the source files, resources and resource map. # Observations: # - Legal projects must keep their source in a directory called 'src' containing at least one *.c file. # - Legal projects must have a resource map at resources/src/resource_map.json # Strategy: # - Find the shortest common prefix for 'resources/src/resource_map.json' and 'src/'. # - This is taken to be the project directory. # - Import every file in 'src/' with the extension .c or .h as a source file # - Parse resource_map.json and import files it references SRC_DIR = 'src/' WORKER_SRC_DIR = 'worker_src/' INCLUDE_SRC_DIR = 'include/' if len(contents) > 400: raise InvalidProjectArchiveException("Too many files in zip file.") archive_items = [ArchiveProjectItem(z, x) for x in contents] base_dir, manifest_item = find_project_root_and_manifest(archive_items) dir_end = len(base_dir) def make_valid_filename(zip_entry): entry_filename = zip_entry.filename if entry_filename[:dir_end] != base_dir: return False entry_filename = entry_filename[dir_end:] if entry_filename == '': return False if not os.path.normpath('/SENTINEL_DO_NOT_ACTUALLY_USE_THIS_NAME/%s' % entry_filename).startswith('/SENTINEL_DO_NOT_ACTUALLY_USE_THIS_NAME/'): raise SuspiciousOperation("Invalid zip file contents.") if zip_entry.file_size > 5242880: # 5 MB raise InvalidProjectArchiveException("Excessively large compressed file.") return entry_filename manifest_kind = make_valid_filename(manifest_item.entry) manifest_dict = json.loads(manifest_item.read()) # Now iterate over the things we found, filter out invalid files and look for the manifest. filtered_contents = [] for entry in contents: filename = make_valid_filename(entry) if not filename or filename in MANIFEST_KINDS: continue else: filtered_contents.append((filename, entry)) with transaction.atomic(): # We have a resource map! We can now try importing things from it. project_options, media_map, dependencies = load_manifest_dict(manifest_dict, manifest_kind) for k, v in project_options.iteritems(): setattr(project, k, v) project.full_clean() project.set_dependencies(dependencies) RES_PATH = project.resources_path tag_map = {v: k for k, v in ResourceVariant.VARIANT_STRINGS.iteritems() if v} desired_resources = {} resources_files = {} resource_variants = {} file_exists_for_root = {} # Go through the media map and look for resources for resource in media_map: file_name = resource['file'] identifier = resource['name'] # Pebble.js and simply.js both have some internal resources that we don't import. if project.project_type in {'pebblejs', 'simplyjs'}: if identifier in {'MONO_FONT_14', 'IMAGE_MENU_ICON', 'IMAGE_LOGO_SPLASH', 'IMAGE_TILE_SPLASH'}: continue tags, root_file_name = get_filename_variant(file_name, tag_map) if (len(tags) != 0): raise ValueError("Generic resource filenames cannot contain a tilde (~)") if file_name not in desired_resources: desired_resources[root_file_name] = [] desired_resources[root_file_name].append(resource) file_exists_for_root[root_file_name] = False # Go through the zip file process all resource and source files. for filename, entry in filtered_contents: if filename.startswith(RES_PATH): base_filename = filename[len(RES_PATH) + 1:] # Let's just try opening the file try: extracted = z.open("%s%s/%s" % (base_dir, RES_PATH, base_filename)) except KeyError: logger.debug("Failed to open %s", base_filename) continue # Now we know the file exists and is in the resource directory - is it the one we want? tags, root_file_name = get_filename_variant(base_filename, tag_map) tags_string = ",".join(str(int(t)) for t in tags) if root_file_name in desired_resources: medias = desired_resources[root_file_name] # Because 'kind' and 'is_menu_icons' are properties of ResourceFile in the database, # we just use the first one. resource = medias[0] # Make only one resource file per base resource. if root_file_name not in resources_files: kind = resource['type'] is_menu_icon = resource.get('menuIcon', False) resources_files[root_file_name] = ResourceFile.objects.create( project=project, file_name=os.path.basename(root_file_name), kind=kind, is_menu_icon=is_menu_icon) # But add a resource variant for every file actual_file_name = resource['file'] resource_variants[actual_file_name] = ResourceVariant.objects.create(resource_file=resources_files[root_file_name], tags=tags_string) resource_variants[actual_file_name].save_file(extracted) file_exists_for_root[root_file_name] = True else: try: base_filename, target = SourceFile.get_details_for_path(project.project_type, filename) except ValueError: # We'll just ignore any out of place files. continue source = SourceFile.objects.create(project=project, file_name=base_filename, target=target) with z.open(entry.filename) as f: source.save_text(f.read().decode('utf-8')) # Now add all the resource identifiers for root_file_name in desired_resources: for resource in desired_resources[root_file_name]: target_platforms = json.dumps(resource['targetPlatforms']) if 'targetPlatforms' in resource else None ResourceIdentifier.objects.create( resource_file=resources_files[root_file_name], resource_id=resource['name'], target_platforms=target_platforms, # Font options character_regex=resource.get('characterRegex', None), tracking=resource.get('trackingAdjust', None), compatibility=resource.get('compatibility', None), # Bitmap options memory_format=resource.get('memoryFormat', None), storage_format=resource.get('storageFormat', None), space_optimisation=resource.get('spaceOptimization', None) ) # Check that at least one variant of each specified resource exists. for root_file_name, loaded in file_exists_for_root.iteritems(): if not loaded: raise KeyError("No file was found to satisfy the manifest filename: {}".format(root_file_name)) project.save() send_td_event('cloudpebble_zip_import_succeeded', project=project) # At this point we're supposed to have successfully created the project. return True except Exception as e: if delete_project: try: Project.objects.get(pk=project_id).delete() except: pass send_td_event('cloudpebble_zip_import_failed', data={ 'data': { 'reason': str(e) } }, user=project.owner) raise