# Copyright (c) 2013-2016 SUSE LLC # # This program is free software; you can redistribute it and/or # modify it under the terms of version 3 of the GNU General Public License as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, contact SUSE LLC. # # To contact SUSE about this file by physical or electronic mail, # you may find current contact information at www.suse.com class ManagedFilesDatabase class ChangedFile < Machinery::Object attr_accessor :type def initialize(type, attrs) super(attrs) @type = type end def config_file? @type == "c" end end def initialize(system) @system = system end def expected_tag?(character, position) if @rpm_changes[position] == character true else @unknown_tag ||= ![".", "?"].include?(@rpm_changes[position]) false end end def changed_files(&block) return @changed_files if @changed_files check_requirements result = managed_files_list(&block).lines.inject({}) do |hash, line| line.chomp! next(hash) unless line =~ /^[^ ]+[ ]+. \/.*$/ file, changes, type = parse_changes_line(line) unless hash[file] package_name, package_version = package_for_file_path(file) hash[file] = ChangedFile.new( type, name: file, package_name: package_name, package_version: package_version, status: "changed", changes: [] ) end hash[file].changes |= changes hash end.values paths = result.reject { |f| f.changes.include?("deleted") }.map(&:name) path_data = get_path_data(paths) result.each do |pkg| next unless path_data[pkg.name] path_data[pkg.name].each do |key, value| pkg[key] = value end end @changed_files = result end def parse_changes_line(line) # rpm provides lines per config file where first 9 characters indicate which # properties of the file are modified @rpm_changes, *fields = line.split(" ") # nine rpm changes are known @unknown_tag = @rpm_changes.size > 9 # For config or documentation files there's an additional field which # contains "c" or "d" type = fields[0].start_with?("/") ? "" : fields.shift path = fields.join(" ") changes = [] if (@rpm_changes == "........." || @rpm_changes == "missing") && path.end_with?(" (replaced)") changes << "replaced" path.slice!(/ \(replaced\)$/) end if @rpm_changes == "missing" changes << "deleted" else changes << "size" if expected_tag?("S", 0) changes << "mode" if expected_tag?("M", 1) changes << "md5" if expected_tag?("5", 2) changes << "device_number" if expected_tag?("D", 3) changes << "link_path" if expected_tag?("L", 4) changes << "user" if expected_tag?("U", 5) changes << "group" if expected_tag?("G", 6) changes << "time" if expected_tag?("T", 7) changes << "capabilities" if @rpm_changes.size > 8 && expected_tag?("P", 8) end if @unknown_tag changes << "other_rpm_changes" end handle_verify_fail(path) if @rpm_changes.include?("?") [path, changes, type] end def parse_stat_line(line) mode, user, group, uid, gid, type, *path_line = line.split(":") path = path_line.join(":").chomp user = uid if user == "UNKNOWN" group = gid if group == "UNKNOWN" type = case type when "directory" "dir" when "symbolic link" "link" when /file$/ "file" else raise( "The inspection failed because of the unknown type `#{type}` of file `#{path}`." ) end [path, { mode: mode, user: user, group: group, type: type }] end def get_link_target(link) @system.run_command( "find", link, "-prune", "-printf", "%l", stdout: :capture, privileged: true ).strip end # get path data for list of files # cur_files is guaranteed to not exceed max command line length def get_file_properties(cur_files) ret = {} out = @system.run_command( "stat", "--printf", "%a:%U:%G:%u:%g:%F:%n\\n", *cur_files, stdout: :capture, privileged: true ) out.each_line do |l| path, values = parse_stat_line(l) ret[path] = values ret[path][:target] = get_link_target(path) if values[:type] == "link" end ret end def get_path_data(paths) ret = {} path_index = 0 # arbitrary number for maximum command line length that should always work max_len = 50000 cur_files = [] cur_len = 0 while path_index < paths.size if cur_files.empty? || paths[path_index].size + cur_len + 1 < max_len cur_files << paths[path_index] cur_len += paths[path_index].size + 1 path_index += 1 else ret.merge!(get_file_properties(cur_files)) cur_files.clear cur_len = 0 end end ret.merge!(get_file_properties(cur_files)) unless cur_files.empty? ret end def handle_verify_fail(path) message = "Could not perform all tests on rpm changes for file '#{path}'." Machinery.logger.warn(message) Machinery::Ui.warn("Warning: #{message}") end end