diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj index aaf566b4d4..1fae14160e 100644 --- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj +++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj @@ -99,6 +99,7 @@ + @@ -509,6 +510,45 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs new file mode 100644 index 0000000000..74cd026286 --- /dev/null +++ b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs @@ -0,0 +1,441 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.tests.integration.infrastructure.app.shimtarget +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using chocolatey.infrastructure.app.domain; + using NUnit.Framework; + using Should; + + public class ShimTargetSpecs + { + public abstract class ShimTargetSpecsBase : TinySpec + { + protected string PackagePath; + protected string RootPath; + protected string IncludeFile; + protected string ErrorMessage; + protected IEnumerable Results; + + public override void Context() + { + PackagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "infrastructure.app", "shimtarget", "pkg"); + RootPath = Path.Combine(PackagePath, "tools"); + IncludeFile = Path.Combine(RootPath, ".shiminclude"); + ErrorMessage = string.Empty; + } + + public override void Because() + { + var shimManager = new ShimTargetManager(PackagePath); + Results = shimManager.get_shim_targets(); + } + + protected bool check_results(string[] expected) + { + var targetsExist = true; + ErrorMessage = string.Empty; + + foreach (var path in expected) + { + var target = resolve_from_root(path); + if (Results.Contains(target, StringComparer.OrdinalIgnoreCase)) continue; + + targetsExist = false; + ErrorMessage = "Expected file missing: {0}".format_with(target); + break; + } + + return targetsExist; + } + + protected string resolve_from_root(string relativePath) + { + return Path.GetFullPath(Path.Combine(RootPath, relativePath)); + } + + protected void write_shim_include(string[] content) + { + File.WriteAllLines(IncludeFile, content, Encoding.UTF8); + } + } + + [Category("Integration")] + public class when_shiminclude_is_empty : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + string[] lines = { }; + write_shim_include(lines); + } + + [Fact] + public void no_targets_should_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_shiminclude_contains_blank_and_comment_lines : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + string[] lines = { "# comment", "", "", "#*.exe" }; + write_shim_include(lines); + } + + [Fact] + public void no_targets_should_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_including_a_target_outside_the_package_directory : ShimTargetSpecsBase + { + private string _target; + + public override void Context() + { + base.Context(); + _target = @"..\..\outside.exe"; + string[] lines = { _target }; + write_shim_include(lines); + } + + [Fact] + public void the_target_should_exist() + { + File.Exists(resolve_from_root(_target)).ShouldBeTrue(); + } + + [Fact] + public void the_target_should_not_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_including_a_target_in_the_package_directory : ShimTargetSpecsBase + { + private string _target; + + public override void Context() + { + base.Context(); + _target = @"..\pkg.exe"; + string[] lines = { _target }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_one_target_returned() + { + Results.Count().ShouldEqual(1); + } + + [Fact] + public void the_target_should_be_returned() + { + string[] expected = { _target }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_including_a_target_with_an_absolute_path : ShimTargetSpecsBase + { + private string _target; + + public override void Context() + { + base.Context(); + _target = resolve_from_root(@"prog\sbin\prog.exe"); + string[] lines = { _target }; + write_shim_include(lines); + } + + [Fact] + public void the_target_should_exist() + { + File.Exists(_target).ShouldBeTrue(); + } + + [Fact] + public void the_target_should_not_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_including_a_target_with_an_absolute_path_from_the_current_drive : ShimTargetSpecsBase + { + private string _target; + + public override void Context() + { + base.Context(); + _target = resolve_from_root(@"prog\sbin\prog.exe"); + string[] lines = { _target.Substring(3) }; + write_shim_include(lines); + } + + [Fact] + public void the_target_should_exist() + { + File.Exists(_target).ShouldBeTrue(); + } + + [Fact] + public void the_target_should_not_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_including_a_target_with_an_unsupported_file_extension : ShimTargetSpecsBase + { + private string _target; + + public override void Context() + { + base.Context(); + _target = @"prog\opt\opt3.vbs"; + string[] lines = { _target }; + write_shim_include(lines); + } + + [Fact] + public void the_target_should_exist() + { + File.Exists(resolve_from_root(_target)).ShouldBeTrue(); + } + + [Fact] + public void the_target_should_not_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_including_exe_targets_in_the_root_directory : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + string[] lines = { "." }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_two_targets_returned() + { + Results.Count().ShouldEqual(2); + } + + [Fact] + public void the_exe_targets_should_be_returned() + { + string[] expected = { "!tools.exe", "#tools.exe" }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_including_targets_starting_with_hash_and_exclamation_mark : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + // also test using an empty directory + string[] lines = { @"\#tools.exe", @"\!tools.exe" }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_two_targets_returned() + { + Results.Count().ShouldEqual(2); + } + + [Fact] + public void the_targets_should_be_returned() + { + string[] expected = { "!tools.exe", "#tools.exe" }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_excluding_a_target_starting_with_exclamation_mark : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + // also test using an empty directory + string[] lines = { "*.exe", "!!tools.exe" }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_one_target_returned() + { + Results.Count().ShouldEqual(1); + } + + [Fact] + public void the_non_excluded_targets_should_be_returned() + { + string[] expected = { "#tools.exe" }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_including_wildcard_folders : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + // also test forward slashes + string[] lines = { "prog/*" }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_three_targets_returned() + { + Results.Count().ShouldEqual(3); + } + + [Fact] + public void the_targets_should_be_returned() + { + string[] expected = { @"prog\opt\opt.exe", @"prog\sbin\prog.exe", @"prog\sbin\prog2.exe" }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_including_wildcard_folders_with_other_extensions : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + // also test forward slashes + string[] lines = { "prog/*/*.bat", "prog/*/*.cmd" }; + write_shim_include(lines); + } + + [Fact] + public void there_should_be_two_targets_returned() + { + Results.Count().ShouldEqual(2); + } + + [Fact] + public void the_targets_should_be_returned() + { + string[] expected = { @"prog\opt\opt2.cmd", @"prog\sbin\prog3.bat" }; + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + + [Category("Integration")] + public class when_including_a_specifically_ignored_target : ShimTargetSpecsBase + { + private string _resolvedTarget; + + public override void Context() + { + base.Context(); + var target = @"prog\ignore-me.exe"; + _resolvedTarget = resolve_from_root(target); + string[] lines = { target }; + write_shim_include(lines); + } + + [Fact] + public void the_target_should_exist() + { + File.Exists(_resolvedTarget).ShouldBeTrue(); + } + + [Fact] + public void the_target_ignore_file_should_exist() + { + File.Exists(_resolvedTarget + ".ignore").ShouldBeTrue(); + } + + [Fact] + public void the_target_should_not_be_returned() + { + Results.ShouldBeEmpty(); + } + } + + [Category("Integration")] + public class when_no_shiminclude_is_found : ShimTargetSpecsBase + { + public override void Context() + { + base.Context(); + File.Delete(IncludeFile); + } + + [Fact] + public void the_shiminclude_file_should_not_exist() + { + File.Exists(IncludeFile).ShouldBeFalse(); + } + + [Fact] + public void there_should_be_six_targets_returned() + { + Results.Count().ShouldEqual(6); + } + + [Fact] + public void the_exe_targets_should_be_returned() + { + string[] expected = + { + @"..\pkg.exe", + @".\!tools.exe", + @".\#tools.exe", + @"prog\opt\opt.exe", + @"prog\sbin\prog.exe", + @"prog\sbin\prog2.exe" + }; + + check_results(expected).ShouldBeTrue(ErrorMessage); + } + } + } +} diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/outside.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/outside.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/pkg.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/pkg.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/!tools.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/!tools.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/#tools.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/#tools.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude new file mode 100644 index 0000000000..fa7c7139fd --- /dev/null +++ b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude @@ -0,0 +1 @@ +# no shims here diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe.ignore b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe.ignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt2.cmd b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt2.cmd new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt3.vbs b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt3.vbs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog2.exe b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog2.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog3.bat b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog3.bat new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index f1d4e6bc7e..a341017926 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -134,6 +134,10 @@ + + + + diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs new file mode 100644 index 0000000000..f26407d7c9 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs @@ -0,0 +1,152 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.IO; + + public class ShimTargetFileFinder + { + /// + /// Returns the last three characters of the file name. + /// + /// The filename + /// The extension + /// + /// Only specific three-character extensions are used. + /// + public string get_extension(string fileName) + { + return fileName.Substring(fileName.Length - 3).ToLower(); + } + + /// + /// Searches the package for all exe files. + /// + /// The package folder location. + /// The exe files to shim. + public IEnumerable get_targets(string path) + { + var result = find(path, "*.exe", SearchOption.AllDirectories); + + return remove_ignored_files(result); + } + + /// + /// Resolves executable file patterns from an include and exclude list. + /// + /// The include list. + /// The exclude list. + /// The executable files to shim. + public IEnumerable get_targets(ShimTargetList includes, ShimTargetList excludes) + { + var result = new List(); + + foreach (var path in includes.Items.Keys) + { + var includedFiles = get_files(path, includes.Items[path]); + var excludedFiles = get_files(excludes, path); + + if (excludedFiles.Count == 0) + { + result.AddRange(includedFiles); + continue; + } + + foreach (var file in includedFiles) + { + if (!excludedFiles.Contains(file)) + { + result.Add(file); + } + } + } + + return remove_ignored_files(result); + } + + /// + /// Searches for files in a specified path, optionally searching subdirectories. + /// + /// The path. + /// The search pattern. + /// TopDirectoryOnly or AllDirectories. + /// The matched file names. + private IEnumerable find(string path, string pattern, SearchOption searchOption) + { + var extension = get_extension(pattern); + + return Directory.EnumerateFiles(path, pattern, searchOption) + .Where(f => f.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Searches for files from a list of search patterns. + /// + /// The path. + /// List of search patterns. + /// The matched file names. + private List get_files(string path, IEnumerable patterns) + { + var result = new List(); + + foreach (var pattern in patterns) + { + var files = find(path, pattern, SearchOption.TopDirectoryOnly); + result.AddRange(files); + } + + return result; + } + + /// + /// Searches for files if a list contains a specific path. + /// + /// The list + /// The path. + /// The matched file names or an empty List. + private List get_files(ShimTargetList list, string path) + { + if (list.Items.ContainsKey(path)) + { + return get_files(path, list.Items[path]); + } + + return new List(); + } + + /// + /// Filters out files that should not be shimmed. + /// + /// The candidate files to shim. + /// The intended files to shim + private List remove_ignored_files(IEnumerable targetFiles) + { + var result = new List(); + + foreach (var file in targetFiles) + { + // ignore the file if there is a matching file suffixed '.ignore' + if (!File.Exists(file + ".ignore")) result.Add(file); + } + + return result; + } + } +} diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs new file mode 100644 index 0000000000..7cbe4824da --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetList.cs @@ -0,0 +1,60 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System; + using System.Collections.Generic; + + public class ShimTargetList + { + public IDictionary> Items; + + /// + /// Creates a ShimTargetList instance. + /// + public ShimTargetList() + { + Items = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Adds a path and file pattern to the items collection. + /// + /// The path. + /// The file pattern. + public void add_directive(string path, string filePattern) + { + List filePatterns; + + // important - always lowercase the file pattern + filePattern = filePattern.ToLower(); + + if (Items.TryGetValue(path, out filePatterns)) + { + if (!filePatterns.Contains(filePattern)) + { + filePatterns.Add(filePattern); + } + } + else + { + filePatterns = new List { filePattern }; + Items.Add(path, filePatterns); + } + } + } +} diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs new file mode 100644 index 0000000000..4e7e86ccb6 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs @@ -0,0 +1,296 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.RegularExpressions; + + public class ShimTargetManager + { + private readonly ShimTargetFileFinder _finder; + private readonly ShimTargetPathResolver _resolver; + private readonly string _packageDir; + private readonly string _includeFile; + private ShimTargetList _includes; + private ShimTargetList _excludes; + + /// + /// Creates a ShimTargetManager instance. + /// + /// The package directory. + public ShimTargetManager(string packageDir) + { + var toolsDir = Path.Combine(packageDir, "tools"); + _packageDir = packageDir; + _includeFile = Path.Combine(toolsDir, ".shiminclude"); + _finder = new ShimTargetFileFinder(); + _resolver = new ShimTargetPathResolver(toolsDir); + } + + /// + /// Searches for executables to shim. + /// + /// Executables to shim or an empty list. + public IEnumerable get_shim_targets() + { + if (!File.Exists(_includeFile)) + { + return _finder.get_targets(_packageDir); + } + + _includes = new ShimTargetList(); + _excludes = new ShimTargetList(); + + var directives = File.ReadAllLines(_includeFile, System.Text.Encoding.UTF8); + parse_include_file(directives); + + return _finder.get_targets(_includes, _excludes); + } + + /// + /// Evaluates the .shiminclude file directives. + /// + /// The list of directives. + /// Populates _includes and _excludes lists. + private void parse_include_file(string[] directives) + { + var includedEntries = new ShimTargetList(); + var excludedEntries = new ShimTargetList(); + + foreach (string lineEntry in directives) + { + var line = lineEntry.Trim(); + + // skip blank or comment lines + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + { + continue; + } + + var entryList = includedEntries; + + if (line.StartsWith("\\#") || line.StartsWith("\\!")) + { + // backslash escapes + line = line.Substring(1); + } + else if (line.StartsWith("!")) + { + // make sure we have enough characters + if (line.Length == 1) continue; + + line = line.Substring(1); + entryList = excludedEntries; + } + + // transform to single backslashes, and remove duplicate asterisks + var path = Regex.Replace(line.Replace('/', '\\'), @"\\+", "\\"); + path = Regex.Replace(path, @"\*+", "*"); + + // relative paths only - skip if rooted or drive path + if (path.StartsWith("\\") || Regex.IsMatch(path, @"^[A-Za-z]:")) continue; + + // check the last segment to determine if we are a filename or a directory + var lastSegment = Path.GetFileName(path); + var filePattern = "*.exe"; + + if (Regex.IsMatch(lastSegment, @"\.(exe|bat|cmd)$", RegexOptions.IgnoreCase)) + { + path = Path.GetDirectoryName(path); + filePattern = lastSegment; + } + + // normalize the path + path = path.TrimEnd('\\'); + if (string.IsNullOrEmpty(path)) + { + path = "."; + } + + entryList.add_directive(path, filePattern); + } + + resolve_directives(includedEntries, _includes); + resolve_directives(excludedEntries, _excludes); + remove_excluded(); + } + + /// + /// Resolves directives and adds them to the list. + /// + /// The parsed directives from the file. + /// The resolved directives. + public void resolve_directives(ShimTargetList entryList, ShimTargetList pathList) + { + foreach (var path in entryList.Items.Keys) + { + var resolvedPaths = _resolver.resolve(path); + + // check we have results and that they are in the package directory + if (resolvedPaths.Count == 0 || !resolvedPaths[0].StartsWith(_packageDir, StringComparison.CurrentCultureIgnoreCase)) + { + continue; + } + + var filePatterns = entryList.Items[path]; + + foreach (var filePattern in filePatterns) + { + foreach (var resolvedPath in resolvedPaths) + { + pathList.add_directive(resolvedPath, filePattern); + } + } + } + + sanitize_directives(pathList); + } + + /// + /// Removes redundant file patterns. + /// + /// List of resolved directives. + private void sanitize_directives(ShimTargetList list) + { + foreach (var filePatterns in list.Items.Values) + { + if (filePatterns.Count == 0) continue; + + var anyPatterns = new List(); + var specificPatterns = new List(); + + // split into anyPatterns (like *.exe) and specificPatterns (like file.exe) + foreach (var filePattern in filePatterns) + { + var match = Regex.Match(filePattern, @"^\*\.(exe|bat|cmd)$").Groups[1]; + if (match.Success) + { + anyPatterns.Add(match.Value); + } + else + { + specificPatterns.Add(filePattern); + } + } + + // remove specific patterns (file.exe) if there is a matching any pattern (*.exe) + foreach (var pattern in specificPatterns) + { + var extension = _finder.get_extension(pattern); + + if (anyPatterns.Contains(extension)) + { + filePatterns.Remove(pattern); + } + } + } + } + + /// + /// Removes matching path-patterns from the included and excluded lists. + /// + private void remove_excluded() + { + var redundantExcludes = new List(); + + foreach (var path in _excludes.Items.Keys) + { + var excludedPatterns = _excludes.Items[path]; + List includedPatterns; + + // add to redundantExcludes if not in includes + if (!_includes.Items.TryGetValue(path, out includedPatterns)) + { + redundantExcludes.Add(path); + continue; + } + + // remove matching patterns from includes list + var removedPatterns = exclude_patterns(excludedPatterns, includedPatterns); + + // remove from includes list if no patterns left + if (includedPatterns.Count == 0) + { + _includes.Items.Remove(path); + } + + // remove matched patterns from excluded patterns + foreach (var pattern in removedPatterns) + { + excludedPatterns.Remove(pattern); + } + + // add to redundantExcludes if no patterns left + if (excludedPatterns.Count == 0) + { + redundantExcludes.Add(path); + } + } + + // remove redundant excludes + foreach (var path in redundantExcludes) + { + _excludes.Items.Remove(path); + } + } + + /// + /// Removes matching file patterns from a list of included patterns. + /// + /// The excluded file patterns. + /// The included file patterns. + /// The file patterns that have been removed. + private List exclude_patterns(List excludedPatterns, List includedPatterns) + { + var result = new List(); + + foreach (var exPattern in excludedPatterns) + { + var match = Regex.Match(exPattern, @"^\*\.(exe|bat|cmd)$").Groups[1]; + if (match.Success) + { + // we match any pattern (like *.exe) + var extension = match.Value; + var removals = new List(); + + // add patterns with the same extension for removal + foreach (var incPattern in includedPatterns) + { + if (extension == _finder.get_extension(incPattern)) + { + removals.Add(incPattern); + } + } + + // remove the patterns + foreach (var pattern in removals) + { + includedPatterns.Remove(pattern); + } + + result.Add(exPattern); + } + else if (includedPatterns.Contains(exPattern)) + { + includedPatterns.Remove(exPattern); + result.Add(exPattern); + } + } + + return result; + } + } +} diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs new file mode 100644 index 0000000000..0aaf9a6d52 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs @@ -0,0 +1,140 @@ +// Copyright © 2017 - 2018 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.RegularExpressions; + + public class ShimTargetPathResolver + { + private readonly string _rootPath; + + /// + /// Creates a ShimTargetPathResolver instance. + /// + /// The directory on which relative paths are based. + public ShimTargetPathResolver(string rootPath) + { + _rootPath = rootPath; + } + + /// + /// Resolves a path. + /// + /// The path relative to the root path. + /// The resolved paths or an empty list. + public List resolve(string path) + { + var resolvedPaths = new List { _rootPath }; + var subDirs = new Queue(path.Split('\\')); + + return resolve_path(resolvedPaths, subDirs); + } + + /// + /// Recursively steps through the components of a directory path in order to resolve it. + /// + /// The list of already resolved directory paths. + /// The remaining subdirectories to resolve. + /// The resolved directory names as absolute paths. + private List resolve_path(List resolvedPaths, Queue subDirs) + { + var result = new List(); + + // safety, shouldn't happen + if (subDirs.Count == 0) return result; + + // remove first directory + var folder = subDirs.Dequeue(); + var wildcard = Regex.IsMatch(folder, @"[?*]"); + + foreach (var path in resolvedPaths) + { + var resolved = wildcard ? search(path, folder) : test(path, folder); + + if (subDirs.Count > 0 && resolved.Count > 0) + { + resolved = resolve_path(resolved, subDirs); + } + + result.AddRange(resolved); + } + + return result; + } + + /// + /// Searches for directories in a specified path. + /// + /// The path. + /// The search pattern. + /// The matched directory names as absolute paths. + private List search(string path, string pattern) + { + var result = new List(); + IEnumerable directories; + + try + { + directories = Directory.GetDirectories(path, pattern, SearchOption.TopDirectoryOnly); + } + catch + { + return result; + } + + // no need for try-catch as error conditions will have already been caught + foreach (var dir in directories) + { + var fullPath = Path.GetFullPath(dir); + result.Add(fullPath); + } + + return result; + } + + /// + /// Tests for a directory in a specified path. + /// + /// The path. + /// The directory name to test. + /// A list containing the absolute path of any match. + private List test(string path, string folder) + { + var result = new List(); + string fullPath; + + try + { + fullPath = Path.GetFullPath(Path.Combine(path, folder)); + } + catch + { + return result; + } + + if (Directory.Exists(fullPath)) + { + result.Add(fullPath); + } + + return result; + } + } +} diff --git a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs index 01d6991b24..afdb3c9fcb 100644 --- a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs +++ b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs @@ -22,6 +22,7 @@ namespace chocolatey.infrastructure.app.services using configuration; using filesystem; using infrastructure.commands; + using infrastructure.app.domain; using results; public class ShimGenerationService : IShimGenerationService @@ -87,8 +88,10 @@ public void install(ChocolateyConfiguration configuration, PackageResult package return; } - //gather all .exes in the folder - var exeFiles = _fileSystem.get_files(packageResult.InstallLocation, pattern: "*.exe", option: SearchOption.AllDirectories); + //gather all .exes in the folder + var shimManager = new ShimTargetManager(packageResult.InstallLocation); + var exeFiles = shimManager.get_shim_targets(); + foreach (string file in exeFiles.or_empty_list_if_null()) { if (_fileSystem.file_exists(file + ".ignore")) continue;