diff --git a/rpmlint/checks/FilesCheck.py b/rpmlint/checks/FilesCheck.py index cbd32259..5264e575 100644 --- a/rpmlint/checks/FilesCheck.py +++ b/rpmlint/checks/FilesCheck.py @@ -420,625 +420,157 @@ class FilesCheck(AbstractCheck): return (chunk, istext) 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)) + self._check_utf8(pkg) # 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 - - deps = pkg.requires + pkg.prereq - 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) + self.devel_pkg = False + self.deps = pkg.requires + pkg.prereq + self.config_files = pkg.config_files + self.ghost_files = pkg.ghost_files + self.req_names = pkg.req_names + self.lib_package = lib_package_regex.search(pkg.name) + self.is_kernel_package = kernel_package_regex.search(pkg.name) + self.debuginfo_package = debuginfo_package_regex.search(pkg.name) + self.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') + self.perl_dep_error = False + self.python_dep_error = False + self.lib_file = False + self.non_lib_file = None + self.log_files = [] + self.logrotate_file = False + self.debuginfo_srcs = False + self.debuginfo_debugs = False # Prefetch scriptlets, strip quotes from them (#169) - postin = pkg[rpm.RPMTAG_POSTIN] or \ + self.postin = pkg[rpm.RPMTAG_POSTIN] or \ pkg.scriptprog(rpm.RPMTAG_POSTINPROG) - if postin: - postin = quotes_regex.sub('', postin) - postun = pkg[rpm.RPMTAG_POSTUN] or \ + if self.postin: + self.postin = quotes_regex.sub('', self.postin) + self.postun = pkg[rpm.RPMTAG_POSTUN] or \ pkg.scriptprog(rpm.RPMTAG_POSTUNPROG) - if postun: - postun = quotes_regex.sub('', postun) + if self.postun: + self.postun = quotes_regex.sub('', self.postun) # Unique (rdev, inode) combinations - hardlinks = {} + self.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 = {} + self.bindir_exes = {} # All man page 'base' names (without section etc extensions) - man_basenames = set() + self.man_basenames = set() - for f, pkgfile in files.items(): - fpath = Path(f) - 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_devel(pkg) + self._check_nodoc(pkg) + self._check_meta_package(pkg) + self._check_empty_debuginfo(pkg) - self._check_manpage(pkg, f) - self._check_infopage_compressed(pkg, f) + # Iterate over all pkg.files + self._check_files(pkg) - 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) + self._check_log_files_without_logrotate(pkg) + self._check_outside_libdir_files(pkg) + self._check_debuginfo_without_sources(pkg) + self._check_bindir_exes(pkg) - 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) + def _check_utf8(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)) - 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) + def _check_devel(self, pkg): + # Check if the package is a development package + self.devel_pkg = devel_regex.search(pkg.name) - 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 not self.devel_pkg: + for p in pkg.provides: + if devel_regex.search(p[0]): + self.devel_pkg = True + break - 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') and not f.endswith('/.cargo-checksum.json')): - 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) - if fpath.name == 'Makefile.am' and str(fpath.with_suffix('.in')) in files and is_doc: - self.output.add_info('W', pkg, 'makefile-junk', f) + def _check_nodoc(self, pkg): + if not self.lib_package and not pkg.doc_files: + self.output.add_info('W', pkg, 'no-documentation') - 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) + def _check_meta_package(self, pkg): + if pkg.files and self.meta_package_regex.search(pkg.name): + self.output.add_info('W', pkg, 'file-in-meta-package') - deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] - if res and 'logrotate' not 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 'crontabs' not in deps and pkg.name != 'crontabs': - self.output.add_info('E', pkg, 'missing-dependency-to-crontabs', 'for cron script', f) - if f.startswith('/etc/xinet.d/') and 'xinetd' not in deps and pkg.name != 'xinetd': - self.output.add_info('E', pkg, 'missing-dependency-to-xinetd', 'for xinet.d script', f) + def _check_empty_debuginfo(self, pkg): + debuginfo = self.debuginfo_package or self.debugsource_package + if not pkg.files and debuginfo: + self.output.add_info('E', pkg, 'empty-debuginfo-package') - if link != '': - ext = compr_regex.search(link) - if ext and not re.compile(r'\.%s$' % ext.group(1)).search(f): - self.output.add_info('E', pkg, 'compressed-symlink-with-wrong-ext', - f, link) + def _check_log_files_without_logrotate(self, pkg): + if len(self.log_files) and not self.logrotate_file: + self.output.add_info('W', pkg, 'log-files-without-logrotate', sorted(self.log_files)) - perm = mode & 0o7777 - mode_is_exec = mode & 0o111 + def _check_outside_libdir_files(self, pkg): + if self.lib_package and self.lib_file and self.non_lib_file: + self.output.add_info('E', pkg, 'outside-libdir-files', self.non_lib_file) - 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 and - 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) - - # look for man pages - res = man_base_regex.fullmatch(f) - if res: - man_basenames.add(res.group('binary')) - - 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) or - f'perl(:MODULE_COMPAT_{vers})' in deps): - 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 and not f.startswith('/etc/ld.so.conf.d/'): - 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 - - 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) - # compressed docs, eg. info and man files etc - if not self.skipdocs_regex.search(ff) and 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.fullmatch(f) - if res: - man_basenames.add(res.group('binary')) - 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: + def _check_debuginfo_without_sources(self, pkg): + if not self.use_debugsource and self.debuginfo_package and self.debuginfo_debugs and not self.debuginfo_srcs: self.output.add_info('E', pkg, 'debuginfo-without-sources') - for exe, paths in bindir_exes.items(): + def _check_bindir_exes(self, pkg): + for exe, paths in self.bindir_exes.items(): if len(paths) > 1: self.output.add_info('W', pkg, 'duplicate-executable', exe, paths) - if exe not in man_basenames: + if exe not in self.man_basenames: self.output.add_info('W', pkg, 'no-manual-page-for-binary', exe) - def _check_manpage(self, pkg, fname): + def _check_files(self, pkg): + for f, pkgfile in pkg.files.items(): + self._check_file(pkg, f, pkgfile) + + def _check_file(self, pkg, fname, pkgfile): + if log_regex.search(fname): + self.log_files.append(fname) + + self._check_file_manpage(pkg, fname) + self._check_file_infopage_compressed(pkg, fname) + self._check_file_unexpandaed_macro(pkg, fname) + self._check_file_non_standard_uid(pkg, fname, pkgfile) + self._check_file_non_standard_gid(pkg, fname, pkgfile) + self._check_file_kernel_modules(pkg, fname) + self._check_file_dir_or_file(pkg, fname) + self._check_file_non_ghost_in_run(pkg, fname) + self._check_file_systemd_unit_in_etc(pkg, fname) + self._check_file_udev_rule_in_etc(pkg, fname) + self._check_file_tmpfiles_conf_in_etc(pkg, fname) + self._check_file_subdir_in_bin(pkg, fname) + self._check_file_siteperl_in_perl_module(pkg, fname) + self._check_file_backup_file_in_package(pkg, fname) + self._check_file_version_control_internal_file(pkg, fname) + self._check_file_htaccess_file(pkg, fname) + self._check_file_hidden_file_or_dir(pkg, fname) + self._check_file_manifest_in_perl_module(pkg, fname) + self._check_file_info_dir_file(pkg, fname) + self._check_file_makefile_junk(pkg, fname) + self._check_file_logrotate(pkg, fname) + self._check_file_crontab(pkg, fname) + self._check_file_xinetd(pkg, fname) + self._check_file_compressed_symlink(pkg, fname, pkgfile) + self._check_file_hardlink(pkg, fname, pkgfile) + + # normal file check + self._check_file_normal_file(pkg, fname, pkgfile) + # normal dir check + self._check_file_dir(pkg, fname, pkgfile) + # symbolic link check + self._check_file_link(pkg, fname, pkgfile) + + self._check_file_crond(pkg, fname, pkgfile) + + def _check_file_manpage(self, pkg, fname): """ Check if the the manual page is compressed with the compression method stated in the rpmlint configuration (CompressExtension option). @@ -1070,7 +602,7 @@ class FilesCheck(AbstractCheck): if str(filename.parent) != '.': self.output.add_info('E', pkg, 'manual-page-in-subfolder', fname) - def _check_infopage_compressed(self, pkg, fname): + def _check_file_infopage_compressed(self, pkg, fname): """ Check if the the info page is compressed with the compression method stated in the rpmlint configuration (CompressExtension option). @@ -1081,3 +613,704 @@ class FilesCheck(AbstractCheck): not fname.endswith('/info/dir') and not fname.endswith(self.compress_ext): self.output.add_info('W', pkg, 'infopage-not-compressed', self.compress_ext, fname) + + def _check_file_crond(self, pkg, fname, pkgfile): + if not fname.startswith('/etc/cron.d/'): + return + + mode = pkgfile.mode + mode_is_exec = mode & 0o111 + if stat.S_ISLNK(mode): + self.output.add_info('E', pkg, 'symlink-crontab-file', fname) + + if mode_is_exec: + self.output.add_info('E', pkg, 'executable-crontab-file', fname) + + if stat.S_IWGRP & mode or stat.S_IWOTH & mode: + self.output.add_info('E', pkg, 'non-owner-writeable-only-crontab-file', fname) + + def _check_file_unexpandaed_macro(self, pkg, fname): + for match in self.macro_regex.findall(fname): + self.output.add_info('W', pkg, 'unexpanded-macro', fname, match) + + def _check_file_non_standard_uid(self, pkg, fname, pkgfile): + if pkgfile.user not in self.standard_users: + self.output.add_info('W', pkg, 'non-standard-uid', fname, pkgfile.user) + + def _check_file_non_standard_gid(self, pkg, fname, pkgfile): + if pkgfile.group not in self.standard_groups: + self.output.add_info('W', pkg, 'non-standard-gid', fname, pkgfile.group) + + def _check_file_kernel_modules(self, pkg, fname): + if not self.module_rpms_ok and kernel_modules_regex.search(fname) and not \ + self.is_kernel_package: + self.output.add_info('E', pkg, 'kernel-modules-not-in-kernel-packages', fname) + + def _check_file_dir_or_file(self, pkg, fname): + for i in self.disallowed_dirs: + if fname.startswith(i): + self.output.add_info('E', pkg, 'dir-or-file-in-%s' % + '-'.join(i.split('/')[1:]), fname) + + def _check_file_non_ghost_in_run(self, pkg, fname): + if fname.startswith('/run/'): + if fname not in self.ghost_files: + self.output.add_info('W', pkg, 'non-ghost-in-run', fname) + + def _check_file_systemd_unit_in_etc(self, pkg, fname): + if fname.startswith('/etc/systemd/system/'): + self.output.add_info('W', pkg, 'systemd-unit-in-etc', fname) + + def _check_file_udev_rule_in_etc(self, pkg, fname): + if fname.startswith('/etc/udev/rules.d/'): + self.output.add_info('W', pkg, 'udev-rule-in-etc', fname) + + def _check_file_tmpfiles_conf_in_etc(self, pkg, fname): + if fname.startswith('/etc/tmpfiles.d/'): + self.output.add_info('W', pkg, 'tmpfiles-conf-in-etc', fname) + + def _check_file_subdir_in_bin(self, pkg, fname): + if sub_bin_regex.search(fname): + self.output.add_info('E', pkg, 'subdir-in-bin', fname) + + def _check_file_siteperl_in_perl_module(self, pkg, fname): + if '/site_perl/' in fname: + self.output.add_info('W', pkg, 'siteperl-in-perl-module', fname) + + def _check_file_backup_file_in_package(self, pkg, fname): + if backup_regex.search(fname): + self.output.add_info('E', pkg, 'backup-file-in-package', fname) + + def _check_file_version_control_internal_file(self, pkg, fname): + if scm_regex.search(fname): + self.output.add_info('E', pkg, 'version-control-internal-file', fname) + + def _check_file_htaccess_file(self, pkg, fname): + if fname.endswith('/.htaccess'): + self.output.add_info('E', pkg, 'htaccess-file', fname) + + def _check_file_hidden_file_or_dir(self, pkg, fname): + if (hidden_file_regex.search(fname) and + not fname.startswith('/etc/skel/') and + not fname.endswith('/.build-id') and + not fname.endswith('/.cargo-checksum.json')): + self.output.add_info('W', pkg, 'hidden-file-or-dir', fname) + + def _check_file_manifest_in_perl_module(self, pkg, fname): + if manifest_perl_regex.search(fname): + self.output.add_info('W', pkg, 'manifest-in-perl-module', fname) + + def _check_file_info_dir_file(self, pkg, fname): + if fname == '/usr/info/dir' or fname == '/usr/share/info/dir': + self.output.add_info('E', pkg, 'info-dir-file', fname) + + def _check_file_makefile_junk(self, pkg, fname): + fpath = Path(fname) + is_doc = fname in pkg.doc_files + if fpath.name == 'Makefile.am' and str(fpath.with_suffix('.in')) in pkg.files and is_doc: + self.output.add_info('W', pkg, 'makefile-junk', fname) + + def _check_file_logrotate(self, pkg, fname): + res = logrotate_regex.search(fname) + if res: + self.logrotate_file = True + if res.group(1) != pkg.name: + self.output.add_info('E', pkg, 'incoherent-logrotate-file', fname) + + deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] + if res and 'logrotate' not in deps and pkg.name != 'logrotate': + self.output.add_info('E', pkg, 'missing-dependency-to-logrotate', 'for logrotate script', fname) + + def _check_file_crontab(self, pkg, fname): + deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] + if fname.startswith('/etc/cron.') and 'crontabs' not in deps and pkg.name != 'crontabs': + self.output.add_info('E', pkg, 'missing-dependency-to-crontabs', 'for cron script', fname) + + def _check_file_xinetd(self, pkg, fname): + deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] + if fname.startswith('/etc/xinet.d/') and 'xinetd' not in deps and pkg.name != 'xinetd': + self.output.add_info('E', pkg, 'missing-dependency-to-xinetd', 'for xinet.d script', fname) + + def _check_file_compressed_symlink(self, pkg, fname, pkgfile): + link = pkgfile.linkto + if link != '': + ext = compr_regex.search(link) + if ext and not re.compile(r'\.%s$' % ext.group(1)).search(fname): + self.output.add_info('E', pkg, 'compressed-symlink-with-wrong-ext', + fname, link) + + def _check_file_hardlink(self, pkg, fname, pkgfile): + rdev = pkgfile.rdev + inode = pkgfile.inode + + # Hardlink check + for hardlink in self.hardlinks.get((rdev, inode), ()): + if Path(hardlink).parent != Path(fname).parent: + self.output.add_info('W', pkg, 'cross-directory-hard-link', fname, hardlink) + self.hardlinks.setdefault((rdev, inode), []).append(fname) + + def _check_file_link_devel(self, pkg, fname, pkgfile): + is_so = sofile_regex.search(fname) + if not self.devel_pkg and is_so and not pkgfile.linkto.endswith('.so'): + self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', fname) + + def _check_file_link_man(self, pkg, fname): + res = man_base_regex.fullmatch(fname) + if res: + self.man_basenames.add(res.group('binary')) + + def _check_file_link_bindir_exes(self, pkg, fname): + res = bin_regex.search(fname) + if res: + exe = res.group(1) + if '/' not in exe: + self.bindir_exes.setdefault(exe, []) + + def _check_file_link_absolute(self, pkg, fname, pkgfile): + link = pkgfile.linkto + + # absolute link + r = absolute_regex.search(link) + if not r: + return + + is_so = sofile_regex.search(fname) + if not is_so and link not in pkg.files and \ + link not in self.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 self.req_names: + self.output.add_info('W', pkg, 'no-dependency-on', + is_exception) + else: + self.output.add_info('W', pkg, 'dangling-symlink', fname, link) + linktop = r.group(1) + + r = absolute_regex.search(fname) + if r: + filetop = r.group(1) + if filetop == linktop or self.use_relative_symlinks: + self.output.add_info('W', pkg, 'symlink-should-be-relative', + fname, link) + + def _check_file_link_relative(self, pkg, fname, pkgfile): + link = pkgfile.linkto + + # relative link + r = absolute_regex.search(link) + if r: + return + + is_so = sofile_regex.search(fname) + if not is_so: + abslink = '%s/%s' % (Path(fname).parent, link) + abslink = os.path.normpath(abslink) + if abslink not in pkg.files and abslink not in self.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 self.req_names: + self.output.add_info('W', pkg, 'no-dependency-on', + is_exception) + else: + self.output.add_info('W', pkg, 'dangling-relative-symlink', + fname, link) + pathcomponents = fname.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', + fname, 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', fname, + 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', + fname, link) + + def _check_file_link(self, pkg, fname, pkgfile): + if not stat.S_ISLNK(pkgfile.mode): + return + + self._check_file_link_devel(pkg, fname, pkgfile) + self._check_file_link_man(pkg, fname) + self._check_file_link_bindir_exes(pkg, fname) + self._check_file_link_absolute(pkg, fname, pkgfile) + self._check_file_link_relative(pkg, fname, pkgfile) + + def _check_file_dir(self, pkg, fname, pkgfile): + if not stat.S_ISDIR(pkgfile.mode): + return + + mode = pkgfile.mode + perm = mode & 0o7777 + if mode & 0o1002 == 2: # world writable w/o sticky bit + self.output.add_info('E', pkg, 'world-writable', fname, '%o' % perm) + if perm != 0o755: + self.output.add_info('E', pkg, 'non-standard-dir-perm', fname, '%o' % perm) + if pkg.name not in filesys_packages and fname in STANDARD_DIRS: + self.output.add_info('E', pkg, 'standard-dir-owned-by-package', fname) + if hidden_file_regex.search(fname) and not fname.endswith('/.build-id'): + self.output.add_info('W', pkg, 'hidden-file-or-dir', fname) + + def _check_file_normal_file(self, pkg, fname, pkgfile): + if not stat.S_ISREG(pkgfile.mode): + return + + self._file_nonexec_file = False + self._file_chunk = None + self._file_istext = False + self._file_interpreter = None + self._file_interpreter_args = None + self._file_is_buildconfig = False + + # set[ug]id bit check + self._check_file_normal_file_setui_bit_check(pkg, fname, pkgfile) + self._check_file_normal_file_libfile(pkg, fname) + self._check_file_normal_file_logfile(pkg, fname, pkgfile) + # Fill class attributes, chunk, istext, interpreter, is_buildconfig + self._check_file_normal_file_getdata(pkg, fname, pkgfile) + self._check_file_normal_file_doc(pkg, fname) + self._check_file_normal_file_non_devel(pkg, fname) + self._check_file_normal_file_lib(pkg, fname, pkgfile) + self._check_file_normal_file_depmod_call(pkg, fname) + self._check_file_normal_file_install_info(pkg, fname) + self._check_file_normal_file_perl_temp(pkg, fname) + self._check_file_normal_file_rpaths_in_buildconfig(pkg, fname) + # look for man pages + self._check_file_normal_file_look_for_man(pkg, fname) + self._check_file_normal_file_bin(pkg, fname, pkgfile) + self._check_file_normal_file_devel(pkg, fname, pkgfile) + self._check_file_normal_file_non_readable(pkg, fname, pkgfile) + self._check_file_normal_file_zero_length(pkg, fname, pkgfile) + self._check_file_normal_file_world_w(pkg, fname, pkgfile) + self._check_file_normal_file_perl_dep(pkg, fname) + self._check_file_normal_file_python_dep(pkg, fname) + self._check_file_normal_file_python_source(pkg, fname) + # normal executable check + self._check_file_normal_file_exec(pkg, fname, pkgfile) + self._check_file_normal_file_non_conf_in_etc(pkg, fname) + self._check_file_normal_file_python_noarch(pkg, fname) + self._check_file_normal_file_debuginfo(pkg, fname) + self._check_file_normal_file_gzipped_svg(pkg, fname) + self._check_file_normal_file_pem(pkg, fname) + self._check_file_normal_file_tcl(pkg, fname) + # text file checks + self._check_file_normal_file_text(pkg, fname, pkgfile) + self._check_file_normal_file_not_utf8(pkg, fname, pkgfile) + + def _check_file_normal_file_setui_bit_check(self, pkg, fname, pkgfile): + user = pkgfile.user + group = pkgfile.group + mode = pkgfile.mode + perm = mode & 0o7777 + if stat.S_ISGID & mode or stat.S_ISUID & mode: + if stat.S_ISUID & mode: + self.output.add_info('E', pkg, 'setuid-binary', fname, user, '%o' % perm) + if (stat.S_ISGID & mode and + not (group == 'games' and + (games_path_regex.search(fname) or + self.games_group_regex.search(pkg[rpm.RPMTAG_GROUP])))): + self.output.add_info('E', pkg, 'setgid-binary', fname, group, + '%o' % perm) + if mode & 0o777 != 0o755: + self.output.add_info('E', pkg, 'non-standard-executable-perm', fname, + '%o' % perm) + + def _check_file_normal_file_libfile(self, pkg, fname): + is_doc = fname in pkg.doc_files + + if not self.devel_pkg: + if lib_path_regex.search(fname): + self.lib_file = True + elif not is_doc: + self.non_lib_file = fname + + def _check_file_normal_file_logfile(self, pkg, fname, pkgfile): + user = pkgfile.user + group = pkgfile.group + + if log_regex.search(fname): + self._file_nonexec_file = True + if user != 'root': + self.output.add_info('E', pkg, 'non-root-user-log-file', fname, user) + if group != 'root': + self.output.add_info('E', pkg, 'non-root-group-log-file', fname, group) + if fname not in self.ghost_files: + self.output.add_info('E', pkg, 'non-ghost-file', fname) + + def _check_file_normal_file_getdata(self, pkg, fname, pkgfile): + 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', fname, e) + else: + if res: + (self._file_chunk, self._file_istext) = self.peek(pkgfile.path, pkg) + + (self._file_interpreter, self._file_interpreter_args) = script_interpreter(self._file_chunk) + self._file_is_buildconfig = self._file_istext and buildconfigfile_regex.search(fname) + + def _check_file_normal_file_doc(self, pkg, fname): + is_doc = fname in pkg.doc_files + if doc_regex.search(fname): + if not self._file_interpreter: + self._file_nonexec_file = True + if not is_doc: + self.output.add_info('E', pkg, 'not-listed-as-documentation', fname) + + def _check_file_normal_file_non_devel(self, pkg, fname): + if self.devel_pkg and fname.endswith('.typelib'): + self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', fname) + + def _check_file_normal_file_lib(self, pkg, fname, pkgfile): + # check ldconfig call in %post and %postun + mode = pkgfile.mode + if lib_regex.search(fname): + if self.devel_pkg and not (sofile_regex.search(fname) and stat.S_ISLNK(mode)): + self.output.add_info('E', pkg, 'non-devel-file-in-devel-package', fname) + if not self.postin: + self.output.add_info('E', pkg, 'library-without-ldconfig-postin', fname) + else: + if not ldconfig_regex.search(self.postin): + self.output.add_info('E', pkg, 'postin-without-ldconfig', fname) + + if not self.postun: + self.output.add_info('E', pkg, 'library-without-ldconfig-postun', fname) + else: + if not ldconfig_regex.search(self.postun): + self.output.add_info('E', pkg, 'postun-without-ldconfig', fname) + + def _check_file_normal_file_depmod_call(self, pkg, fname): + # check depmod call in %post and %postun + res = not self.is_kernel_package and kernel_modules_regex.search(fname) + 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 self.postin or not depmod_regex.search(self.postin): + self.output.add_info('E', pkg, 'module-without-depmod-postin', fname) + # check that we run depmod on the right kernel + elif not kernel_version_regex.search(self.postin): + self.output.add_info('E', pkg, 'postin-with-wrong-depmod', fname) + + if not self.postun or not depmod_regex.search(self.postun): + self.output.add_info('E', pkg, 'module-without-depmod-postun', fname) + # check that we run depmod on the right kernel + elif not kernel_version_regex.search(self.postun): + self.output.add_info('E', pkg, 'postun-with-wrong-depmod', fname) + + def _check_file_normal_file_install_info(self, pkg, fname): + # check install-info call in %post and %postun + if fname.startswith('/usr/share/info/'): + if not self.postin: + self.output.add_info('E', pkg, + 'info-files-without-install-info-postin', fname) + elif not install_info_regex.search(self.postin): + self.output.add_info('E', pkg, 'postin-without-install-info', fname) + + preun = pkg[rpm.RPMTAG_PREUN] or \ + pkg.scriptprog(rpm.RPMTAG_PREUNPROG) + if not self.postun and not preun: + self.output.add_info('E', pkg, + 'info-files-without-install-info-postun', fname) + elif not ((self.postun and install_info_regex.search(self.postun)) or + (preun and install_info_regex.search(preun))): + self.output.add_info('E', pkg, 'postin-without-install-info', fname) + + def _check_file_normal_file_perl_temp(self, pkg, fname): + # check perl temp file + if perl_temp_file_regex.search(fname): + self.output.add_info('W', pkg, 'perl-temp-file', fname) + + def _check_file_normal_file_rpaths_in_buildconfig(self, pkg, fname): + # check rpaths in buildconfig files + if self._file_is_buildconfig: + ln = pkg.grep(buildconfig_rpath_regex, fname) + if ln: + self.output.add_info('E', pkg, 'rpath-in-buildconfig', fname, 'lines', ln) + + def _check_file_normal_file_look_for_man(self, pkg, fname): + res = man_base_regex.fullmatch(fname) + if res: + self.man_basenames.add(res.group('binary')) + + def _check_file_normal_file_bin(self, pkg, fname, pkgfile): + mode = pkgfile.mode + perm = mode & 0o7777 + mode_is_exec = mode & 0o111 + res = bin_regex.search(fname) + if res: + if not mode_is_exec: + self.output.add_info('W', pkg, 'non-executable-in-bin', fname, + '%o' % perm) + else: + exe = res.group(1) + if '/' not in exe: + self.bindir_exes.setdefault(exe, []).append(fname) + + def _check_file_normal_file_devel(self, pkg, fname, pkgfile): + is_doc = fname in pkg.doc_files + if (not self.devel_pkg and not is_doc and + (self._file_is_buildconfig or includefile_regex.search(fname) or + develfile_regex.search(fname))): + self.output.add_info('W', pkg, 'devel-file-in-non-devel-package', fname) + + def _check_file_normal_file_non_readable(self, pkg, fname, pkgfile): + mode = pkgfile.mode + perm = mode & 0o7777 + if mode & 0o444 != 0o444 and perm & 0o7000 == 0: + ok_nonreadable = False + for regex in non_readable_regexs: + if regex.search(fname): + ok_nonreadable = True + break + if not ok_nonreadable: + self.output.add_info('E', pkg, 'non-readable', fname, '%o' % perm) + + def _check_file_normal_file_zero_length(self, pkg, fname, pkgfile): + size = pkgfile.size + if size == 0 and not normal_zero_length_regex.search(fname) and \ + fname not in self.ghost_files: + self.output.add_info('E', pkg, 'zero-length', fname) + + def _check_file_normal_file_world_w(self, pkg, fname, pkgfile): + mode = pkgfile.mode + perm = mode & 0o7777 + if mode & stat.S_IWOTH: + self.output.add_info('E', pkg, 'world-writable', fname, '%o' % perm) + + def _check_file_normal_file_perl_dep(self, pkg, fname): + if not self.perl_dep_error: + res = perl_regex.search(fname) + if res: + deps = [x[0] for x in pkg.requires + pkg.recommends + pkg.suggests] + 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) or + f'perl(:MODULE_COMPAT_{vers})' in deps): + self.output.add_info('E', pkg, 'no-dependency-on', + 'perl-base', vers) + self.perl_dep_error = True + + def _check_file_normal_file_python_dep(self, pkg, fname): + if not self.python_dep_error: + res = python_regex.search(fname) + 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)) + self.python_dep_error = True + + def _check_file_normal_file_python_source(self, pkg, fname): + source_file = python_bytecode_to_script(fname) + if not source_file: + return + + if source_file in pkg.files: + if self._file_chunk: + # Verify that the magic ABI value embedded in the + # .pyc header is correct + found_magic = pyc_magic_from_chunk(self._file_chunk) + exp_magic, exp_version = get_expected_pyc_magic(fname, 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', + fname, '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(self._file_chunk) + # If it's a symlink, check target file mtime. + srcfile = pkg.readlink(pkg.files[source_file]) + if not srcfile: + self.output.add_info('W', pkg, 'python-bytecode-without-source', fname) + 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', + fname, cts, srcfile.name, sts) + else: + self.output.add_info('W', pkg, 'python-bytecode-without-source', fname) + + def _check_file_normal_file_exec(self, pkg, fname, pkgfile): + mode = pkgfile.mode + perm = mode & 0o7777 + mode_is_exec = mode & 0o111 + if mode & stat.S_IXUSR and perm != 0o755: + self.output.add_info('E', pkg, 'non-standard-executable-perm', + fname, '%o' % perm) + if mode_is_exec: + if fname in self.config_files: + self.output.add_info('E', pkg, 'executable-marked-as-config-file', fname) + if not self._file_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. + self._file_nonexec_file = fname.endswith('.pc') or \ + compr_regex.search(fname) or \ + includefile_regex.search(fname) or \ + develfile_regex.search(fname) or \ + logrotate_regex.search(fname) + if self._file_nonexec_file: + self.output.add_info('W', pkg, 'spurious-executable-perm', fname) + + def _check_file_normal_file_non_conf_in_etc(self, pkg, fname): + if fname.startswith('/etc/') and fname not in self.config_files and \ + fname not in self.ghost_files and not fname.startswith('/etc/ld.so.conf.d/'): + self.output.add_info('W', pkg, 'non-conffile-in-etc', fname) + + def _check_file_normal_file_python_noarch(self, pkg, fname): + if pkg.arch == 'noarch' and fname.startswith('/usr/lib64/python'): + self.output.add_info('E', pkg, 'noarch-python-in-64bit-path', fname) + + def _check_file_normal_file_debuginfo(self, pkg, fname): + if self.debuginfo_package: + if fname.endswith('.debug'): + self.debuginfo_debugs = True + else: + self.debuginfo_srcs = True + + def _check_file_normal_file_gzipped_svg(self, pkg, fname): + if fname.endswith('.svgz') and fname[0:-1] not in pkg.files \ + and scalable_icon_regex.search(fname): + self.output.add_info('W', pkg, 'gzipped-svg-icon', fname) + + def _check_file_normal_file_pem(self, pkg, fname): + if fname.endswith('.pem') and fname not in self.ghost_files: + if pkg.grep(start_certificate_regex, fname): + self.output.add_info('W', pkg, 'pem-certificate', fname) + if pkg.grep(start_private_key_regex, fname): + self.output.add_info('E', pkg, 'pem-private-key', fname) + + def _check_file_normal_file_tcl(self, pkg, fname): + if tcl_regex.search(fname): + self.output.add_info('E', pkg, 'tcl-extension-file', fname) + + def _check_file_normal_file_text(self, pkg, fname, pkgfile): + if not self._file_istext: + return + + mode = pkgfile.mode + perm = mode & 0o7777 + mode_is_exec = mode & 0o111 + is_doc = fname in pkg.doc_files + + # ignore perl module shebang -- TODO: disputed... + if fname.endswith('.pm'): + self._file_interpreter = None + # sourced scripts should not be executable + if sourced_script_regex.search(fname): + if self._file_interpreter: + self.output.add_info('E', pkg, + 'sourced-script-with-shebang', fname, + self._file_interpreter, self._file_interpreter_args) + if mode_is_exec: + self.output.add_info('E', pkg, 'executable-sourced-script', + fname, '%o' % perm) + # ...but executed ones should + elif self._file_interpreter or mode_is_exec or script_regex.search(fname): + if self._file_interpreter: + res = interpreter_regex.search(self._file_interpreter) + if (mode_is_exec or script_regex.search(fname)): + if res and res.group(1) == 'env': + self.output.add_info('E', pkg, 'env-script-interpreter', + fname, self._file_interpreter, + self._file_interpreter_args) + elif not res: + self.output.add_info('E', pkg, 'wrong-script-interpreter', + fname, self._file_interpreter, + self._file_interpreter_args) + elif not self._file_nonexec_file and not \ + (lib_path_regex.search(fname) and + fname.endswith('.la')): + self.output.add_info('E', pkg, 'script-without-shebang', fname) + + if not mode_is_exec and not is_doc and \ + self._file_interpreter and self._file_interpreter.startswith('/'): + self.output.add_info('E', pkg, 'non-executable-script', fname, + '%o' % perm, self._file_interpreter, + self._file_interpreter_args) + if b'\r' in self._file_chunk: + self.output.add_info('E', pkg, 'wrong-script-end-of-line-encoding', fname) + elif is_doc and not self.skipdocs_regex.search(fname): + if b'\r' in self._file_chunk: + self.output.add_info('W', pkg, 'wrong-file-end-of-line-encoding', fname) + # 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', fname) + if fsf_license_regex.search(self._file_chunk) and \ + fsf_wrong_address_regex.search(self._file_chunk): + self.output.add_info('E', pkg, 'incorrect-fsf-address', fname) + + def _check_file_normal_file_not_utf8(self, pkg, fname, pkgfile): + is_doc = fname in pkg.doc_files + if not self._file_istext and is_doc and self._file_chunk and compr_regex.search(fname): + ff = compr_regex.sub('', fname) + # compressed docs, eg. info and man files etc + if not self.skipdocs_regex.search(ff) and not is_utf8(pkgfile.path): + self.output.add_info('W', pkg, 'file-not-utf8', fname)