class Chef::Util::Diff

Public Instance Methods

diff(old_file, new_file) click to toggle source
# File lib/chef/util/diff.rb, line 75
def diff(old_file, new_file)
  use_tempfile_if_missing(old_file) do |old_file|
    use_tempfile_if_missing(new_file) do |new_file|
      @error = do_diff(old_file, new_file)
    end
  end
end
for_output() click to toggle source

@todo: to_a, to_s, to_json, inspect defs, accessors for @diff and @error @todo: move coercion to UTF-8 into to_json

# File lib/chef/util/diff.rb, line 49
def for_output
  # formatted output to a terminal uses arrays of strings and returns error strings
  @diff.nil? ? [ @error ] : @diff
end
for_reporting() click to toggle source
# File lib/chef/util/diff.rb, line 54
def for_reporting
  # caller needs to ensure that new files aren't posted to resource reporting
  return nil if @diff.nil?

  @diff.join("\\n")
end
udiff(old_file, new_file) click to toggle source

produces a unified-output-format diff with 3 lines of context ChefFS uses udiff() directly

# File lib/chef/util/diff.rb, line 85
def udiff(old_file, new_file)
  require "diff/lcs"
  require "diff/lcs/hunk"

  diff_str = ""
  file_length_difference = 0

  old_data = IO.readlines(old_file).map(&:chomp)
  new_data = IO.readlines(new_file).map(&:chomp)
  diff_data = ::Diff::LCS.diff(old_data, new_data)

  return diff_str if old_data.empty? && new_data.empty?
  return "No differences encountered\n" if diff_data.empty?

  # write diff header (standard unified format)
  ft = File.stat(old_file).mtime.localtime.strftime("%Y-%m-%d %H:%M:%S.%N %z")
  diff_str << "--- #{old_file}\t#{ft}\n"
  ft = File.stat(new_file).mtime.localtime.strftime("%Y-%m-%d %H:%M:%S.%N %z")
  diff_str << "+++ #{new_file}\t#{ft}\n"

  # loop over diff hunks. if a hunk overlaps with the last hunk,
  # join them. otherwise, print out the old one.
  old_hunk = hunk = nil
  diff_data.each do |piece|

    hunk = ::Diff::LCS::Hunk.new(old_data, new_data, piece, 3, file_length_difference)
    file_length_difference = hunk.file_length_difference
    next unless old_hunk
    next if hunk.merge(old_hunk)

    diff_str << old_hunk.diff(:unified) << "\n"
  ensure
    old_hunk = hunk

  end
  diff_str << old_hunk.diff(:unified) << "\n"
  diff_str
end
use_tempfile_if_missing(file) { |file| ... } click to toggle source
# File lib/chef/util/diff.rb, line 61
def use_tempfile_if_missing(file)
  tempfile = nil
  unless TargetIO::File.exist?(file)
    Chef::Log.trace("File #{file} does not exist to diff against, using empty tempfile")
    tempfile = Tempfile.new("chef-diff")
    file = tempfile.path
  end
  yield file
  unless tempfile.nil?
    tempfile.close
    tempfile.unlink
  end
end

Private Instance Methods

do_diff(old_file, new_file) click to toggle source
# File lib/chef/util/diff.rb, line 126
def do_diff(old_file, new_file)
  if Chef::Config[:diff_disabled]
    return "(diff output suppressed by config)"
  end

  diff_filesize_threshold = Chef::Config[:diff_filesize_threshold]
  diff_output_threshold = Chef::Config[:diff_output_threshold]

  # Download files for diffs in Target Mode, then work locally
  if ChefConfig::Config.target_mode?
    connection = Chef.run_context&.transport_connection

    old_copy = Tempfile.new(old_file)
    connection.download(old_file, old_copy.path) if connection.file(old_file).exist?
    old_file = old_copy.path

    new_copy = Tempfile.new(new_file)
    connection.download(new_file, new_copy.path) if connection.file(new_file).exist?
    new_file = new_copy.path
  end

  if ::File.size(old_file) > diff_filesize_threshold || ::File.size(new_file) > diff_filesize_threshold
    return "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)"
  end

  # macOS(BSD?) diff will *sometimes* happily spit out nasty binary diffs
  return "(current file is binary, diff output suppressed)" if is_binary?(old_file)
  return "(new content is binary, diff output suppressed)" if is_binary?(new_file)

  begin
    Chef::Log.trace("Running: diff -u #{old_file} #{new_file}")
    diff_str = udiff(old_file, new_file)

  rescue Exception => e
    # Should *not* receive this, but in some circumstances it seems that
    # an exception can be thrown even using shell_out instead of shell_out!
    return "Could not determine diff. Error: #{e.message}"
  end

  if !diff_str.empty? && diff_str != "No differences encountered\n"
    if diff_str.length > diff_output_threshold
      "(long diff of over #{diff_output_threshold} characters, diff output suppressed)"
    else
      diff_str = encode_diff_for_json(diff_str)
      @diff = diff_str.split("\n")
      "(diff available)"
    end
  else
    "(no diff)"
  end
end
encode_diff_for_json(diff_str) click to toggle source
# File lib/chef/util/diff.rb, line 193
def encode_diff_for_json(diff_str)
  diff_str.encode!("UTF-8", invalid: :replace, undef: :replace, replace: "?")
end
is_binary?(path) click to toggle source
# File lib/chef/util/diff.rb, line 178
def is_binary?(path)
  File.open(path) do |file|
    # XXX: this slurps into RAM, but we should have already checked our diff has a reasonable size
    buff = file.read
    buff = "" if buff.nil?
    begin
      return buff !~ /\A[\s[:print:]]*\z/m
    rescue ArgumentError => e
      return true if /invalid byte sequence/.match?(e.message)

      raise
    end
  end
end