class UploadedFile: def __init__(self, is_strict=True): """ default public attributes """ self.fw = Firmware() self.is_strict = is_strict self.enable_inf_parsing = True self.fwupd_min_version = '0.8.0' # a guess, but everyone should have this self.version_formats = {} self.category_map = {'X-Device': 1} self.protocol_map = {} # strip out any unlisted files self.cabarchive_repacked = CabArchive() # private self._data_size = 0 self.cabarchive_upload = None self._version_inf = None def _parse_inf(self, contents): # FIXME is banned... if contents.find('FIXME') != -1: raise MetadataInvalid( 'The inf file was not complete; Any FIXME text must be ' 'replaced with the correct values.') # check .inf file is valid try: cfg = InfParser(contents) except configparser.MissingSectionHeaderError as _: raise MetadataInvalid('The inf file could not be parsed') try: tmp = cfg.get('Version', 'Class') except (configparser.NoOptionError, configparser.NoSectionError) as _: raise MetadataInvalid('The inf file Version:Class was missing') if tmp.lower() != 'firmware': raise MetadataInvalid('The inf file Version:Class was invalid') try: tmp = cfg.get('Version', 'ClassGuid') except configparser.NoOptionError as _: raise MetadataInvalid('The inf file Version:ClassGuid was missing') if tmp.lower() != '{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}': raise MetadataInvalid('The inf file Version:ClassGuid was invalid') try: tmp = cfg.get('Version', 'DriverVer').split(',') if len(tmp) != 2: raise MetadataInvalid( 'The inf file Version:DriverVer was invalid') self.fw.version_display = tmp[1] except configparser.NoOptionError as _: pass # this is optional, but if supplied must match the version in the XML # -- also note this will not work with multi-component .cab files if len(self.fw.mds) == 1 and self.fw.mds[0].version.isdigit(): try: self._version_inf = cfg.get('Firmware_AddReg', 'HKR->FirmwareVersion') if self._version_inf.startswith('0x'): self._version_inf = str(int(self._version_inf[2:], 16)) if self._version_inf == '0': self._version_inf = None except (configparser.NoOptionError, configparser.NoSectionError) as _: pass @staticmethod def _parse_release(md, release): # get description try: md.release_description = _node_validate_text( release.xpath('description')[0], minlen=3, maxlen=1000, nourl=True) except IndexError as _: pass md.install_duration = int(release.get('install_duration', '0')) md.release_urgency = release.get('urgency') # date, falling back to timestamp if 'date' in release.attrib: try: dt = datetime.datetime.strptime(release.get('date'), "%Y-%m-%d") dt_utc = dt.replace(tzinfo=datetime.timezone.utc) md.release_timestamp = int(dt_utc.timestamp()) except ValueError as e: raise MetadataInvalid( '<release> has invalid date attribute: {}'.format(str(e))) elif 'timestamp' in release.attrib: try: md.release_timestamp = int(release.get('timestamp')) except ValueError as e: raise MetadataInvalid( '<release> has invalid timestamp attribute: {}'.format( str(e))) else: raise MetadataInvalid( '<release> had no date or timestamp attributes') # optional release tag if 'tag' in release.attrib: md.release_tag = release.attrib['tag'] if len(md.release_tag) < 4: raise MetadataInvalid( '<release> tag was too short to identify the firmware') md.add_keywords_from_string(md.release_tag, priority=5) # get list of CVEs for issue in release.xpath('issues/issue'): kind = issue.get('type') if not kind: raise MetadataInvalid('<issue> had no type attribute') if kind != 'cve': raise MetadataInvalid('<issue> type can only be \'cve\'') value = _node_validate_text(issue, minlen=3, maxlen=1000, nourl=True) md.issues.append(ComponentIssue(kind=kind, value=value)) # get <url type="details"> try: md.details_url = _node_validate_text( release.xpath('url[@type="details"]')[0], minlen=12, maxlen=1000) except IndexError as _: pass # get <url type="source"> try: md.source_url = _node_validate_text( release.xpath('url[@type="source"]')[0], minlen=12, maxlen=1000) except IndexError as _: pass # fix up hex version md.version = release.get('version') if not md.version: raise MetadataInvalid('<release> had no version attribute') if md.version.startswith('0x'): md.version = str(int(md.version[2:], 16)) # ensure there's always a contents filename try: md.filename_contents = release.xpath( 'checksum[@target="content"]')[0].get('filename') except IndexError as _: pass if not md.filename_contents: md.filename_contents = 'firmware.bin' # ensure there's always a contents filename for csum in release.xpath('checksum[@target="device"]'): text = _node_validate_text(csum, minlen=32, maxlen=128) if csum.get('kind') == 'sha1': md.device_checksums.append(Checksum(text, 'SHA1')) elif csum.get('kind') == 'sha256': md.device_checksums.append(Checksum(text, 'SHA256')) if not md.filename_contents: md.filename_contents = 'firmware.bin' def _parse_component(self, component): # get priority md = Component() md.priority = int(component.get('priority', '0')) # check type if component.get('type') != 'firmware': raise MetadataInvalid('<component type="firmware"> required') # get <id> try: md.appstream_id = _node_validate_text(component.xpath('id')[0], minlen=10, maxlen=256) if not md.appstream_id: raise MetadataInvalid('<id> value invalid') for char in md.appstream_id: if char.isspace(): raise MetadataInvalid('<id> Cannot contain spaces') if char in ['/', '\\']: raise MetadataInvalid('<id> Cannot contain slashes') if char not in ['-', '_', '.'] and not char.isalnum(): raise MetadataInvalid( '<id> Cannot contain {}'.format(char)) if len(md.appstream_id.split('.')) < 4: raise MetadataInvalid( '<id> Should contain at least 4 sections to identify the model' ) except IndexError as _: raise MetadataInvalid('<id> tag missing') # get <developer_name> try: md.developer_name = _node_validate_text( component.xpath('developer_name')[0], minlen=3, maxlen=50, nourl=True) if md.developer_name == 'LenovoLtd.': md.developer_name = 'Lenovo Ltd.' md.add_keywords_from_string(md.developer_name, priority=10) except IndexError as _: raise MetadataInvalid('<developer_name> tag missing') if md.developer_name.find('@') != -1 or md.developer_name.find( '_at_') != -1: raise MetadataInvalid( '<developer_name> cannot contain an email address') # get <name> try: md.name = _node_validate_text(component.xpath('name')[0], minlen=3, maxlen=500) md.add_keywords_from_string(md.name, priority=3) # use categories instead if self.is_strict: category = { 'system': 'X-System', 'device': 'X-Device', 'bios': 'X-System', 'me': 'X-ManagementEngine', 'embedded': 'X-EmbeddedController', 'controller': 'X-EmbeddedController', } words = [word.lower() for word in md.name.split(' ')] for search in category: if search in words: raise MetadataInvalid('<name> tag should not contain {}, use ' '<categories><category>{}' '</category></categories> instead'.\ format(search, category[search])) # tokens banned outright for search in ['firmware', 'update', '(r)', '(c)']: if search in words: raise MetadataInvalid('<name> tag should not contain ' 'the word "{}"'.format(search)) # should not include the vendor in the name if md.developer_name_display: if md.developer_name_display.lower() in words: raise MetadataInvalid('<name> tag should not contain ' 'the vendor name "{}"'.format( md.developer_name_display)) except IndexError as _: raise MetadataInvalid('<name> tag missing') # get <summary> try: md.summary = _node_validate_text(component.xpath('summary')[0], minlen=10, maxlen=500) md.add_keywords_from_string(md.summary, priority=1) except IndexError as _: raise MetadataInvalid('<summary> tag missing') # get optional <name_variant_suffix> try: md.name_variant_suffix = _node_validate_text( component.xpath('name_variant_suffix')[0], minlen=2, maxlen=500) except IndexError as _: pass # get optional <description} try: md.description = _node_validate_text( component.xpath('description')[0], minlen=25, maxlen=1000, nourl=True) except IndexError as _: pass # get <metadata_license> if self.is_strict: try: md.metadata_license = _node_validate_text( component.xpath('metadata_license')[0]) if md.metadata_license not in [ 'CC0-1.0', 'FSFAP', 'CC-BY-3.0', 'CC-BY-SA-3.0', 'CC-BY-4.0', 'CC-BY-SA-4.0', 'GFDL-1.1', 'GFDL-1.2', 'GFDL-1.3' ]: raise MetadataInvalid( 'Invalid <metadata_license> tag of {}'.format( md.metadata_license)) except AttributeError as _: raise MetadataInvalid('<metadata_license> tag') except IndexError as _: raise MetadataInvalid('<metadata_license> tag missing') # get <project_license> try: md.project_license = _node_validate_text( component.xpath('project_license')[0], minlen=4, maxlen=50, nourl=True) except IndexError as _: raise MetadataInvalid('<project_license> tag missing') if not md.project_license: raise MetadataInvalid('<project_license> value invalid') # get <url type="homepage"> try: md.url_homepage = _node_validate_text( component.xpath('url[@type="homepage"]')[0], minlen=7, maxlen=1000) except IndexError as _: raise MetadataInvalid('<url type="homepage"> tag missing') if not md.url_homepage: raise MetadataInvalid('<url type="homepage"> value invalid') # add manually added keywords for keyword in component.xpath('keywords/keyword'): text = _node_validate_text(keyword, minlen=3, maxlen=50, nourl=True) if text.find(' ') != -1: raise MetadataInvalid('<keywords> cannot contain spaces') md.add_keywords_from_string(text, priority=5) # add provides for prov in component.xpath('provides/firmware[@type="flashed"]'): text = _node_validate_text(prov, minlen=5, maxlen=1000) if not _validate_guid(text): raise MetadataInvalid('The GUID {} was invalid.'.format(text)) if text in [ '230c8b18-8d9b-53ec-838b-6cfc0383493a', # main-system-firmware 'f15aa55c-9cd5-5942-85ae-a6bf8740b96c', # MST-panamera 'd6072785-6fc0-5f83-9d49-11376e7f48b1', # MST-leaf '49ec4eb4-c02b-58fc-8935-b1ee182405c7' ]: # MST-tesla raise MetadataInvalid( 'The GUID {} is too generic'.format(text)) md.guids.append(Guid(md.component_id, text)) if not md.guids: raise MetadataInvalid( 'The metadata file did not provide any GUID.') # check the file didn't try to add it's own <require> on vendor-id # to work around the vendor-id security checks in fwupd if component.xpath('requires/firmware[text()="vendor-id"]'): raise MetadataInvalid('Firmware cannot specify vendor-id') # check only recognised requirements are added for req in component.xpath('requires/*'): if req.tag == 'firmware': text = _node_validate_text(req, minlen=3, maxlen=1000, allow_none=True) rq = Requirement(kind=req.tag, value=text, compare=req.get('compare'), version=req.get('version'), depth=req.get('depth', None)) md.requirements.append(rq) elif req.tag == 'id': text = _node_validate_text(req, minlen=3, maxlen=1000) rq = Requirement(kind=req.tag, value=text, compare=req.get('compare'), version=req.get('version')) md.requirements.append(rq) if text == 'org.freedesktop.fwupd': self.fwupd_min_version = req.get('version') elif req.tag == 'hardware': text = _node_validate_text(req, minlen=3, maxlen=1000) for req_value in text.split('|'): rq = Requirement(kind=req.tag, value=req_value, compare=req.get('compare'), version=req.get('version')) md.requirements.append(rq) else: raise MetadataInvalid('<{}> requirement was invalid'.format( req.tag)) # from the first screenshot try: md.screenshot_caption = _node_validate_text( component.xpath('screenshots/screenshot/caption')[0], minlen=8, maxlen=1000, nourl=True) except IndexError as _: pass try: md.screenshot_url = _node_validate_text( component.xpath('screenshots/screenshot/image')[0], minlen=8, maxlen=1000) except IndexError as _: pass # allows OEM to hide the direct download link on the LVFS if component.xpath('custom/value[@key="LVFS::InhibitDownload"]'): md.inhibit_download = True # allows OEM to disable ignore all kinds of statistics on this firmware if component.xpath('custom/value[@key="LVFS::DoNotTrack"]'): md.fw.do_not_track = True # allows OEM to change the triplet (AA.BB.CCDD) to quad (AA.BB.CC.DD) try: version_format = _node_validate_text( component.xpath('custom/value[@key="LVFS::VersionFormat"]')[0]) if not self.version_formats: raise MetadataInvalid( 'Valid version formats have not been added') if version_format not in self.version_formats: raise MetadataInvalid('LVFS::VersionFormat can only be {}'.\ format(','.join(self.version_formats.keys()))) md.verfmt = self.version_formats[version_format] except IndexError as _: pass # enforce the VersionFormat if the version is an integer if self.is_strict and md.version: if md.version.isdigit() and not md.version_format: raise MetadataInvalid( 'LVFS::VersionFormat is required for integer version') # allows OEM to specify protocol try: text = _node_validate_text( component.xpath('custom/value[@key="LVFS::UpdateProtocol"]') [0]) if text not in self.protocol_map: raise MetadataInvalid( 'No valid UpdateProtocol {} found'.format(text)) md.protocol_id = self.protocol_map[text] except IndexError as _: pass # allows OEM to set banned country codes try: text = _node_validate_text(component.xpath( 'custom/value[@key="LVFS::BannedCountryCodes"]')[0], minlen=2, maxlen=1000, nourl=True) self.fw.banned_country_codes = text except IndexError as _: pass # should we parse the .inf file? try: text = _node_validate_text(component.xpath( 'custom/value[@key="LVFS::EnableInfParsing"]')[0], minlen=2, maxlen=10, nourl=True) if text == 'true': self.enable_inf_parsing = True elif text == 'false': self.enable_inf_parsing = False else: raise MetadataInvalid( 'LVFS::EnableInfParsing only allowed true or false, got {}' .format(text)) except IndexError as _: pass # allows OEM to specify category for category in component.xpath('categories/category'): text = _node_validate_text(category, minlen=8, maxlen=50, nourl=True) if text in self.category_map: md.category_id = self.category_map[text] break # parse the default (first) release try: default_release = component.xpath('releases/release')[0] except IndexError as _: raise MetadataInvalid( 'The metadata file did not provide any releases') self._parse_release(md, default_release) # ensure the update description does not refer to a file in the archive if md.release_description: for word in md.release_description.split(' '): if word.find('.') == -1: # any word without a dot is not a fn continue if word in self.cabarchive_upload: raise MetadataInvalid( 'The release description should not reference other files.' ) # check the inf file matches up with the .xml file if self._version_inf and self._version_inf != md.version: raise MetadataInvalid( 'The inf Firmware_AddReg[HKR->FirmwareVersion] ' '%s did not match the metainfo.xml value %s.' % (self._version_inf, md.version)) # success return md def _parse_metainfo(self, cabfile): # check the file does not have any missing request.form if cabfile.buf.decode('utf-8', 'ignore').find('FIXME') != -1: raise MetadataInvalid( 'The metadata file was not complete; ' 'Any FIXME text must be replaced with the correct values.') # has UTF-8 BOM: https://en.wikipedia.org/wiki/Byte_order_mark if cabfile.buf.startswith(b'\xEF\xBB\xBF'): raise MetadataInvalid( 'The metadata file has a UTF-8 BOM that must be removed') # add to the archive self.cabarchive_repacked[cabfile.filename] = cabfile # parse MetaInfo file try: components = ET.fromstring(cabfile.buf).xpath('/component') if not components: raise MetadataInvalid('<component> tag missing') if len(components) > 1: raise MetadataInvalid('Multiple <component> tags') except UnicodeDecodeError as e: raise MetadataInvalid( 'The metadata file could not be parsed: {}'.format(str(e))) except ET.XMLSyntaxError as e: raise MetadataInvalid( 'The metadata file could not be parsed: {}'.format(str(e))) md = self._parse_component(components[0]) md.release_download_size = self._data_size # add the firmware.bin to the archive try: cabfile_fw = self.cabarchive_upload[md.filename_contents] except KeyError as _: raise MetadataInvalid('No {} found in the archive'.format( md.filename_contents)) self.cabarchive_repacked[cabfile_fw.filename] = cabfile_fw md.checksum_contents = hashlib.sha1(cabfile_fw.buf).hexdigest() md.release_installed_size = len(cabfile_fw.buf) self.fw.mds.append(md) def parse(self, filename, data, use_hashed_prefix=True): # check size self._data_size = len(data) if self._data_size > 104857600: raise FileTooLarge('File too large, limit is 100Mb') if self._data_size < 1024: raise FileTooSmall('File too small, minimum is 1k') # get new filename self.fw.checksum_upload_sha1 = hashlib.sha1(data).hexdigest() self.fw.checksum_upload_sha256 = hashlib.sha256(data).hexdigest() if use_hashed_prefix: self.fw.filename = self.fw.checksum_upload_sha256 + '-' + filename.replace( '.zip', '.cab') else: self.fw.filename = filename.replace('.zip', '.cab') # parse the file try: if filename.endswith('.cab'): self.cabarchive_upload = CabArchive(data, flattern=True) else: self.cabarchive_upload = _repackage_archive(filename, data) except NotImplementedError as e: raise FileNotSupported('Invalid file type: %s' % str(e)) # load metainfo files cabfiles = [ cabfile for cabfile in self.cabarchive_upload.values() if fnmatch.fnmatch(cabfile.filename, '*.metainfo.xml') ] if not cabfiles: raise MetadataInvalid( 'The firmware file had no .metainfo.xml files') # parse each MetaInfo file for cabfile in cabfiles: self._parse_metainfo(cabfile) # verify .inf files if they exists inffiles = [ cabfile for cabfile in self.cabarchive_upload.values() if fnmatch.fnmatch(cabfile.filename, '*.inf') ] for cabfile in inffiles: # add to the archive self.cabarchive_repacked[cabfile.filename] = cabfile # parse if self.enable_inf_parsing: encoding = detect_encoding_from_bom(cabfile.buf) self._parse_inf(cabfile.buf.decode(encoding))
class UploadedFile: def __init__(self): """ default public attributes """ self.fw = Firmware() self.fwupd_min_version = '0.8.0' # a guess, but everyone should have this self.version_formats = ['plain', 'pair', 'triplet', 'quad', 'intel-me', 'intel-me2'] self.category_map = {'X-Device' : 1} self.protocol_map = {} # strip out any unlisted files self.cabarchive_repacked = CabArchive() # private self._data_size = 0 self.cabarchive_upload = None self._version_inf = None def _parse_inf(self, contents): # FIXME is banned... if contents.find('FIXME') != -1: raise MetadataInvalid('The inf file was not complete; Any FIXME text must be ' 'replaced with the correct values.') # check .inf file is valid try: cfg = InfParser(contents) except configparser.MissingSectionHeaderError as _: raise MetadataInvalid('The inf file could not be parsed') try: tmp = cfg.get('Version', 'Class') except (configparser.NoOptionError, configparser.NoSectionError) as _: raise MetadataInvalid('The inf file Version:Class was missing') if tmp.lower() != 'firmware': raise MetadataInvalid('The inf file Version:Class was invalid') try: tmp = cfg.get('Version', 'ClassGuid') except configparser.NoOptionError as _: raise MetadataInvalid('The inf file Version:ClassGuid was missing') if tmp.lower() != '{f2e7dd72-6468-4e36-b6f1-6488f42c1b52}': raise MetadataInvalid('The inf file Version:ClassGuid was invalid') try: tmp = cfg.get('Version', 'DriverVer').split(',') if len(tmp) != 2: raise MetadataInvalid('The inf file Version:DriverVer was invalid') self.fw.version_display = tmp[1] except configparser.NoOptionError as _: pass # this is optional, but if supplied must match the version in the XML # -- also note this will not work with multi-firmware .cab files try: self._version_inf = cfg.get('Firmware_AddReg', 'HKR->FirmwareVersion') if self._version_inf.startswith('0x'): self._version_inf = str(int(self._version_inf[2:], 16)) if self._version_inf == '0': self._version_inf = None except (configparser.NoOptionError, configparser.NoSectionError) as _: pass @staticmethod def _parse_release(md, release): # get description try: md.release_description = _node_validate_text(release.xpath('description')[0], maxlen=1000) except AttributeError as _: raise MetadataInvalid('<description> tag not founf') md.install_duration = int(release.get('install_duration', '0')) md.release_urgency = release.get('urgency') # date, falling back to timestamp try: dt = datetime.datetime.strptime(release.get('date'), "%Y-%m-%d") except TypeError as _: md.release_timestamp = int(release.get('timestamp', '0')) else: md.release_timestamp = dt.fromtimestamp(0) if not md.release_timestamp: raise MetadataInvalid('<release> had no date attribute') # get <url type="details"> try: md.details_url = release.xpath('url[@type="details"]')[0].text except IndexError as _: pass # get <url type="source"> try: md.source_url = release.xpath('url[@type="source"]')[0].text except IndexError as _: pass # fix up hex version md.version = release.get('version') if not md.version: raise MetadataInvalid('<release> had no version attribute') if md.version.startswith('0x'): md.version = str(int(md.version[2:], 16)) # ensure there's always a contents filename try: md.filename_contents = release.xpath('checksum[@target="content"]')[0].get('filename') except IndexError as _: pass if not md.filename_contents: md.filename_contents = 'firmware.bin' # ensure there's always a contents filename for csum in release.xpath('checksum[@target="device"]'): if csum.get('kind') == 'sha1': md.device_checksums.append(Checksum(csum.text, 'SHA1')) elif csum.get('kind') == 'sha256': md.device_checksums.append(Checksum(csum.text, 'SHA256')) if not md.filename_contents: md.filename_contents = 'firmware.bin' def _parse_component(self, component): # get priority md = Component() md.priority = int(component.get('priority', '0')) # check type if component.get('type') != 'firmware': raise MetadataInvalid('<component type="firmware"> required') # get <id> try: md.appstream_id = component.xpath('id')[0].text if not md.appstream_id: raise MetadataInvalid('<id> value invalid') for char in md.appstream_id: if char in ['/', '\\']: raise MetadataInvalid('<id> Cannot contain {}'.format(char)) if char not in ['-', '_', '.'] and not char.isalnum(): raise MetadataInvalid('<id> Cannot contain {}'.format(char)) if len(md.appstream_id.split('.')) < 4: raise MetadataInvalid('<id> Should contain at least 4 sections to identify the model') except IndexError as _: raise MetadataInvalid('<id> tag missing') # get <name> try: md.name = _node_validate_text(component.xpath('name')[0]) md.add_keywords_from_string(md.name, priority=3) except IndexError as _: raise MetadataInvalid('<name> tag missing') # get <summary> try: md.summary = _node_validate_text(component.xpath('summary')[0]) md.add_keywords_from_string(md.summary, priority=1) except IndexError as _: raise MetadataInvalid('<summary> tag missing') # get optional <description} try: md.description = _node_validate_text(component.xpath('description')[0], maxlen=1000) except IndexError as _: pass # get <developer_name> try: md.developer_name = _node_validate_text(component.xpath('developer_name')[0]) if md.developer_name == 'LenovoLtd.': md.developer_name = 'Lenovo Ltd.' md.add_keywords_from_string(md.developer_name, priority=10) except IndexError as _: raise MetadataInvalid('<developer_name> tag missing') if md.developer_name.find('@') != -1 or md.developer_name.find('_at_') != -1: raise MetadataInvalid('<developer_name> cannot contain an email address') # get <metadata_license> try: md.metadata_license = component.xpath('metadata_license')[0].text if md.metadata_license not in ['CC0-1.0', 'FSFAP', 'CC-BY-3.0', 'CC-BY-SA-3.0', 'CC-BY-4.0', 'CC-BY-SA-4.0', 'GFDL-1.1', 'GFDL-1.2', 'GFDL-1.3']: raise MetadataInvalid('Invalid <metadata_license> tag of {}'.format(md.metadata_license)) except AttributeError as _: raise MetadataInvalid('<metadata_license> tag') # get <project_license> try: md.project_license = component.xpath('project_license')[0].text except IndexError as _: raise MetadataInvalid('<project_license> tag missing') if not md.project_license: raise MetadataInvalid('<project_license> value invalid') # get <url type="homepage"> try: md.url_homepage = component.xpath('url[@type="homepage"]')[0].text except IndexError as _: raise MetadataInvalid('<url type="homepage"> tag missing') if not md.url_homepage: raise MetadataInvalid('<url type="homepage"> value invalid') # add manually added keywords for keyword in component.xpath('keywords/keyword'): md.add_keywords_from_string(keyword.text, priority=5) # add provides for prov in component.xpath('provides/firmware[@type="flashed"]'): if not _validate_guid(prov.text): raise MetadataInvalid('The GUID {} was invalid.'.format(prov.text)) md.guids.append(Guid(md.component_id, prov.text)) if not md.guids: raise MetadataInvalid('The metadata file did not provide any GUID.') # check the file didn't try to add it's own <require> on vendor-id # to work around the vendor-id security checks in fwupd if component.xpath('requires/firmware[text()="vendor-id"]'): raise MetadataInvalid('Firmware cannot specify vendor-id') # check only recognised requirements are added for req in component.xpath('requires/*'): if req.tag not in ['firmware', 'id', 'hardware']: raise MetadataInvalid('Requirement \'%s\' was invalid' % req.tag) if req.tag == 'id' and req.text == 'org.freedesktop.fwupd': self.fwupd_min_version = req.get('version') if req.tag == 'hardware': req_values = req.text.split('|') else: req_values = [req.text] for req_value in req_values: rq = Requirement(md.component_id, req.tag, req_value, req.get('compare'), req.get('version')) md.requirements.append(rq) # from the first screenshot try: md.screenshot_caption = component.xpath('screenshots/screenshot/caption')[0] md.screenshot_caption = md.screenshot_caption.replace('<p>', '') md.screenshot_caption = md.screenshot_caption.replace('</p>', '') except IndexError as _: pass try: md.screenshot_url = component.xpath('screenshots/screenshot/image')[0] except IndexError as _: pass # allows OEM to hide the direct download link on the LVFS if component.xpath('custom/value[@key="LVFS::InhibitDownload"]'): md.inhibit_download = True # allows OEM to change the triplet (AA.BB.CCDD) to quad (AA.BB.CC.DD) try: md.version_format = component.xpath('custom/value[@key="LVFS::VersionFormat"]')[0].text if md.version_format not in self.version_formats: raise MetadataInvalid('LVFS::VersionFormat can only be %s' % self.version_formats) except IndexError as _: pass # allows OEM to specify protocol try: update_protocol = component.xpath('custom/value[@key="LVFS::UpdateProtocol"]')[0].text if update_protocol not in self.protocol_map: raise MetadataInvalid('No valid UpdateProtocol {} found'.format(update_protocol)) md.protocol_id = self.protocol_map[update_protocol] except IndexError as _: pass # allows OEM to set banned country codes try: self.fw.banned_country_codes = component.xpath('custom/value[@key="LVFS::BannedCountryCodes"]')[0].text except IndexError as _: pass # allows OEM to specify category for category in component.xpath('categories/category'): if category.text in self.category_map: md.category_id = self.category_map[category.text] break # fallback to device -- DROP THIS 2020-01-01 if not md.category_id: md.category_id = self.category_map['X-Device'] # parse the default (first) release try: default_release = component.xpath('releases/release')[0] except IndexError as _: raise MetadataInvalid('The metadata file did not provide any releases') self._parse_release(md, default_release) # ensure the update description does not refer to a file in the archive for word in md.release_description.split(' '): if word.find('.') == -1: # any word without a dot is not a fn continue if word in self.cabarchive_upload: raise MetadataInvalid('The release description should not reference other files.') # check the inf file matches up with the .xml file if self._version_inf and self._version_inf != md.version: raise MetadataInvalid('The inf Firmware_AddReg[HKR->FirmwareVersion] ' '%s did not match the metainfo.xml value %s.' % (self._version_inf, md.version)) # success return md def _parse_metainfo(self, cabfile): # check the file does not have any missing request.form if cabfile.buf.decode('utf-8', 'ignore').find('FIXME') != -1: raise MetadataInvalid('The metadata file was not complete; ' 'Any FIXME text must be replaced with the correct values.') # add to the archive self.cabarchive_repacked[cabfile.filename] = cabfile # parse MetaInfo file try: components = ET.fromstring(cabfile.buf).xpath('/component') if not components: raise MetadataInvalid('<component> tag missing') if len(components) > 1: raise MetadataInvalid('Multiple <component> tags') except UnicodeDecodeError as e: raise MetadataInvalid('The metadata file could not be parsed: {}'.format(str(e))) md = self._parse_component(components[0]) md.release_download_size = self._data_size # add the firmware.bin to the archive try: cabfile_fw = self.cabarchive_upload[md.filename_contents] except KeyError as _: raise MetadataInvalid('No {} found in the archive'.format(md.filename_contents)) self.cabarchive_repacked[cabfile_fw.filename] = cabfile_fw md.checksum_contents = hashlib.sha1(cabfile_fw.buf).hexdigest() md.release_installed_size = len(cabfile_fw.buf) self.fw.mds.append(md) def parse(self, filename, data, use_hashed_prefix=True): # check size self._data_size = len(data) if self._data_size > 104857600: raise FileTooLarge('File too large, limit is 100Mb') if self._data_size < 1024: raise FileTooSmall('File too small, minimum is 1k') # get new filename self.fw.checksum_upload = hashlib.sha1(data).hexdigest() if use_hashed_prefix: self.fw.filename = self.fw.checksum_upload + '-' + filename.replace('.zip', '.cab') else: self.fw.filename = filename.replace('.zip', '.cab') # parse the file try: if filename.endswith('.cab'): self.cabarchive_upload = CabArchive(data, flattern=True) else: self.cabarchive_upload = _repackage_archive(filename, data) except NotImplementedError as e: raise FileNotSupported('Invalid file type: %s' % str(e)) # verify .inf files if they exists inffiles = [cabfile for cabfile in self.cabarchive_upload.values() if fnmatch.fnmatch(cabfile.filename, '*.inf')] for cabfile in inffiles: encoding = detect_encoding_from_bom(cabfile.buf) self._parse_inf(cabfile.buf.decode(encoding)) # load metainfo files cabfiles = [cabfile for cabfile in self.cabarchive_upload.values() if fnmatch.fnmatch(cabfile.filename, '*.metainfo.xml')] if not cabfiles: raise MetadataInvalid('The firmware file had no .metadata.xml files') # parse each MetaInfo file for cabfile in cabfiles: self._parse_metainfo(cabfile)