def check_file(self, pkg, filename): root = pkg.dirName() f = root + filename st = getstatusoutput(('desktop-file-validate', f), True) if st[0]: error_printed = False for line in st[1].splitlines(): if 'error: ' in line: self.output.add_info('E', pkg, 'invalid-desktopfile', filename, line.split('error: ')[1]) error_printed = True if not error_printed: self.output.add_info('E', pkg, 'invalid-desktopfile', filename) if not is_utf8(f): self.output.add_info('E', pkg, 'non-utf8-desktopfile', filename) self.parse_desktop_file(pkg, root, f, filename)
def check(self, pkg): for filename in pkg.header[rpm.RPMTAG_FILENAMES] or (): if not is_utf8_bytestr(filename): self.output.add_info('E', pkg, 'filename-not-utf8', byte_to_string(filename)) # Rest of the checks are for binary packages only if pkg.is_source: return files = pkg.files # Check if the package is a development package devel_pkg = devel_regex.search(pkg.name) if not devel_pkg: for p in pkg.provides: if devel_regex.search(p[0]): devel_pkg = True break config_files = pkg.config_files ghost_files = pkg.ghost_files req_names = pkg.req_names lib_package = lib_package_regex.search(pkg.name) is_kernel_package = kernel_package_regex.search(pkg.name) debuginfo_package = debuginfo_package_regex.search(pkg.name) debugsource_package = debugsource_package_regex.search(pkg.name) # report these errors only once perl_dep_error = False python_dep_error = False lib_file = False non_lib_file = None log_files = [] logrotate_file = False debuginfo_srcs = False debuginfo_debugs = False if not lib_package and not pkg.doc_files: self.output.add_info('W', pkg, 'no-documentation') if files: if self.meta_package_regex.search(pkg.name): self.output.add_info('W', pkg, 'file-in-meta-package') elif debuginfo_package or debugsource_package: self.output.add_info('E', pkg, 'empty-debuginfo-package') # Prefetch scriptlets, strip quotes from them (#169) postin = pkg[rpm.RPMTAG_POSTIN] or \ pkg.scriptprog(rpm.RPMTAG_POSTINPROG) if postin: postin = quotes_regex.sub('', postin) postun = pkg[rpm.RPMTAG_POSTUN] or \ pkg.scriptprog(rpm.RPMTAG_POSTUNPROG) if postun: postun = quotes_regex.sub('', postun) # Unique (rdev, inode) combinations hardlinks = {} # All executable files from standard bin dirs (basename => [paths]) # Hack: basenames with empty paths links are symlinks (not subject # to duplicate binary check, but yes for man page existence check) bindir_exes = {} # All man page 'base' names (without section etc extensions) man_basenames = set() for f, pkgfile in files.items(): mode = pkgfile.mode user = pkgfile.user group = pkgfile.group link = pkgfile.linkto size = pkgfile.size rdev = pkgfile.rdev inode = pkgfile.inode is_doc = f in pkg.doc_files nonexec_file = False self._check_manpage_compressed(pkg, f) self._check_infopage_compressed(pkg, f) for match in self.macro_regex.findall(f): self.output.add_info('W', pkg, 'unexpanded-macro', f, match) if user not in self.standard_users: self.output.add_info('W', pkg, 'non-standard-uid', f, user) if group not in self.standard_groups: self.output.add_info('W', pkg, 'non-standard-gid', f, group) if not self.module_rpms_ok and kernel_modules_regex.search(f) and not \ is_kernel_package: self.output.add_info('E', pkg, 'kernel-modules-not-in-kernel-packages', f) for i in self.disallowed_dirs: if f.startswith(i): self.output.add_info('E', pkg, 'dir-or-file-in-%s' % '-'.join(i.split('/')[1:]), f) if f.startswith('/run/'): if f not in ghost_files: self.output.add_info('W', pkg, 'non-ghost-in-run', f) elif f.startswith('/etc/systemd/system/'): self.output.add_info('W', pkg, 'systemd-unit-in-etc', f) elif f.startswith('/etc/udev/rules.d/'): self.output.add_info('W', pkg, 'udev-rule-in-etc', f) elif f.startswith('/etc/tmpfiles.d/'): self.output.add_info('W', pkg, 'tmpfiles-conf-in-etc', f) elif sub_bin_regex.search(f): self.output.add_info('E', pkg, 'subdir-in-bin', f) elif '/site_perl/' in f: self.output.add_info('W', pkg, 'siteperl-in-perl-module', f) if backup_regex.search(f): self.output.add_info('E', pkg, 'backup-file-in-package', f) elif scm_regex.search(f): self.output.add_info('E', pkg, 'version-control-internal-file', f) elif f.endswith('/.htaccess'): self.output.add_info('E', pkg, 'htaccess-file', f) elif hidden_file_regex.search(f) and not f.startswith('/etc/skel/') and not f.endswith('/.build-id'): self.output.add_info('W', pkg, 'hidden-file-or-dir', f) elif manifest_perl_regex.search(f): self.output.add_info('W', pkg, 'manifest-in-perl-module', f) elif f == '/usr/info/dir' or f == '/usr/share/info/dir': self.output.add_info('E', pkg, 'info-dir-file', f) elif makefile_regex.search(f) and not f.startswith('/usr/share/selinux'): self.output.add_info('E', pkg, 'makefile-junk', f) res = logrotate_regex.search(f) if res: logrotate_file = True if res.group(1) != pkg.name: self.output.add_info('E', pkg, 'incoherent-logrotate-file', f) deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] if res and not ('logrotate' in deps) and pkg.name != 'logrotate': self.output.add_info('E', pkg, 'missing-dependency-to-logrotate', 'for logrotate script', f) if f.startswith('/etc/cron.') \ and not ('cron' in deps) and pkg.name != 'cron': self.output.add_info('E', pkg, 'missing-dependency-to-cron', 'for cron script', f) if f.startswith('/etc/xinet.d/') \ and not ('xinetd' in deps) and pkg.name != 'xinetd': self.output.add_info('E', pkg, 'missing-dependency-to-xinetd', 'for xinet.d script', f) if link != '': ext = compr_regex.search(link) if ext: if not re.compile(r'\.%s$' % ext.group(1)).search(f): self.output.add_info('E', pkg, 'compressed-symlink-with-wrong-ext', f, link) perm = mode & 0o7777 mode_is_exec = mode & 0o111 if log_regex.search(f): log_files.append(f) # Hardlink check for hardlink in hardlinks.get((rdev, inode), ()): if Path(hardlink).parent != Path(f).parent: self.output.add_info('W', pkg, 'cross-directory-hard-link', f, hardlink) hardlinks.setdefault((rdev, inode), []).append(f) # normal file check if stat.S_ISREG(mode): # set[ug]id bit check if stat.S_ISGID & mode or stat.S_ISUID & mode: if stat.S_ISUID & mode: self.output.add_info('E', pkg, 'setuid-binary', f, user, '%o' % perm) if stat.S_ISGID & mode: if not (group == 'games' and (games_path_regex.search(f) or self.games_group_regex.search( pkg[rpm.RPMTAG_GROUP]))): self.output.add_info('E', pkg, 'setgid-binary', f, group, '%o' % perm) if mode & 0o777 != 0o755: self.output.add_info('E', pkg, 'non-standard-executable-perm', f, '%o' % perm) if not devel_pkg: if lib_path_regex.search(f): lib_file = True elif not is_doc: non_lib_file = f if log_regex.search(f): nonexec_file = True if user != 'root': self.output.add_info('E', pkg, 'non-root-user-log-file', f, user) if group != 'root': self.output.add_info('E', pkg, 'non-root-group-log-file', f, group) if f not in ghost_files: self.output.add_info('E', pkg, 'non-ghost-file', f) chunk = None istext = False res = None try: res = os.access(pkgfile.path, os.R_OK) except UnicodeError as e: # e.g. non-ASCII, C locale, python 3 self.output.add_info('W', pkg, 'inaccessible-filename', f, e) else: if res: (chunk, istext) = self.peek(pkgfile.path, pkg) (interpreter, interpreter_args) = script_interpreter(chunk) if doc_regex.search(f): if not interpreter: nonexec_file = True if not is_doc: self.output.add_info('E', pkg, 'not-listed-as-documentation', f) if devel_pkg and f.endswith('.typelib'): self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', f) # check ldconfig call in %post and %postun if lib_regex.search(f): if devel_pkg and not (sofile_regex.search(f) and stat.S_ISLNK(mode)): self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', f) if not postin: self.output.add_info('E', pkg, 'library-without-ldconfig-postin', f) else: if not ldconfig_regex.search(postin): self.output.add_info('E', pkg, 'postin-without-ldconfig', f) if not postun: self.output.add_info('E', pkg, 'library-without-ldconfig-postun', f) else: if not ldconfig_regex.search(postun): self.output.add_info('E', pkg, 'postun-without-ldconfig', f) # check depmod call in %post and %postun res = not is_kernel_package and kernel_modules_regex.search(f) if res: kernel_version = res.group(1) kernel_version_regex = re.compile( r'\bdepmod\s+-a.*F\s+/boot/System\.map-' + re.escape(kernel_version) + r'\b.*\b' + re.escape(kernel_version) + r'\b', re.MULTILINE | re.DOTALL) if not postin or not depmod_regex.search(postin): self.output.add_info('E', pkg, 'module-without-depmod-postin', f) # check that we run depmod on the right kernel elif not kernel_version_regex.search(postin): self.output.add_info('E', pkg, 'postin-with-wrong-depmod', f) if not postun or not depmod_regex.search(postun): self.output.add_info('E', pkg, 'module-without-depmod-postun', f) # check that we run depmod on the right kernel elif not kernel_version_regex.search(postun): self.output.add_info('E', pkg, 'postun-with-wrong-depmod', f) # check install-info call in %post and %postun if f.startswith('/usr/share/info/'): if not postin: self.output.add_info('E', pkg, 'info-files-without-install-info-postin', f) elif not install_info_regex.search(postin): self.output.add_info('E', pkg, 'postin-without-install-info', f) preun = pkg[rpm.RPMTAG_PREUN] or \ pkg.scriptprog(rpm.RPMTAG_PREUNPROG) if not postun and not preun: self.output.add_info('E', pkg, 'info-files-without-install-info-postun', f) elif not ((postun and install_info_regex.search(postun)) or (preun and install_info_regex.search(preun))): self.output.add_info('E', pkg, 'postin-without-install-info', f) # check perl temp file if perl_temp_file_regex.search(f): self.output.add_info('W', pkg, 'perl-temp-file', f) is_buildconfig = istext and buildconfigfile_regex.search(f) # check rpaths in buildconfig files if is_buildconfig: ln = pkg.grep(buildconfig_rpath_regex, f) if ln: self.output.add_info('E', pkg, 'rpath-in-buildconfig', f, 'lines', ln) res = bin_regex.search(f) if res: if not mode_is_exec: self.output.add_info('W', pkg, 'non-executable-in-bin', f, '%o' % perm) else: exe = res.group(1) if '/' not in exe: bindir_exes.setdefault(exe, []).append(f) if (not devel_pkg and not is_doc and (is_buildconfig or includefile_regex.search(f) or develfile_regex.search(f))): self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', f) if mode & 0o444 != 0o444 and perm & 0o7000 == 0: ok_nonreadable = False for regex in non_readable_regexs: if regex.search(f): ok_nonreadable = True break if not ok_nonreadable: self.output.add_info('E', pkg, 'non-readable', f, '%o' % perm) if size == 0 and not normal_zero_length_regex.search(f) and \ f not in ghost_files: self.output.add_info('E', pkg, 'zero-length', f) if mode & stat.S_IWOTH: self.output.add_info('E', pkg, 'world-writable', f, '%o' % perm) if not perl_dep_error: res = perl_regex.search(f) if res: if self.perl_version_trick: vers = res.group(1) + '.' + res.group(2) else: vers = res.group(1) + res.group(2) if not (pkg.check_versioned_dep('perl-base', vers) or pkg.check_versioned_dep('perl', vers)): self.output.add_info('E', pkg, 'no-dependency-on', 'perl-base', vers) perl_dep_error = True if not python_dep_error: res = python_regex.search(f) if (res and not any((pkg.check_versioned_dep(dep, res.group(1)) for dep in ( 'python', 'python-base', 'python(abi)')))): self.output.add_info('E', pkg, 'no-dependency-on', 'python-base', res.group(1)) python_dep_error = True source_file = python_bytecode_to_script(f) if source_file: if source_file in files: if chunk: # Verify that the magic ABI value embedded in the # .pyc header is correct found_magic = pyc_magic_from_chunk(chunk) exp_magic, exp_version = get_expected_pyc_magic(f, self.python_default_version) if exp_magic and found_magic not in exp_magic: found_version = 'unknown' for (pv, pm) in _python_magic_values.items(): if found_magic in pm: found_version = pv break # If expected version was from the file path, # issue # an error, otherwise a warning. msg = (pkg, 'python-bytecode-wrong-magic-value', f, 'expected %s (%s), found %d (%s)' % (' or '.join(map(str, exp_magic)), exp_version or self.python_default_version, found_magic, found_version)) if exp_version is not None: self.output.add_info('E', *msg) else: self.output.add_info('W', *msg) # Verify that the timestamp embedded in the .pyc # header matches the mtime of the .py file: pyc_timestamp = pyc_mtime_from_chunk(chunk) # If it's a symlink, check target file mtime. srcfile = pkg.readlink(files[source_file]) if not srcfile: self.output.add_info('W', pkg, 'python-bytecode-without-source', f) elif (pyc_timestamp is not None and pyc_timestamp != srcfile.mtime): cts = datetime.fromtimestamp( pyc_timestamp).isoformat() sts = datetime.fromtimestamp( srcfile.mtime).isoformat() self.output.add_info('E', pkg, 'python-bytecode-inconsistent-mtime', f, cts, srcfile.name, sts) else: self.output.add_info('W', pkg, 'python-bytecode-without-source', f) # normal executable check if mode & stat.S_IXUSR and perm != 0o755: self.output.add_info('E', pkg, 'non-standard-executable-perm', f, '%o' % perm) if mode_is_exec: if f in config_files: self.output.add_info('E', pkg, 'executable-marked-as-config-file', f) if not nonexec_file: # doc_regex and log_regex checked earlier, no match, # check rest of usual cases here. Sourced scripts have # their own check, so disregard them here. nonexec_file = f.endswith('.pc') or \ compr_regex.search(f) or \ includefile_regex.search(f) or \ develfile_regex.search(f) or \ logrotate_regex.search(f) if nonexec_file: self.output.add_info('W', pkg, 'spurious-executable-perm', f) elif f.startswith('/etc/') and f not in config_files and \ f not in ghost_files: self.output.add_info('W', pkg, 'non-conffile-in-etc', f) if pkg.arch == 'noarch' and f.startswith('/usr/lib64/python'): self.output.add_info('E', pkg, 'noarch-python-in-64bit-path', f) if debuginfo_package: if f.endswith('.debug'): debuginfo_debugs = True else: debuginfo_srcs = True res = man_base_regex.search(f) if res: man_basenames.add(res.group(1)) if chunk: # TODO: sequence based invocation command = subprocess.run( '%s %s | gtbl | groff -mtty-char -Tutf8 ' '-P-c -mandoc -w%s >%s' % (catcmd(f), quote(pkgfile.path), quote(self.man_warn_category), os.devnull), shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=dict(os.environ, LC_ALL='en_US.UTF-8')) for line in command.stdout.decode().split('\n'): res = man_warn_regex.search(line) if not res or man_nowarn_regex.search(line): continue self.output.add_info('W', pkg, 'manual-page-warning', f, line[res.end(1):]) if f.endswith('.svgz') and f[0:-1] not in files \ and scalable_icon_regex.search(f): self.output.add_info('W', pkg, 'gzipped-svg-icon', f) if f.endswith('.pem') and f not in ghost_files: if pkg.grep(start_certificate_regex, f): self.output.add_info('W', pkg, 'pem-certificate', f) if pkg.grep(start_private_key_regex, f): self.output.add_info('E', pkg, 'pem-private-key', f) if tcl_regex.search(f): self.output.add_info('E', pkg, 'tcl-extension-file', f) # text file checks if istext: # ignore perl module shebang -- TODO: disputed... if f.endswith('.pm'): interpreter = None # sourced scripts should not be executable if sourced_script_regex.search(f): if interpreter: self.output.add_info('E', pkg, 'sourced-script-with-shebang', f, interpreter, interpreter_args) if mode_is_exec: self.output.add_info('E', pkg, 'executable-sourced-script', f, '%o' % perm) # ...but executed ones should elif interpreter or mode_is_exec or script_regex.search(f): if interpreter: res = interpreter_regex.search(interpreter) if (mode_is_exec or script_regex.search(f)): if res and res.group(1) == 'env': self.output.add_info('E', pkg, 'env-script-interpreter', f, interpreter, interpreter_args) elif not res: self.output.add_info('E', pkg, 'wrong-script-interpreter', f, interpreter, interpreter_args) elif not nonexec_file and not \ (lib_path_regex.search(f) and f.endswith('.la')): self.output.add_info('E', pkg, 'script-without-shebang', f) if not mode_is_exec and not is_doc and \ interpreter and interpreter.startswith('/'): self.output.add_info('E', pkg, 'non-executable-script', f, '%o' % perm, interpreter, interpreter_args) if b'\r' in chunk: self.output.add_info('E', pkg, 'wrong-script-end-of-line-encoding', f) elif is_doc and not self.skipdocs_regex.search(f): if b'\r' in chunk: self.output.add_info('W', pkg, 'wrong-file-end-of-line-encoding', f) # We check only doc text files for UTF-8-ness; # checking everything may be slow and can generate # lots of unwanted noise. if not is_utf8(pkgfile.path): self.output.add_info('W', pkg, 'file-not-utf8', f) if fsf_license_regex.search(chunk) and \ fsf_wrong_address_regex.search(chunk): self.output.add_info('E', pkg, 'incorrect-fsf-address', f) elif is_doc and chunk and compr_regex.search(f): ff = compr_regex.sub('', f) if not self.skipdocs_regex.search(ff): # compressed docs, eg. info and man files etc if not is_utf8(pkgfile.path): self.output.add_info('W', pkg, 'file-not-utf8', f) # normal dir check elif stat.S_ISDIR(mode): if mode & 0o1002 == 2: # world writable w/o sticky bit self.output.add_info('E', pkg, 'world-writable', f, '%o' % perm) if perm != 0o755: self.output.add_info('E', pkg, 'non-standard-dir-perm', f, '%o' % perm) if pkg.name not in filesys_packages and f in STANDARD_DIRS: self.output.add_info('E', pkg, 'standard-dir-owned-by-package', f) if hidden_file_regex.search(f) and not f.endswith('/.build-id'): self.output.add_info('W', pkg, 'hidden-file-or-dir', f) # symbolic link check elif stat.S_ISLNK(mode): is_so = sofile_regex.search(f) if not devel_pkg and is_so and not link.endswith('.so'): self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', f) res = man_base_regex.search(f) if res: man_basenames.add(res.group(1)) else: res = bin_regex.search(f) if res: exe = res.group(1) if '/' not in exe: bindir_exes.setdefault(exe, []) # absolute link r = absolute_regex.search(link) if r: if not is_so and link not in files and \ link not in req_names: is_exception = False for e in self.dangling_exceptions.values(): if e['path'].search(link): is_exception = e['name'] break if is_exception: if is_exception not in req_names: self.output.add_info('W', pkg, 'no-dependency-on', is_exception) else: self.output.add_info('W', pkg, 'dangling-symlink', f, link) linktop = r.group(1) r = absolute_regex.search(f) if r: filetop = r.group(1) if filetop == linktop or self.use_relative_symlinks: self.output.add_info('W', pkg, 'symlink-should-be-relative', f, link) # relative link else: if not is_so: abslink = '%s/%s' % (Path(f).parent, link) abslink = os.path.normpath(abslink) if abslink not in files and abslink not in req_names: is_exception = False for e in self.dangling_exceptions.values(): if e['path'].search(link): is_exception = e['name'] break if is_exception: if is_exception not in req_names: self.output.add_info('W', pkg, 'no-dependency-on', is_exception) else: self.output.add_info('W', pkg, 'dangling-relative-symlink', f, link) pathcomponents = f.split('/')[1:] r = points_regex.search(link) lastpop = None mylink = None while r: mylink = r.group(1) if len(pathcomponents) == 0: self.output.add_info('E', pkg, 'symlink-has-too-many-up-segments', f, link) break else: lastpop = pathcomponents[0] pathcomponents = pathcomponents[1:] r = points_regex.search(mylink) if mylink and lastpop: r = absolute2_regex.search(mylink) linktop = r.group(1) # does the link go up and then down into the same # directory? # if linktop == lastpop: # self.output.add_info('W', pkg, 'lengthy-symlink', f, link) # have we reached the root directory? if len(pathcomponents) == 0 and linktop != lastpop \ and not self.use_relative_symlinks: # relative link into other toplevel directory self.output.add_info('W', pkg, 'symlink-should-be-absolute', f, link) # check additional segments for mistakes like # `foo/../bar/' for linksegment in mylink.split('/'): if linksegment == '..': self.output.add_info('E', pkg, 'symlink-contains-up-and-down-segments', f, link) if f.startswith('/etc/cron.d/'): if stat.S_ISLNK(mode): self.output.add_info('E', pkg, 'symlink-crontab-file', f) if mode_is_exec: self.output.add_info('E', pkg, 'executable-crontab-file', f) if stat.S_IWGRP & mode or stat.S_IWOTH & mode: self.output.add_info('E', pkg, 'non-owner-writeable-only-crontab-file', f) if len(log_files) and not logrotate_file: self.output.add_info('W', pkg, 'log-files-without-logrotate', sorted(log_files)) if lib_package and lib_file and non_lib_file: self.output.add_info('E', pkg, 'outside-libdir-files', non_lib_file) if not self.use_debugsource and debuginfo_package and debuginfo_debugs and not debuginfo_srcs: self.output.add_info('E', pkg, 'debuginfo-without-sources') for exe, paths in bindir_exes.items(): if len(paths) > 1: self.output.add_info('W', pkg, 'duplicate-executable', exe, paths) if exe not in man_basenames: self.output.add_info('W', pkg, 'no-manual-page-for-binary', exe)
def check_spec(self, pkg): self._spec_file = pkg.name spec_only = isinstance(pkg, Pkg.FakePkg) spec_lines = Pkg.readlines(self._spec_file) patches = {} applied_patches = [] applied_patches_ifarch = [] patches_auto_applied = False source_dir = False buildroot = False configure_linenum = None configure_cmdline = '' mklibname = False is_lib_pkg = False if_depth = 0 ifarch_depth = -1 current_section = 'package' buildroot_clean = {'clean': False, 'install': False} depscript_override = False depgen_disabled = False patch_fuzz_override = False indent_spaces = 0 indent_tabs = 0 section = {} # None == main package current_package = None package_noarch = {} if self._spec_file: if not Pkg.is_utf8(self._spec_file): self.output.add_info('E', pkg, 'non-utf8-spec-file', self._spec_name or self._spec_file) # gather info from spec lines pkg.current_linenum = 0 nbsp = UNICODE_NBSP for line in spec_lines: pkg.current_linenum += 1 char = line.find(nbsp) if char != -1: self.output.add_info('W', pkg, 'non-break-space', 'line %s, char %d' % (pkg.current_linenum, char)) section_marker = False for sec, regex in section_regexs.items(): res = regex.search(line) if res: current_section = sec section_marker = True section[sec] = section.get(sec, 0) + 1 if sec in ('package', 'files'): rest = filelist_regex.sub('', line[res.end() - 1:]) res = pkgname_regex.search(rest) if res: current_package = res.group(1) else: current_package = None break if section_marker: if not is_lib_pkg and lib_package_regex.search(line): is_lib_pkg = True continue if (current_section in Pkg.RPM_SCRIPTLETS + ('prep', 'build') and contains_buildroot(line)): self.output.add_info('W', pkg, 'rpm-buildroot-usage', '%' + current_section, line[:-1].strip()) if make_check_regex.search(line) and current_section not in \ ('check', 'changelog', 'package', 'description'): self.output.add_info('W', pkg, 'make-check-outside-check-section', line[:-1]) if current_section in buildroot_clean and \ not buildroot_clean[current_section] and \ contains_buildroot(line) and rm_regex.search(line): buildroot_clean[current_section] = True if ifarch_regex.search(line): if_depth = if_depth + 1 ifarch_depth = if_depth if if_regex.search(line): if_depth = if_depth + 1 if setup_regex.match(line): if not setup_q_regex.search(line): # Don't warn if there's a -T without -a or -b if setup_t_regex.search(line): if setup_ab_regex.search(line): self.output.add_info('W', pkg, 'setup-not-quiet') else: self.output.add_info('W', pkg, 'setup-not-quiet') if current_section != 'prep': self.output.add_info('W', pkg, 'setup-not-in-prep') elif autopatch_regex.search(line): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autopatch-not-in-prep') else: res = autosetup_regex.search(line) if res: if not autosetup_n_regex.search(res.group(1)): patches_auto_applied = True if current_section != 'prep': self.output.add_info('W', pkg, '%autosetup-not-in-prep') if endif_regex.search(line): if ifarch_depth == if_depth: ifarch_depth = -1 if_depth = if_depth - 1 res = applied_patch_regex.search(line) if res: pnum = res.group(1) or 0 for tmp in applied_patch_p_regex.findall(line) or [pnum]: pnum = int(tmp) applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) else: res = applied_patch_pipe_regex.search(line) if res: pnum = int(res.group(1)) applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) else: res = applied_patch_i_regex.search(line) if res: pnum = int(res.group(1)) applied_patches.append(pnum) if ifarch_depth > 0: applied_patches_ifarch.append(pnum) if not res and not source_dir: res = source_dir_regex.search(line) if res: source_dir = True self.output.add_info('E', pkg, 'use-of-RPM_SOURCE_DIR') if configure_linenum: if configure_cmdline[-1] == '\\': configure_cmdline = configure_cmdline[:-1] + line.strip() else: res = configure_libdir_spec_regex.search(configure_cmdline) if not res: # Hack to get the correct (start of ./configure) line # number displayed: real_linenum = pkg.current_linenum pkg.current_linenum = configure_linenum self.output.add_info('W', pkg, 'configure-without-libdir-spec') pkg.current_linenum = real_linenum elif res.group(1): res = re.match(hardcoded_library_paths, res.group(1)) if res: self.output.add_info('E', pkg, 'hardcoded-library-path', res.group(1), 'in configure options') configure_linenum = None hashPos = line.find('#') if current_section != 'changelog': cfgPos = line.find('./configure') if cfgPos != -1 and (hashPos == -1 or hashPos > cfgPos): # store line where it started configure_linenum = pkg.current_linenum configure_cmdline = line.strip() res = hardcoded_library_path_regex.search(line) if current_section != 'changelog' and res and not \ (biarch_package_regex.match(pkg.name) or self.hardcoded_lib_path_exceptions_regex.search( res.group(1).lstrip())): self.output.add_info('E', pkg, 'hardcoded-library-path', 'in', res.group(1).lstrip()) if '%mklibname' in line: mklibname = True if current_section == 'package': # Would be cleaner to get sources and patches from the # specfile parsed in Python (see below), but we want to # catch %ifarch'd etc ones as well, and also catch these when # the specfile is not parseable. res = patch_regex.search(line) if res: pnum = int(res.group(1) or 0) patches[pnum] = res.group(2) res = obsolete_tags_regex.search(line) if res: self.output.add_info('W', pkg, 'obsolete-tag', res.group(1)) res = buildroot_regex.search(line) if res: buildroot = True if res.group(1).startswith('/'): self.output.add_info('W', pkg, 'hardcoded-path-in-buildroot-tag', res.group(1)) res = buildarch_regex.search(line) if res: if res.group(1) != 'noarch': self.output.add_info('E', pkg, 'buildarch-instead-of-exclusivearch-tag', res.group(1)) else: package_noarch[current_package] = True res = packager_regex.search(line) if res: self.output.add_info('W', pkg, 'hardcoded-packager-tag', res.group(1)) res = prefix_regex.search(line) if res: if not res.group(1).startswith('%'): self.output.add_info('W', pkg, 'hardcoded-prefix-tag', res.group(1)) res = prereq_regex.search(line) if res: self.output.add_info('E', pkg, 'prereq-use', res.group(2)) res = buildprereq_regex.search(line) if res: self.output.add_info('E', pkg, 'buildprereq-use', res.group(1)) if scriptlet_requires_regex.search(line): self.output.add_info('E', pkg, 'broken-syntax-in-scriptlet-requires', line.strip()) res = requires_regex.search(line) if res: reqs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(reqs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Requires: %s' % e) for req in unversioned(reqs): if compop_regex.search(req): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', req) res = provides_regex.search(line) if res: provs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(provs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Provides: %s' % e) for prov in unversioned(provs): if not prov.startswith('/'): self.output.add_info('W', pkg, 'unversioned-explicit-provides', prov) if compop_regex.search(prov): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', prov) res = obsoletes_regex.search(line) if res: obses = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(obses) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Obsoletes: %s' % e) for obs in unversioned(obses): if not obs.startswith('/'): self.output.add_info('W', pkg, 'unversioned-explicit-obsoletes', obs) if compop_regex.search(obs): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', obs) res = conflicts_regex.search(line) if res: confs = Pkg.parse_deps(res.group(1)) e = Pkg.has_forbidden_controlchars(confs) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', 'Conflicts: %s' % e) for conf in unversioned(confs): if compop_regex.search(conf): self.output.add_info('W', pkg, 'comparison-operator-in-deptoken', conf) if current_section == 'changelog': e = Pkg.has_forbidden_controlchars(line) if e: self.output.add_info('E', pkg, 'forbidden-controlchar-found', '%%changelog: %s' % e) for match in self.macro_regex.findall(line): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-%changelog', match) else: if not depscript_override: depscript_override = \ depscript_override_regex.search(line) is not None if not depgen_disabled: depgen_disabled = \ depgen_disable_regex.search(line) is not None if not patch_fuzz_override: patch_fuzz_override = \ patch_fuzz_override_regex.search(line) is not None if current_section == 'files': # TODO: check scriptlets for these too? if package_noarch.get(current_package) or \ (current_package not in package_noarch and package_noarch.get(None)): res = libdir_regex.search(line) if res: pkgname = current_package if pkgname is None: pkgname = '(main package)' self.output.add_info('W', pkg, 'libdir-macro-in-noarch-package', pkgname, line.rstrip()) if not indent_tabs and '\t' in line: indent_tabs = pkg.current_linenum if not indent_spaces and indent_spaces_regex.search(line): indent_spaces = pkg.current_linenum # Check if egrep or fgrep is used if current_section not in \ ('package', 'changelog', 'description', 'files'): greps = deprecated_grep_regex.findall(line) if greps: self.output.add_info('W', pkg, 'deprecated-grep', greps) # If not checking spec file only, we're checking one inside a # SRPM -> skip this check to avoid duplicate warnings (#167) if spec_only and self.valid_groups and \ line.lower().startswith('group:'): group = line[6:].strip() if group not in self.valid_groups: self.output.add_info('W', pkg, 'non-standard-group', group) # Test if there are macros in comments if hashPos != -1 and \ (hashPos == 0 or line[hashPos - 1] in (' ', '\t')): for match in self.macro_regex.findall( line[hashPos + 1:]): res = re.match('%+', match) if len(res.group(0)) % 2: self.output.add_info('W', pkg, 'macro-in-comment', match) # Last line read is not useful after this point pkg.current_linenum = None for sect in (x for x in buildroot_clean if not buildroot_clean[x]): self.output.add_info('W', pkg, 'no-cleaning-of-buildroot', '%' + sect) if not buildroot: self.output.add_info('W', pkg, 'no-buildroot-tag') for sec in ('prep', 'build', 'install', 'clean'): if not section.get(sec): self.output.add_info('W', pkg, 'no-%%%s-section' % sec) for sec in ('changelog',): # prep, build, install, clean, check prevented by rpmbuild 4.4 if section.get(sec, 0) > 1: self.output.add_info('W', pkg, 'more-than-one-%%%s-section' % sec) if is_lib_pkg and not mklibname: self.output.add_info('E', pkg, 'lib-package-without-%mklibname') if depscript_override and not depgen_disabled: self.output.add_info('W', pkg, 'depscript-without-disabling-depgen') if patch_fuzz_override: self.output.add_info('W', pkg, 'patch-fuzz-is-changed') if indent_spaces and indent_tabs: pkg.current_linenum = max(indent_spaces, indent_tabs) self.output.add_info('W', pkg, 'mixed-use-of-spaces-and-tabs', '(spaces: line %d, tab: line %d)' % (indent_spaces, indent_tabs)) pkg.current_linenum = None # process gathered info if not patches_auto_applied: for pnum, pfile in patches.items(): if pnum in applied_patches_ifarch: self.output.add_info('W', pkg, '%ifarch-applied-patch', 'Patch%d:' % pnum, pfile) if pnum not in applied_patches: self.output.add_info('W', pkg, 'patch-not-applied', 'Patch%d:' % pnum, pfile) # Rest of the checks require a real spec file if not self._spec_file: return # We'd like to parse the specfile only once using python bindings, # but it seems errors from rpmlib get logged to stderr and we can't # capture and print them nicely, so we do it once each way :P outcmd = subprocess.run( ('rpm', '-q', '--qf=', '-D', '_sourcedir %s' % Path(self._spec_file).parent, '--specfile', self._spec_file), stdout=subprocess.PIPE) text = outcmd.stdout.decode() if text.endswith('\n'): text = text[:-1] parse_error = False for line in text.splitlines(): # No such file or dir hack: https://bugzilla.redhat.com/487855 if 'No such file or directory' not in line: parse_error = True self.output.add_info('E', pkg, 'specfile-error', line) if not parse_error: # grab sources and patches from parsed spec object to get # them with macros expanded for URL checking spec_obj = None rpm.addMacro('_sourcedir', pkg.dirName()) try: ts = rpm.TransactionSet() spec_obj = ts.parseSpec(str(self._spec_file)) except (ValueError, rpm.error): # errors logged above already pass rpm.delMacro('_sourcedir') if spec_obj: try: # rpm < 4.8.0 sources = spec_obj.sources() except TypeError: # rpm >= 4.8.0 sources = spec_obj.sources for src in sources: (url, num, flags) = src (scheme, netloc) = urlparse(url)[0:2] if flags & 1: # rpmspec.h, rpm.org ticket #123 srctype = 'Source' else: srctype = 'Patch' tag = '%s%s' % (srctype, num) if scheme and netloc: continue elif srctype == 'Source' and tarball_regex.search(url): self.output.add_info('W', pkg, 'invalid-url', '%s:' % tag, url)
def _check_non_utf8_spec_file(self, pkg): """Check if spec file has UTF-8 character encoding.""" if self._spec_file: if not Pkg.is_utf8(self._spec_file): self.output.add_info('E', pkg, 'non-utf8-spec-file', self._spec_name or self._spec_file)