From 3e4c16f4bc714dbda289147be06b024006413fe6 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Thu, 25 Jun 2020 18:22:37 +0100 Subject: [PATCH 1/2] (GH-2074) Control shim generation with a .shiminclude file A .shiminclude file specifies executables that Chocolatey should generate a shim for. This mechanism provides an opt-in alternative to the default behaviour, which is to automatically shim all .exe files found in the package unless there is a matching file suffixed .ignore. Each line in a .shiminclude file specifies a path relative to the package's tools directory. This can point to either a specific file type (.exe, .bat or .cmd), or to a directory or group of directories in which to search for .exe files. Format: - A .shiminclude file is UTF-8 encoded without a BOM. - A blank line matches no directories or files. - A line starting with `#` serves as a comment. Put a backslash `\` in front of the first hash for paths that begin with a hash. - An optional prefix `!` negates the path. Put a backslash `\` in front of the first `!` for paths that begin with a literal `!`. - A backslash `\` or a forward slash `/` can be used as a directory separator. - The `?` wildcard matches zero or one character except a directory separator. - The `*` wildcard matches zero or more characters except a directory separator. - The order of entries is not considered: duplicates are skipped and negated paths are irrevocable. Usage: - A .shiminclude file must be located in the package's tools directory and its entries must be relative paths from this location. If an entry resolves to a location outside the package directory it will be skipped. If no .shiminclude file is found, Chocolatey will search for .exe files to shim. - Chocolatey will not search for files to shim if the .shiminclude file is empty, or contains only blank/comment entries. - An entry is treated as a path to a file if it ends with an .exe, .bat or .cmd extension, otherwise it is considered a path to a directory. - If a matching file suffixed .ignore is found, a shim will not be generated. Examples: The following examples assume that a package has installed its software into the `\tools\prog` directory, where `` is the location of the package. The shiminclude file is located at `\tools\.shiminclude`. - `prog\sbin\prog.exe` matches the `\tools\prog\sbin\prog.exe` file. - `prog\sbin` matches all .exe files in `\tools\prog\sbin`. - `prog\*` matches all .exe files in all top level directories in `\tools\prog`. - `prog\sbin\prog?.exe` matches `\tools\prog\sbin\prog.exe` and `\tools\prog\sbin\prog2.exe`. - The file extension must be included to match other file types. For example `prog\sbin\prog3.bat` matches the `\tools\prog\sbin\prog3.bat` file, while `prog\*\*.bat` matches all .bat files in all top level directories in `\tools\prog`. --- .../chocolatey.tests.integration.csproj | 40 ++ .../shimtarget/ShimTargetSpecs.cs | 441 ++++++++++++++++++ .../infrastructure.app/shimtarget/outside.exe | 0 .../infrastructure.app/shimtarget/pkg/pkg.exe | 0 .../shimtarget/pkg/tools/!tools.exe | 0 .../shimtarget/pkg/tools/#tools.exe | 0 .../shimtarget/pkg/tools/.shiminclude | 1 + .../shimtarget/pkg/tools/prog/ignore-me.exe | 0 .../pkg/tools/prog/ignore-me.exe.ignore | 0 .../shimtarget/pkg/tools/prog/opt/opt.exe | 0 .../shimtarget/pkg/tools/prog/opt/opt2.cmd | 0 .../shimtarget/pkg/tools/prog/opt/opt3.vbs | 0 .../shimtarget/pkg/tools/prog/sbin/prog.exe | 0 .../shimtarget/pkg/tools/prog/sbin/prog2.exe | 0 .../shimtarget/pkg/tools/prog/sbin/prog3.bat | 0 src/chocolatey/chocolatey.csproj | 4 + .../domain/ShimTargetFileFinder.cs | 152 ++++++ .../domain/ShimTargetList.cs | 60 +++ .../domain/ShimTargetManager.cs | 296 ++++++++++++ .../domain/ShimTargetPathResolver.cs | 139 ++++++ .../services/ShimGenerationService.cs | 7 +- 21 files changed, 1138 insertions(+), 2 deletions(-) create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/outside.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/pkg.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/!tools.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/#tools.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/.shiminclude create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/ignore-me.exe.ignore create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt2.cmd create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/opt/opt3.vbs create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog2.exe create mode 100644 src/chocolatey.tests.integration/infrastructure.app/shimtarget/pkg/tools/prog/sbin/prog3.bat create mode 100644 src/chocolatey/infrastructure.app/domain/ShimTargetFileFinder.cs create mode 100644 src/chocolatey/infrastructure.app/domain/ShimTargetList.cs create mode 100644 src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs create mode 100644 src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs 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..758b1f1efe --- /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 = resolve_from_root(@"..\..\outside.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_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..5d3ec22bb2 --- /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 from includes list if no patterns left + 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..73988f38ba --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs @@ -0,0 +1,139 @@ +// 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; From 98fd854294c1c40415c5bd08e25b841957af304b Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 3 Jul 2020 10:52:40 +0100 Subject: [PATCH 2/2] Fix test and doc tweaks --- .../infrastructure.app/shimtarget/ShimTargetSpecs.cs | 4 ++-- src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs | 2 +- .../infrastructure.app/domain/ShimTargetPathResolver.cs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs index 758b1f1efe..74cd026286 100644 --- a/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs +++ b/src/chocolatey.tests.integration/infrastructure.app/shimtarget/ShimTargetSpecs.cs @@ -120,7 +120,7 @@ public class when_including_a_target_outside_the_package_directory : ShimTargetS public override void Context() { base.Context(); - _target = resolve_from_root(@"..\..\outside.exe"); + _target = @"..\..\outside.exe"; string[] lines = { _target }; write_shim_include(lines); } @@ -128,7 +128,7 @@ public override void Context() [Fact] public void the_target_should_exist() { - File.Exists(_target).ShouldBeTrue(); + File.Exists(resolve_from_root(_target)).ShouldBeTrue(); } [Fact] diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs index 5d3ec22bb2..4e7e86ccb6 100644 --- a/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetManager.cs @@ -218,7 +218,7 @@ private void remove_excluded() continue; } - // remove from includes list if no patterns left + // remove matching patterns from includes list var removedPatterns = exclude_patterns(excludedPatterns, includedPatterns); // remove from includes list if no patterns left diff --git a/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs index 73988f38ba..0aaf9a6d52 100644 --- a/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs +++ b/src/chocolatey/infrastructure.app/domain/ShimTargetPathResolver.cs @@ -72,6 +72,7 @@ private List resolve_path(List resolvedPaths, Queue subD { resolved = resolve_path(resolved, subDirs); } + result.AddRange(resolved); }