mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CmdPal: Fix binary file corruption in Create Extension (#46490)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This PR fixes a problem with invisible icons in newly create Command Palette extensions, when created through built-in command: - Avoids modifying binary files during extension creation from the template to prevent corruption. - Refactors template expansion and physical extension creation into a separate ExtensionTemplateService. - Adds unit tests. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #46448 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
private static readonly string _creatingText = "Creating new extension...";
|
||||
private readonly IExtensionTemplateService _extensionTemplateService;
|
||||
private readonly StatusMessage _creatingMessage = new()
|
||||
{
|
||||
Message = _creatingText,
|
||||
@@ -20,7 +21,15 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
};
|
||||
|
||||
public NewExtensionForm()
|
||||
: this(new ExtensionTemplateService())
|
||||
{
|
||||
}
|
||||
|
||||
private NewExtensionForm(IExtensionTemplateService extensionTemplateService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(extensionTemplateService);
|
||||
|
||||
_extensionTemplateService = extensionTemplateService;
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
@@ -115,7 +124,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
|
||||
try
|
||||
{
|
||||
CreateExtension(extensionName, displayName, outputPath);
|
||||
_extensionTemplateService.CreateExtension(extensionName, displayName, outputPath);
|
||||
|
||||
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
|
||||
|
||||
@@ -131,57 +140,6 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private void CreateExtension(string extensionName, string newDisplayName, string outputPath)
|
||||
{
|
||||
var newGuid = Guid.NewGuid().ToString();
|
||||
|
||||
// Unzip `template.zip` to a temp dir:
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
|
||||
// Does the output path exist?
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
var assetsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip");
|
||||
ZipFile.ExtractToDirectory(assetsPath, tempDir);
|
||||
|
||||
var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var text = File.ReadAllText(file);
|
||||
|
||||
// Replace all the instances of `FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF` with a new random guid:
|
||||
text = text.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid);
|
||||
|
||||
// Then replace all the `TemplateCmdPalExtension` with `extensionName`
|
||||
text = text.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Then replace all the `TemplateDisplayName` with `newDisplayName`
|
||||
text = text.Replace("TemplateDisplayName", newDisplayName);
|
||||
|
||||
// We're going to write the file to the same relative location in the output path
|
||||
var relativePath = Path.GetRelativePath(tempDir, file);
|
||||
|
||||
var newFileName = Path.Combine(outputPath, relativePath);
|
||||
|
||||
// if the file name had `TemplateCmdPalExtension` in it, replace it with `extensionName`
|
||||
newFileName = newFileName.Replace("TemplateCmdPalExtension", extensionName);
|
||||
|
||||
// Make sure the directory exists
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!);
|
||||
|
||||
File.WriteAllText(newFileName, text);
|
||||
|
||||
// Delete the old file
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
// Delete the temp dir
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
|
||||
private string FormatJsonString(string str) =>
|
||||
|
||||
// Escape the string for JSON
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
internal sealed class ExtensionTemplateService : IExtensionTemplateService
|
||||
{
|
||||
internal enum TemplateFileHandling
|
||||
{
|
||||
ReplaceTokens,
|
||||
CopyAsIs,
|
||||
}
|
||||
|
||||
private const string TemplateArchiveRelativePath = "Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip";
|
||||
|
||||
private static readonly HashSet<string> _replaceTokensTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".appxmanifest",
|
||||
".config",
|
||||
".cs",
|
||||
".csproj",
|
||||
".json",
|
||||
".manifest",
|
||||
".props",
|
||||
".pubxml",
|
||||
".sln",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _copyAsIsTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png",
|
||||
};
|
||||
|
||||
private readonly string _templateZipPath;
|
||||
|
||||
public ExtensionTemplateService()
|
||||
: this(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateArchiveRelativePath))
|
||||
{
|
||||
}
|
||||
|
||||
internal ExtensionTemplateService(string templateZipPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(templateZipPath);
|
||||
_templateZipPath = templateZipPath;
|
||||
}
|
||||
|
||||
internal static IReadOnlyCollection<string> ReplaceTokensTemplateExtensions => _replaceTokensTemplateExtensions;
|
||||
|
||||
internal static IReadOnlyCollection<string> CopyAsIsTemplateExtensions => _copyAsIsTemplateExtensions;
|
||||
|
||||
public void CreateExtension(string extensionName, string displayName, string outputPath)
|
||||
{
|
||||
var newGuid = Guid.NewGuid().ToString();
|
||||
|
||||
// Unzip `template.zip` to a temp dir:
|
||||
var tempDir = Directory.CreateTempSubdirectory("CmdPal_ExtTemplate");
|
||||
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
}
|
||||
|
||||
ZipFile.ExtractToDirectory(_templateZipPath, tempDir.FullName);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(tempDir.FullName, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
CopyTemplateFile(tempDir.FullName, file, outputPath, extensionName, displayName, newGuid);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
tempDir.Delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CopyTemplateFile(string templateRoot, string sourceFile, string outputPath, string extensionName, string displayName, string newGuid)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(templateRoot, sourceFile);
|
||||
var newFileName = Path.Combine(outputPath, relativePath).Replace("TemplateCmdPalExtension", extensionName, StringComparison.Ordinal);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFileName)!);
|
||||
|
||||
switch (GetTemplateFileHandling(sourceFile))
|
||||
{
|
||||
case TemplateFileHandling.ReplaceTokens:
|
||||
var sourceText = File.ReadAllText(sourceFile);
|
||||
var updatedText = ReplaceTemplateTokens(sourceText, extensionName, displayName, newGuid);
|
||||
if (string.Equals(sourceText, updatedText, StringComparison.Ordinal))
|
||||
{
|
||||
File.Copy(sourceFile, newFileName, overwrite: true);
|
||||
break;
|
||||
}
|
||||
|
||||
File.WriteAllText(newFileName, updatedText);
|
||||
break;
|
||||
case TemplateFileHandling.CopyAsIs:
|
||||
default:
|
||||
File.Copy(sourceFile, newFileName, overwrite: true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal static TemplateFileHandling GetTemplateFileHandling(string filePath)
|
||||
{
|
||||
var extension = Path.GetExtension(filePath);
|
||||
if (_replaceTokensTemplateExtensions.Contains(extension))
|
||||
{
|
||||
return TemplateFileHandling.ReplaceTokens;
|
||||
}
|
||||
|
||||
if (_copyAsIsTemplateExtensions.Contains(extension))
|
||||
{
|
||||
return TemplateFileHandling.CopyAsIs;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Template file '{filePath}' has unsupported extension '{extension}'. Update the template file handling lists in {nameof(ExtensionTemplateService)}.");
|
||||
}
|
||||
|
||||
private static string ReplaceTemplateTokens(string text, string extensionName, string displayName, string newGuid) =>
|
||||
text
|
||||
.Replace("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", newGuid, StringComparison.Ordinal)
|
||||
.Replace("TemplateCmdPalExtension", extensionName, StringComparison.Ordinal)
|
||||
.Replace("TemplateDisplayName", displayName, StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Creates new Command Palette extensions from the built-in project template.
|
||||
/// </summary>
|
||||
internal interface IExtensionTemplateService
|
||||
{
|
||||
/// <summary>
|
||||
/// Scaffolds a new extension project by extracting the template archive,
|
||||
/// replacing placeholder tokens, and writing the result to <paramref name="outputPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="extensionName">The code-safe name used for the project and namespaces.</param>
|
||||
/// <param name="displayName">The human-readable name shown in the extension catalog.</param>
|
||||
/// <param name="outputPath">The directory where the new extension project will be created.</param>
|
||||
void CreateExtension(string extensionName, string displayName, string outputPath);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class ExtensionTemplateServiceTests
|
||||
{
|
||||
private string _templateRoot = null!;
|
||||
private string _outputRoot = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), $"{nameof(ExtensionTemplateServiceTests)}_{Guid.NewGuid():N}");
|
||||
_templateRoot = Path.Combine(tempRoot, "template");
|
||||
_outputRoot = Path.Combine(tempRoot, "output");
|
||||
|
||||
Directory.CreateDirectory(_templateRoot);
|
||||
Directory.CreateDirectory(_outputRoot);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
var tempRoot = Directory.GetParent(_templateRoot)?.FullName;
|
||||
if (!string.IsNullOrEmpty(tempRoot) && Directory.Exists(tempRoot))
|
||||
{
|
||||
Directory.Delete(tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CreateExtension_BuildsExtensionFromTemplateArchive()
|
||||
{
|
||||
// Arrange
|
||||
var archiveRoot = Path.Combine(_templateRoot, "archive");
|
||||
var templateProjectRoot = Path.Combine(archiveRoot, "TemplateCmdPalExtension", "TemplateCmdPalExtension");
|
||||
Directory.CreateDirectory(Path.Combine(templateProjectRoot, "Assets"));
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(templateProjectRoot, "Program.cs"),
|
||||
"TemplateCmdPalExtension TemplateDisplayName FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
|
||||
File.WriteAllBytes(Path.Combine(templateProjectRoot, "Assets", "Logo.png"), [0x89, 0x50, 0x4E, 0x47]);
|
||||
|
||||
var templateZipPath = Path.Combine(_templateRoot, "template.zip");
|
||||
ZipFile.CreateFromDirectory(archiveRoot, templateZipPath);
|
||||
|
||||
var service = new ExtensionTemplateService(templateZipPath);
|
||||
|
||||
// Act
|
||||
service.CreateExtension("MyExtension", "My Display Name", _outputRoot);
|
||||
|
||||
// Assert
|
||||
var programFile = Path.Combine(_outputRoot, "MyExtension", "MyExtension", "Program.cs");
|
||||
var imageFile = Path.Combine(_outputRoot, "MyExtension", "MyExtension", "Assets", "Logo.png");
|
||||
|
||||
Assert.IsTrue(File.Exists(programFile));
|
||||
Assert.IsTrue(File.Exists(imageFile));
|
||||
StringAssert.Contains(File.ReadAllText(programFile), "MyExtension");
|
||||
StringAssert.Contains(File.ReadAllText(programFile), "My Display Name");
|
||||
Assert.IsFalse(File.ReadAllText(programFile).Contains("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", StringComparison.Ordinal));
|
||||
CollectionAssert.AreEqual(new byte[] { 0x89, 0x50, 0x4E, 0x47 }, File.ReadAllBytes(imageFile));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CopyTemplateFile_RewritesTextFiles()
|
||||
{
|
||||
// Arrange
|
||||
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Program.cs");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
||||
File.WriteAllText(sourceFile, "TemplateCmdPalExtension TemplateDisplayName FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
|
||||
|
||||
// Act
|
||||
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
||||
|
||||
// Assert
|
||||
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Program.cs");
|
||||
Assert.IsTrue(File.Exists(outputFile));
|
||||
Assert.AreEqual(
|
||||
"MyExtension My Display Name 11111111-1111-1111-1111-111111111111",
|
||||
File.ReadAllText(outputFile));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CopyTemplateFile_CopiesUnchangedTextFilesVerbatim()
|
||||
{
|
||||
// Arrange
|
||||
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Properties", "launchSettings.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
||||
|
||||
var sourceBytes = Encoding.UTF8.GetPreamble()
|
||||
.Concat(Encoding.UTF8.GetBytes("{\"profiles\":{\"CmdPal\":{}}}"))
|
||||
.ToArray();
|
||||
File.WriteAllBytes(sourceFile, sourceBytes);
|
||||
|
||||
// Act
|
||||
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
||||
|
||||
// Assert
|
||||
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Properties", "launchSettings.json");
|
||||
Assert.IsTrue(File.Exists(outputFile));
|
||||
CollectionAssert.AreEqual(sourceBytes, File.ReadAllBytes(outputFile));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CopyTemplateFile_CopiesBinaryFilesWithoutRewritingContents()
|
||||
{
|
||||
// Arrange
|
||||
var sourceFile = Path.Combine(_templateRoot, "TemplateCmdPalExtension", "Assets", "Logo.png");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(sourceFile)!);
|
||||
|
||||
var binaryContent = new byte[]
|
||||
{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00,
|
||||
};
|
||||
var embeddedText = Encoding.UTF8.GetBytes("TemplateCmdPalExtension TemplateDisplayName");
|
||||
File.WriteAllBytes(sourceFile, [.. binaryContent, .. embeddedText]);
|
||||
|
||||
// Act
|
||||
ExtensionTemplateService.CopyTemplateFile(_templateRoot, sourceFile, _outputRoot, "MyExtension", "My Display Name", "11111111-1111-1111-1111-111111111111");
|
||||
|
||||
// Assert
|
||||
var outputFile = Path.Combine(_outputRoot, "MyExtension", "Assets", "Logo.png");
|
||||
Assert.IsTrue(File.Exists(outputFile));
|
||||
CollectionAssert.AreEqual(File.ReadAllBytes(sourceFile), File.ReadAllBytes(outputFile));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateFileHandling_ThrowsForUnknownExtension()
|
||||
{
|
||||
var ex = Assert.ThrowsException<InvalidOperationException>(() => ExtensionTemplateService.GetTemplateFileHandling("template.svg"));
|
||||
|
||||
StringAssert.Contains(ex.Message, ".svg");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateExtensionCategories_AreDisjointAndCoverTemplateZip()
|
||||
{
|
||||
var replaceTokens = ExtensionTemplateService.ReplaceTokensTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var copyAsIs = ExtensionTemplateService.CopyAsIsTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var allAccountedFor = replaceTokens.Concat(copyAsIs).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var templateZipExtensions = GetTemplateZipExtensions();
|
||||
|
||||
CollectionAssert.AreEqual(Array.Empty<string>(), replaceTokens.Intersect(copyAsIs, StringComparer.OrdinalIgnoreCase).ToArray());
|
||||
CollectionAssert.AreEquivalent(templateZipExtensions.OrderBy(x => x).ToArray(), allAccountedFor.OrderBy(x => x).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateZipFiles_AllUseKnownHandling()
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(TemplateZipPath);
|
||||
var replaceTokens = ExtensionTemplateService.ReplaceTokensTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var copyAsIs = ExtensionTemplateService.CopyAsIsTemplateExtensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(entry => !string.IsNullOrEmpty(Path.GetExtension(entry.FullName))))
|
||||
{
|
||||
var extension = Path.GetExtension(entry.FullName);
|
||||
var expectedHandling = replaceTokens.Contains(extension)
|
||||
? ExtensionTemplateService.TemplateFileHandling.ReplaceTokens
|
||||
: ExtensionTemplateService.TemplateFileHandling.CopyAsIs;
|
||||
|
||||
Assert.AreEqual(expectedHandling, ExtensionTemplateService.GetTemplateFileHandling(entry.FullName), entry.FullName);
|
||||
Assert.IsTrue(replaceTokens.Contains(extension) || copyAsIs.Contains(extension), entry.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TemplateZipPath => Path.Combine(AppContext.BaseDirectory, "Assets", "template.zip");
|
||||
|
||||
private static HashSet<string> GetTemplateZipExtensions()
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(TemplateZipPath);
|
||||
return archive.Entries
|
||||
.Where(entry => !string.IsNullOrEmpty(Path.GetExtension(entry.FullName)))
|
||||
.Select(entry => Path.GetExtension(entry.FullName))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,10 @@
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\Microsoft.CmdPal.UI.ViewModels\Assets\template.zip" Link="Assets\template.zip">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user