lib/inspec/file_provider.rb in inspec-1.51.15 vs lib/inspec/file_provider.rb in inspec-1.51.18

- old
+ new

@@ -1,275 +1,275 @@ -# encoding: utf-8 -require 'rubygems/package' -require 'zlib' -require 'zip' - -module Inspec - class FileProvider - def self.for_path(path) - if path.is_a?(Hash) - MockProvider.new(path) - elsif File.directory?(path) - DirProvider.new(path) - elsif File.exist?(path) && path.end_with?('.tar.gz', 'tgz') - TarProvider.new(path) - elsif File.exist?(path) && path.end_with?('.zip') - ZipProvider.new(path) - elsif File.exist?(path) - DirProvider.new(path) - else - raise "No file provider for the provided path: #{path}" - end - end - - def initialize(_path) - end - - # List all files that are offered. - # - # @return [Array[String]] list of file paths that are included - def files - raise "Fetcher #{self} does not implement `files()`. This is required." - end - - # Read the contents of a file. This will typically refer to a text - # file reading a string. - # - # @param _file [String] path of the file to be read - # @return [String] contents of the file described - def read(_file) - raise "#{self} does not implement `read(...)`. This is required." - end - - # Provide a method for reading binary contents from a file. - # It will default to #read if not defined. For most streams that implement - # it, it will be the same. For some special cases, it will add change the - # way in which encoding of the returned data structure is handled. - # Does not work with alias nor alias_method. - def binread(file) - read(file) - end - - def relative_provider - RelativeFileProvider.new(self) - end - end - - class MockProvider < FileProvider - attr_reader :files - def initialize(path) - @data = path[:mock] - @files = @data.keys - end - - def read(file) - @data[file] - end - end - - class DirProvider < FileProvider - attr_reader :files - def initialize(path) - @files = if File.file?(path) - [path] - else - Dir[File.join(Shellwords.shellescape(path), '**', '*')] - end - @path = path - end - - def read(file) - return nil unless files.include?(file) - return nil unless File.file?(file) - File.read(file) - end - - def binread(file) - return nil unless files.include?(file) - return nil unless File.file?(file) - File.binread(file) - end - end - - class ZipProvider < FileProvider - attr_reader :files - - def initialize(path) - @path = path - @contents = {} - @files = [] - walk_zip(@path) do |io| - while (entry = io.get_next_entry) - name = entry.name.sub(%r{/+$}, '') - @files.push(name) unless name.empty? - end - end - end - - def read(file) - @contents[file] ||= read_from_zip(file) - end - - private - - def walk_zip(path, &callback) - ::Zip::InputStream.open(path, &callback) - end - - def read_from_zip(file) - return nil unless @files.include?(file) - res = nil - walk_zip(@path) do |io| - while (entry = io.get_next_entry) - next unless file == entry.name - res = io.read - break - end - end - res - end - end - - class TarProvider < FileProvider - attr_reader :files - - def initialize(path) - @path = path - @contents = {} - @files = [] - walk_tar(@path) do |tar| - @files = tar.find_all(&:file?) - - # delete all entries with no name - @files = @files.find_all { |x| !x.full_name.empty? } - - # delete all entries that have a PaxHeader - @files = @files.delete_if { |x| x.full_name.include?('PaxHeader/') } - - # replace all items of the array simply with the relative filename of the file - @files.map! { |x| Pathname.new(x.full_name).relative_path_from(Pathname.new('.')).to_s } - end - end - - def read(file) - @contents[file] ||= read_from_tar(file) - end - - private - - def walk_tar(path, &callback) - Gem::Package::TarReader.new(Zlib::GzipReader.open(path), &callback) - end - - def read_from_tar(file) - return nil unless @files.include?(file) - res = nil - # NB `TarReader` includes `Enumerable` beginning with Ruby 2.x - walk_tar(@path) do |tar| - tar.each do |entry| - next unless entry.file? && [file, "./#{file}"].include?(entry.full_name) - res = entry.read - break - end - end - res - end - end - - class RelativeFileProvider - BLACKLIST_FILES = [ - '/pax_global_header', - 'pax_global_header', - ].freeze - - attr_reader :files - attr_reader :prefix - attr_reader :parent - - def initialize(parent_provider) - @parent = parent_provider - @prefix = get_prefix(parent.files) - if @prefix.nil? - raise "Could not determine path prefix for #{parent}" - end - - # select all files that begin with the prefix, and strip off the prefix from the file. - # - # strip off any leading top-level relative path (./) which is common in - # PAX-formatted tar files. Do not do any translation of the path if the - # path is an absolute path. - @files = parent.files - .find_all { |x| x.start_with?(prefix) && x != prefix } - .map { |x| x[prefix.length..-1] } - .map do |x| - path = Pathname.new(x) - path.absolute? ? path.to_s : path.relative_path_from(Pathname.new('.')).to_s - end - end - - def abs_path(file) - return nil if file.nil? - prefix + file - end - - def read(file) - parent.read(abs_path(file)) - end - - def binread(file) - parent.binread(abs_path(file)) - end - - private - - def get_prefix(fs) - return '' if fs.empty? - - # filter backlisted files - fs -= BLACKLIST_FILES - - sorted = fs.sort_by(&:length) - get_folder_prefix(sorted) - end - - def prefix_candidate_for(file) - if file.end_with?(File::SEPARATOR) - file - else - file + File::SEPARATOR - end - end - - def get_folder_prefix(fs) - return get_files_prefix(fs) if fs.length == 1 - first, *rest = fs - pre = prefix_candidate_for(first) - - if rest.all? { |i| i.start_with? pre } - return get_folder_prefix(rest) - end - get_files_prefix(fs) - end - - def get_files_prefix(fs) - return '' if fs.empty? - - file = fs[0] - bn = File.basename(file) - # no more prefixes - return '' if bn == file - - i = file.rindex(bn) - pre = file[0..i-1] - - rest = fs.find_all { |f| !f.start_with?(pre) } - return pre if rest.empty? - - new_pre = get_prefix(rest) - return new_pre if pre.start_with? new_pre - # edge case: completely different prefixes; retry prefix detection - a = File.dirname(pre + 'a') - b = File.dirname(new_pre + 'b') - get_prefix([a, b]) - end - end -end +# encoding: utf-8 +require 'rubygems/package' +require 'zlib' +require 'zip' + +module Inspec + class FileProvider + def self.for_path(path) + if path.is_a?(Hash) + MockProvider.new(path) + elsif File.directory?(path) + DirProvider.new(path) + elsif File.exist?(path) && path.end_with?('.tar.gz', 'tgz') + TarProvider.new(path) + elsif File.exist?(path) && path.end_with?('.zip') + ZipProvider.new(path) + elsif File.exist?(path) + DirProvider.new(path) + else + raise "No file provider for the provided path: #{path}" + end + end + + def initialize(_path) + end + + # List all files that are offered. + # + # @return [Array[String]] list of file paths that are included + def files + raise "Fetcher #{self} does not implement `files()`. This is required." + end + + # Read the contents of a file. This will typically refer to a text + # file reading a string. + # + # @param _file [String] path of the file to be read + # @return [String] contents of the file described + def read(_file) + raise "#{self} does not implement `read(...)`. This is required." + end + + # Provide a method for reading binary contents from a file. + # It will default to #read if not defined. For most streams that implement + # it, it will be the same. For some special cases, it will add change the + # way in which encoding of the returned data structure is handled. + # Does not work with alias nor alias_method. + def binread(file) + read(file) + end + + def relative_provider + RelativeFileProvider.new(self) + end + end + + class MockProvider < FileProvider + attr_reader :files + def initialize(path) + @data = path[:mock] + @files = @data.keys + end + + def read(file) + @data[file] + end + end + + class DirProvider < FileProvider + attr_reader :files + def initialize(path) + @files = if File.file?(path) + [path] + else + Dir[File.join(Shellwords.shellescape(path), '**', '*')] + end + @path = path + end + + def read(file) + return nil unless files.include?(file) + return nil unless File.file?(file) + File.read(file) + end + + def binread(file) + return nil unless files.include?(file) + return nil unless File.file?(file) + File.binread(file) + end + end + + class ZipProvider < FileProvider + attr_reader :files + + def initialize(path) + @path = path + @contents = {} + @files = [] + walk_zip(@path) do |io| + while (entry = io.get_next_entry) + name = entry.name.sub(%r{/+$}, '') + @files.push(name) unless name.empty? + end + end + end + + def read(file) + @contents[file] ||= read_from_zip(file) + end + + private + + def walk_zip(path, &callback) + ::Zip::InputStream.open(path, &callback) + end + + def read_from_zip(file) + return nil unless @files.include?(file) + res = nil + walk_zip(@path) do |io| + while (entry = io.get_next_entry) + next unless file == entry.name + res = io.read + break + end + end + res + end + end + + class TarProvider < FileProvider + attr_reader :files + + def initialize(path) + @path = path + @contents = {} + @files = [] + walk_tar(@path) do |tar| + @files = tar.find_all(&:file?) + + # delete all entries with no name + @files = @files.find_all { |x| !x.full_name.empty? } + + # delete all entries that have a PaxHeader + @files = @files.delete_if { |x| x.full_name.include?('PaxHeader/') } + + # replace all items of the array simply with the relative filename of the file + @files.map! { |x| Pathname.new(x.full_name).relative_path_from(Pathname.new('.')).to_s } + end + end + + def read(file) + @contents[file] ||= read_from_tar(file) + end + + private + + def walk_tar(path, &callback) + Gem::Package::TarReader.new(Zlib::GzipReader.open(path), &callback) + end + + def read_from_tar(file) + return nil unless @files.include?(file) + res = nil + # NB `TarReader` includes `Enumerable` beginning with Ruby 2.x + walk_tar(@path) do |tar| + tar.each do |entry| + next unless entry.file? && [file, "./#{file}"].include?(entry.full_name) + res = entry.read + break + end + end + res + end + end + + class RelativeFileProvider + BLACKLIST_FILES = [ + '/pax_global_header', + 'pax_global_header', + ].freeze + + attr_reader :files + attr_reader :prefix + attr_reader :parent + + def initialize(parent_provider) + @parent = parent_provider + @prefix = get_prefix(parent.files) + if @prefix.nil? + raise "Could not determine path prefix for #{parent}" + end + + # select all files that begin with the prefix, and strip off the prefix from the file. + # + # strip off any leading top-level relative path (./) which is common in + # PAX-formatted tar files. Do not do any translation of the path if the + # path is an absolute path. + @files = parent.files + .find_all { |x| x.start_with?(prefix) && x != prefix } + .map { |x| x[prefix.length..-1] } + .map do |x| + path = Pathname.new(x) + path.absolute? ? path.to_s : path.relative_path_from(Pathname.new('.')).to_s + end + end + + def abs_path(file) + return nil if file.nil? + prefix + file + end + + def read(file) + parent.read(abs_path(file)) + end + + def binread(file) + parent.binread(abs_path(file)) + end + + private + + def get_prefix(fs) + return '' if fs.empty? + + # filter backlisted files + fs -= BLACKLIST_FILES + + sorted = fs.sort_by(&:length) + get_folder_prefix(sorted) + end + + def prefix_candidate_for(file) + if file.end_with?(File::SEPARATOR) + file + else + file + File::SEPARATOR + end + end + + def get_folder_prefix(fs) + return get_files_prefix(fs) if fs.length == 1 + first, *rest = fs + pre = prefix_candidate_for(first) + + if rest.all? { |i| i.start_with? pre } + return get_folder_prefix(rest) + end + get_files_prefix(fs) + end + + def get_files_prefix(fs) + return '' if fs.empty? + + file = fs[0] + bn = File.basename(file) + # no more prefixes + return '' if bn == file + + i = file.rindex(bn) + pre = file[0..i-1] + + rest = fs.find_all { |f| !f.start_with?(pre) } + return pre if rest.empty? + + new_pre = get_prefix(rest) + return new_pre if pre.start_with? new_pre + # edge case: completely different prefixes; retry prefix detection + a = File.dirname(pre + 'a') + b = File.dirname(new_pre + 'b') + get_prefix([a, b]) + end + end +end