diff --git a/lib/inspec/plugins/fetcher.rb b/lib/inspec/plugins/fetcher.rb index 5fb7adb3f..984378e44 100644 --- a/lib/inspec/plugins/fetcher.rb +++ b/lib/inspec/plugins/fetcher.rb @@ -27,6 +27,66 @@ module Inspec def read(_file) fail "Fetcher #{self} does not implement `read(...)`. This is required." end + + def relative_target + RelFetcher.new(self) + end + end + + class RelFetcher + attr_reader :files + + def initialize(fetcher) + @parent = fetcher + @prefix = get_prefix(fetcher.files) + @files = fetcher.files.find_all { |x| x.start_with? @prefix } + .map { |x| x[@prefix.length..-1] } + end + + def read(file) + @parent.read(@prefix + file) + end + + private + + def get_prefix(files) + return '' if files.empty? + sorted = files.sort_by(&:length) + get_folder_prefix(sorted) + end + + def get_folder_prefix(files, first_iteration = true) + return get_files_prefix(files) if files.length == 1 + pre = files[0] + File::SEPARATOR + rest = files[1..-1] + if rest.all? { |i| i.start_with? pre } + return get_folder_prefix(rest, false) + end + return get_files_prefix(files) if first_iteration + files + end + + def get_files_prefix(files) + return '' if files.empty? + + file = files[0] + bn = File.basename(file) + # no more prefixes + return '' if bn == file + + i = file.rindex(bn) + prefix = file[0..i-1] + + rest = files.find_all { |f| !f.start_with?(prefix) } + return prefix if rest.empty? + + nu_prefix = get_prefix(rest) + return nu_prefix if prefix.start_with? nu_prefix + # edge case: completely different prefixes; retry prefix detection + a = File.dirname(prefix + 'a') + b = File.dirname(nu_prefix + 'b') + get_prefix([a, b]) + end end end end diff --git a/test/unit/fetchers.rb b/test/unit/fetchers.rb index d7e90b521..418eb02fa 100644 --- a/test/unit/fetchers.rb +++ b/test/unit/fetchers.rb @@ -10,3 +10,52 @@ describe Inspec::Fetcher do res.must_be_kind_of Fetchers::Local end end + +describe Inspec::Plugins::RelFetcher do + def fetcher + src_fetcher.expects(:files).returns(in_files).at_least_once + Inspec::Plugins::RelFetcher.new(src_fetcher) + end + + let(:src_fetcher) { mock() } + + IN_AND_OUT = { + [] => [], + %w{file} => %w{file}, + # don't prefix just by filename + %w{file file_a} => %w{file file_a}, + %w{path/file path/file_a} => %w{file file_a}, + %w{path/to/file} => %w{file}, + %w{/path/to/file} => %w{file}, + %w{alice bob} => %w{alice bob}, + # mixed paths + %w{x/a y/b} => %w{x/a y/b}, + %w{/x/a /y/b} => %w{x/a y/b}, + %w{z/x/a z/y/b} => %w{x/a y/b}, + %w{/z/x/a /z/y/b} => %w{x/a y/b}, + # mixed with relative path + %w{a path/to/b} => %w{a path/to/b}, + %w{path/to/b a} => %w{path/to/b a}, + %w{path/to/b path/a} => %w{to/b a}, + %w{path/to/b path/a c} => %w{path/to/b path/a c}, + # mixed with absolute paths + %w{/path/to/b /a} => %w{path/to/b a}, + %w{/path/to/b /path/a} => %w{to/b a}, + %w{/path/to/b /path/a /c} => %w{path/to/b path/a c}, + # mixing absolute and relative paths + %w{path/a /path/b} => %w{path/a /path/b}, + %w{/path/a path/b} => %w{/path/a path/b}, + # extract folder structure buildup + %w{/a /a/b /a/b/c} => %w{c}, + %w{/a /a/b /a/b/c/d/e} => %w{e}, + }.each do |ins, outs| + describe 'empty profile' do + let(:in_files) { ins } + + it 'also has no files' do + fetcher.files.must_equal outs + end + end + end + +end diff --git a/test/unit/fetchers/url_test.rb b/test/unit/fetchers/url_test.rb index a1a37d506..3d7a0772a 100644 --- a/test/unit/fetchers/url_test.rb +++ b/test/unit/fetchers/url_test.rb @@ -32,7 +32,8 @@ describe Fetchers::Url do end it 'must contain all files' do - _(res.files).must_equal ["inspec.yml", "libraries", "libraries/testlib.rb", "controls", "controls/filesystem_spec.rb"] + _(res.files).must_equal %w{inspec.yml libraries libraries/testlib.rb + controls controls/filesystem_spec.rb} end it 'must not read if the file isnt included' do @@ -60,7 +61,8 @@ describe Fetchers::Url do end it 'must contain all files' do - _(res.files).must_equal ["inspec.yml", "controls", "controls/filesystem_spec.rb"] + _(res.files).must_equal %w{inspec.yml libraries libraries/testlib.rb + controls controls/filesystem_spec.rb} end it 'must not read if the file isnt included' do