diff --git a/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs b/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs
index f0a7176..b5a2987 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.HtmlViewer/WebpagePanel.cs
@@ -20,6 +20,8 @@ using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Web.WebView2.Core;
@@ -30,8 +32,8 @@ namespace QuickLook.Plugin.HtmlViewer
{
public class WebpagePanel : UserControl
{
- public Uri _currentUri;
- public WebView2 _webView;
+ private Uri _currentUri;
+ private WebView2 _webView;
public WebpagePanel()
{
@@ -83,9 +85,131 @@ namespace QuickLook.Plugin.HtmlViewer
return;
var newUri = new Uri(e.Uri);
- if (newUri != _currentUri) e.Cancel = true;
+ if (newUri == _currentUri) return;
+ e.Cancel = true;
+
+ // Open in default browser
+ try
+ {
+ if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri))
+ {
+ Debug.WriteLine($"Invalid URI format: {e.Uri}");
+ return;
+ }
+
+ // Safe schemes can open directly
+ if (uri.Scheme == Uri.UriSchemeHttp ||
+ uri.Scheme == Uri.UriSchemeHttps ||
+ uri.Scheme == Uri.UriSchemeMailto)
+ {
+ try
+ {
+ Process.Start(uri.AbsoluteUri);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Failed to open URL: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ return;
+ }
+
+ // Ask user for unsafe schemes. Use dispatcher to avoid blocking thread.
+ string associatedApp = GetAssociatedAppForScheme(uri.Scheme);
+ Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ var result = MessageBox.Show(
+ !string.IsNullOrEmpty(associatedApp) ?
+ $"The following link will open in {associatedApp}:\n{e.Uri}" : $"The following link will open:\n{e.Uri}",
+ !string.IsNullOrEmpty(associatedApp) ?
+ $"Open {associatedApp}?" : "Open custom URI?",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Information);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ try
+ {
+ Process.Start(e.Uri);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Failed to open URL: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ }));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to open URL: {ex.Message}");
+ }
}
+ #region Get Associated App For Scheme
+ [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
+ private static extern uint AssocQueryString(
+ AssocF flags,
+ AssocStr str,
+ string pszAssoc,
+ string pszExtra,
+ [Out] StringBuilder pszOut,
+ ref uint pcchOut);
+
+ [Flags]
+ private enum AssocF
+ {
+ None = 0,
+ VerifyExists = 0x1
+ }
+
+ private enum AssocStr
+ {
+ Command = 1,
+ Executable = 2,
+ FriendlyAppName = 4
+ }
+
+ private string GetAssociatedAppForScheme(string scheme)
+ {
+ try
+ {
+ // Try to get friendly app name first
+ uint pcchOut = 0;
+ AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, null, ref pcchOut);
+
+ if (pcchOut > 0)
+ {
+ StringBuilder pszOut = new StringBuilder((int)pcchOut);
+ AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, pszOut, ref pcchOut);
+
+ var appName = pszOut.ToString().Trim();
+ if (!string.IsNullOrEmpty(appName))
+ return appName;
+ }
+
+ // Fall back to executable name if friendly name is not available
+ pcchOut = 0;
+ AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, null, ref pcchOut);
+
+ if (pcchOut > 0)
+ {
+ StringBuilder pszOut = new StringBuilder((int)pcchOut);
+ AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, pszOut, ref pcchOut);
+
+ var exeName = pszOut.ToString().Trim();
+ if (!string.IsNullOrEmpty(exeName))
+ return Path.GetFileName(exeName);
+ }
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to get associated app: {ex.Message}");
+ return null;
+ }
+ }
+ #endregion
+
private void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
_webView.DefaultBackgroundColor = Color.White; // Reset to white after page load to match expected default behavior
diff --git a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/GenerateEmbeddedResourcesHash.ps1 b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/GenerateEmbeddedResourcesHash.ps1
new file mode 100644
index 0000000..fe9bd91
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/GenerateEmbeddedResourcesHash.ps1
@@ -0,0 +1,54 @@
+param(
+ [Parameter(Mandatory=$true)]
+ [string]$ResourcesDir,
+
+ [Parameter(Mandatory=$true)]
+ [string]$OutputFile
+)
+
+# Get all files in the Resources directory
+$files = Get-ChildItem -Path $ResourcesDir -Recurse -File | Sort-Object -Property FullName
+
+# Create SHA256 hasher
+$sha256 = [System.Security.Cryptography.SHA256]::Create()
+
+# Create memory stream to combine all file contents
+$memoryStream = New-Object System.IO.MemoryStream
+
+# Add each file's path and contents to the hash
+foreach ($file in $files) {
+ # Get relative path and convert to lowercase for consistent hashing
+ $relativePath = $file.FullName.Substring($ResourcesDir.Length + 1).ToLowerInvariant()
+ $pathBytes = [System.Text.Encoding]::UTF8.GetBytes("QuickLook.Plugin.MarkdownViewer.Resources.$relativePath".Replace("\", "/"))
+ $memoryStream.Write($pathBytes, 0, $pathBytes.Length)
+
+ # Add file contents
+ $fileBytes = [System.IO.File]::ReadAllBytes($file.FullName)
+ $memoryStream.Write($fileBytes, 0, $fileBytes.Length)
+}
+
+# Calculate final hash
+$hashBytes = $sha256.ComputeHash($memoryStream.ToArray())
+$hash = [BitConverter]::ToString($hashBytes).Replace("-", "").ToLowerInvariant()
+
+# Generate C# code
+$code = @"
+//
+// This file was generated during build to indicate changes to embedded resources.
+//
+
+namespace QuickLook.Plugin.MarkdownViewer
+{
+ internal static class EmbeddedResourcesHash
+ {
+ internal const string Hash = "$hash";
+ }
+}
+"@
+
+# Write to output file
+[System.IO.File]::WriteAllText($OutputFile, $code)
+
+# Cleanup
+$memoryStream.Dispose()
+$sha256.Dispose()
diff --git a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Plugin.cs
index b5d68c1..5c45eed 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Plugin.cs
@@ -20,12 +20,9 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
-using System.Reflection;
-using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Threading;
-using Microsoft.Web.WebView2.Core;
using QuickLook.Common.Helpers;
using QuickLook.Common.Plugin;
using QuickLook.Plugin.HtmlViewer;
@@ -36,84 +33,21 @@ namespace QuickLook.Plugin.MarkdownViewer
public class Plugin : IViewer
{
private WebpagePanel? _panel;
- private static string _resourcePath;
- private static readonly string ResourcePrefix = "QuickLook.Plugin.MarkdownViewer.Resources.";
private string? _currentHtmlPath;
- private static bool OverrideFilesInDevelopment => true && Debugger.IsAttached; // Debug setting
-
- static Plugin()
- {
- // Set up resource path in AppData
- _resourcePath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Plugin.MarkdownViewer");
- }
+ private static readonly string _resourcePath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Plugin.MarkdownViewer");
+ private static readonly string _resourcePrefix = "QuickLook.Plugin.MarkdownViewer.Resources.";
+ private static readonly ResourceManager _resourceManager = new ResourceManager(_resourcePath, _resourcePrefix);
public int Priority => 0;
public void Init()
{
- // Create directory if it doesn't exist
- if (!Directory.Exists(_resourcePath) || OverrideFilesInDevelopment)
- {
- Directory.CreateDirectory(_resourcePath);
- ExtractResources();
- }
+ // Initialize resources and handle versioning
+ _resourceManager.InitializeResources();
// Clean up any temporary HTML files if QuickLook was forcibly terminated
- try
- {
- var tempFiles = Directory.GetFiles(_resourcePath, "temp_*.html");
- foreach (var file in tempFiles)
- {
- try
- {
- File.Delete(file);
- }
- catch (IOException) { } // Ignore deletion errors
- }
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to clean up temporary HTML files: {ex.Message}");
- }
- }
-
- private void ExtractResources()
- {
- var assembly = Assembly.GetExecutingAssembly();
- var resourceNames = assembly.GetManifestResourceNames();
-
- foreach (var resourceName in resourceNames)
- {
- if (!resourceName.StartsWith(ResourcePrefix)) continue;
-
- var relativePath = resourceName.Substring(ResourcePrefix.Length);
- if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue; // Skip 'resources' binary file
-
- var targetPath = Path.Combine(_resourcePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
-
- // Create directory if it doesn't exist
- var directory = Path.GetDirectoryName(targetPath);
- if (directory != null)
- Directory.CreateDirectory(directory);
-
- // Extract the resource (skip if it already exists, unless in debug mode)
- if (File.Exists(targetPath) && !OverrideFilesInDevelopment)
- continue;
-
- using (var resourceStream = assembly.GetManifestResourceStream(resourceName))
- using (var fileStream = File.Create(targetPath))
- {
- resourceStream?.CopyTo(fileStream);
- }
- }
-
- // Verify that md2html.html was extracted
- var htmlPath = Path.Combine(_resourcePath, "md2html.html");
- if (!File.Exists(htmlPath))
- {
- throw new FileNotFoundException($"Required template file md2html.html not found in resources. Available resources: {string.Join(", ", resourceNames)}");
- }
+ CleanupTempFiles();
}
public bool CanHandle(string path)
@@ -135,123 +69,6 @@ namespace QuickLook.Plugin.MarkdownViewer
var htmlPath = GenerateMarkdownHtml(path);
_panel.NavigateToFile(htmlPath);
_panel.Dispatcher.Invoke(() => { context.IsBusy = false; }, DispatcherPriority.Loaded);
-
- _panel._webView.NavigationStarting += NavigationStarting_CancelNavigation;
- }
-
- private void NavigationStarting_CancelNavigation(object sender, CoreWebView2NavigationStartingEventArgs e)
- {
- if (e.Uri.StartsWith("data:")) // when using NavigateToString
- return;
-
- var newUri = new Uri(e.Uri);
- if (newUri == _panel?._currentUri) return;
- e.Cancel = true;
-
- // Open in default browser
- try
- {
- if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri))
- {
- Debug.WriteLine($"Invalid URI format: {e.Uri}");
- return;
- }
-
- // Safe schemes can open directly
- if (uri.Scheme == Uri.UriSchemeHttp ||
- uri.Scheme == Uri.UriSchemeHttps ||
- uri.Scheme == Uri.UriSchemeMailto)
- {
- Process.Start(uri.AbsoluteUri);
- return;
- }
-
- // Ask user for unsafe schemes. Use dispatcher to avoid blocking thread.
- var associatedApp = GetAssociatedAppForScheme(uri.Scheme);
- Application.Current.Dispatcher.BeginInvoke(new Action(() =>
- {
- var result = MessageBox.Show(
- !string.IsNullOrEmpty(associatedApp) ?
- $"The following link will open in {associatedApp}:\n{e.Uri}" : $"The following link will open:\n{e.Uri}",
- !string.IsNullOrEmpty(associatedApp) ?
- $"Open {associatedApp}?" : "Open custom URI?",
- MessageBoxButton.YesNo,
- MessageBoxImage.Information);
-
- if (result == MessageBoxResult.Yes)
- {
- Process.Start(e.Uri);
- }
- }));
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to open URL in browser: {ex.Message}");
- }
- }
-
- [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
- private static extern uint AssocQueryString(
- AssocF flags,
- AssocStr str,
- string pszAssoc,
- string? pszExtra,
- [Out] StringBuilder? pszOut,
- ref uint pcchOut);
-
- [Flags]
- private enum AssocF
- {
- None = 0,
- VerifyExists = 0x1
- }
-
- private enum AssocStr
- {
- Command = 1,
- Executable = 2,
- FriendlyAppName = 4
- }
-
- private string? GetAssociatedAppForScheme(string scheme)
- {
- try
- {
- // Try to get friendly app name first
- uint pcchOut = 0;
- AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, null, ref pcchOut);
-
- if (pcchOut > 0)
- {
- StringBuilder pszOut = new StringBuilder((int)pcchOut);
- AssocQueryString(AssocF.None, AssocStr.FriendlyAppName, scheme, null, pszOut, ref pcchOut);
-
- var appName = pszOut.ToString().Trim();
- if (!string.IsNullOrEmpty(appName))
- return appName;
- }
-
- // Fall back to executable name if friendly name is not available
- pcchOut = 0;
- AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, null, ref pcchOut);
-
- if (pcchOut > 0)
- {
- StringBuilder pszOut = new StringBuilder((int)pcchOut);
- AssocQueryString(AssocF.None, AssocStr.Executable, scheme, null, pszOut, ref pcchOut);
-
- var exeName = pszOut.ToString().Trim();
- if (!string.IsNullOrEmpty(exeName))
- return Path.GetFileName(exeName);
- }
-
- return null;
- }
- catch (Exception ex)
- {
- Debug.WriteLine($"Failed to get associated app: {ex.Message}");
- return null;
- }
}
private string GenerateMarkdownHtml(string path)
@@ -286,6 +103,7 @@ namespace QuickLook.Plugin.MarkdownViewer
return outputPath;
}
+ #region Cleanup
private void CleanupTempHtmlFile()
{
if (!string.IsNullOrEmpty(_currentHtmlPath) && File.Exists(_currentHtmlPath))
@@ -298,6 +116,26 @@ namespace QuickLook.Plugin.MarkdownViewer
}
}
+ private void CleanupTempFiles()
+ {
+ try
+ {
+ var tempFiles = Directory.GetFiles(_resourcePath, "temp_*.html");
+ foreach (var file in tempFiles)
+ {
+ try
+ {
+ File.Delete(file);
+ }
+ catch (IOException) { } // Ignore deletion errors
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to clean up temporary HTML files: {ex.Message}");
+ }
+ }
+
public void Cleanup()
{
GC.SuppressFinalize(this);
@@ -307,5 +145,6 @@ namespace QuickLook.Plugin.MarkdownViewer
_panel?.Dispose();
_panel = null;
}
+ #endregion
}
}
\ No newline at end of file
diff --git a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/QuickLook.Plugin.MarkdownViewer.csproj b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/QuickLook.Plugin.MarkdownViewer.csproj
index 9300258..9070b6b 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/QuickLook.Plugin.MarkdownViewer.csproj
+++ b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/QuickLook.Plugin.MarkdownViewer.csproj
@@ -75,4 +75,22 @@
+
+
+ $(IntermediateOutputPath)Generated
+ $(HashFileDir)\EmbeddedResourcesHash.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/ResourceManager.cs b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/ResourceManager.cs
new file mode 100644
index 0000000..a700766
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/ResourceManager.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
+using System.Windows;
+using QuickLook.Common.Helpers;
+
+namespace QuickLook.Plugin.MarkdownViewer
+{
+ internal class ResourceManager
+ {
+ private readonly string _resourcePath;
+ private readonly string _resourcePrefix;
+ private readonly string _versionFilePath;
+ private readonly string _noUpdateFilePath;
+ private readonly string _embeddedHash;
+
+ public ResourceManager(string resourcePath, string resourcePrefix)
+ {
+ _resourcePath = resourcePath;
+ _resourcePrefix = resourcePrefix;
+ _versionFilePath = Path.Combine(_resourcePath, ".version");
+ _noUpdateFilePath = Path.Combine(_resourcePath, ".noupdate");
+ _embeddedHash = GetEmbeddedResourcesHash();
+ }
+
+ public void InitializeResources()
+ {
+ // Extract resources for the first time
+ if (!Directory.Exists(_resourcePath))
+ {
+ ExtractResources();
+ return;
+ }
+
+ // Check if updates are disabled
+ if (File.Exists(_noUpdateFilePath))
+ return;
+
+ // Check if resources need updating by comparing hashes
+ var versionInfo = ReadVersionFile();
+
+ if (versionInfo == null)
+ {
+ // No version file exists, create it and extract resources
+ ExtractResources();
+ return;
+ }
+
+ // If embedded hash matches stored hash, no update needed
+ if (_embeddedHash == versionInfo.EmbeddedHash) return;
+
+ // Calculate current directory hash
+ var currentDirectoryHash = CalculateDirectoryHash(_resourcePath);
+
+ // If current directory matches the stored extracted hash, user hasn't modified files
+ if (currentDirectoryHash == versionInfo.ExtractedHash)
+ {
+ // Safe to update
+ ExtractResources();
+ return;
+ }
+
+ // User has modified files, ask for permission to update
+ var result = MessageBox.Show(
+ "The MarkdownViewer resources have been updated. Would you like to update to the newest version?\n\n" +
+ "Note: Your current resources appear to have been modified. Updating will overwrite your modifications.",
+ "MarkdownViewer Update Available",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ ExtractResources();
+ }
+ else
+ {
+ // Update the embedded hash in the version file to prevent any more prompts for this version
+ UpdateVersionFileEmbeddedHash();
+ }
+ }
+
+ private void ExtractResources()
+ {
+ // Delete and recreate directory to ensure clean state
+ if (Directory.Exists(_resourcePath))
+ {
+ try
+ {
+ Directory.Delete(_resourcePath, true);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to delete directory {_resourcePath}: {ex.Message}");
+ // If we can't delete the directory, we'll try to continue with existing one
+ }
+ }
+ Directory.CreateDirectory(_resourcePath);
+
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceNames = assembly.GetManifestResourceNames();
+
+ foreach (var resourceName in resourceNames)
+ {
+ if (!resourceName.StartsWith(_resourcePrefix)) continue;
+
+ var relativePath = resourceName.Substring(_resourcePrefix.Length);
+ if (relativePath.Equals("resources", StringComparison.OrdinalIgnoreCase)) continue; // Skip 'resources' binary file
+
+ var targetPath = Path.Combine(_resourcePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
+
+ // Create directory if it doesn't exist
+ var directory = Path.GetDirectoryName(targetPath);
+ if (directory != null)
+ Directory.CreateDirectory(directory);
+
+ // Extract the resource
+ using (var stream = assembly.GetManifestResourceStream(resourceName))
+ using (var fileStream = File.Create(targetPath))
+ {
+ if (stream == null) continue;
+ stream.CopyTo(fileStream);
+ }
+ }
+
+ // Generate version file after extracting all resources
+ GenerateVersionFile();
+
+ // Verify that md2html.html was extracted
+ var htmlPath = Path.Combine(_resourcePath, "md2html.html");
+ if (!File.Exists(htmlPath))
+ {
+ throw new FileNotFoundException($"Required template file md2html.html not found in resources. Available resources: {string.Join(", ", resourceNames)}");
+ }
+ }
+
+ private class VersionInfo
+ {
+ public string EmbeddedHash { get; set; }
+ public string ExtractedHash { get; set; }
+
+ public VersionInfo(string embeddedHash, string extractedHash)
+ {
+ EmbeddedHash = embeddedHash;
+ ExtractedHash = extractedHash;
+ }
+ }
+
+ private static string CalculateDirectoryHash(string directory)
+ {
+ var files = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories)
+ .Where(f => !f.EndsWith(".version"))
+ .OrderBy(f => f)
+ .ToList();
+
+ using (var sha256 = SHA256.Create())
+ {
+ var combinedBytes = new List();
+ foreach (var file in files)
+ {
+ var relativePath = file.Substring(directory.Length + 1);
+ var pathBytes = Encoding.UTF8.GetBytes(relativePath.ToLowerInvariant());
+ combinedBytes.AddRange(pathBytes);
+
+ var contentBytes = File.ReadAllBytes(file);
+ combinedBytes.AddRange(contentBytes);
+ }
+
+ var hash = sha256.ComputeHash(combinedBytes.ToArray());
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ private string GetEmbeddedResourcesHash()
+ {
+ try
+ {
+ return EmbeddedResourcesHash.Hash;
+ }
+ catch (Exception)
+ {
+ Debug.WriteLine("QuickLook.Plugin.MarkdownViewer: Embedded resources hash file not found.");
+ return CalculateEmbeddedResourcesHash();
+ }
+ }
+
+ private string CalculateEmbeddedResourcesHash()
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using (var sha256 = SHA256.Create())
+ {
+ var combinedBytes = new List();
+ var resourceNames = assembly.GetManifestResourceNames()
+ .Where(name => name.StartsWith(_resourcePrefix) &&
+ !name.EndsWith(".version"))
+ .OrderBy(name => name)
+ .ToList();
+
+ foreach (var resourceName in resourceNames)
+ {
+ var nameBytes = Encoding.UTF8.GetBytes(resourceName.ToLowerInvariant());
+ combinedBytes.AddRange(nameBytes);
+
+ using (var stream = assembly.GetManifestResourceStream(resourceName))
+ {
+ if (stream == null) continue;
+ var buffer = new byte[stream.Length];
+ stream.Read(buffer, 0, buffer.Length);
+ combinedBytes.AddRange(buffer);
+ }
+ }
+
+ var hash = sha256.ComputeHash(combinedBytes.ToArray());
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ private void GenerateVersionFile()
+ {
+ var extractedHash = CalculateDirectoryHash(_resourcePath);
+ var newVersionContent = $"{_embeddedHash}{Environment.NewLine}{extractedHash}";
+ File.WriteAllText(_versionFilePath, newVersionContent);
+ }
+
+ private void UpdateVersionFileEmbeddedHash()
+ {
+ var versionInfo = ReadVersionFile() ?? throw new InvalidOperationException("Cannot update version file: no existing version file found");
+
+ var newVersionContent = $"{_embeddedHash}{Environment.NewLine}{versionInfo.ExtractedHash}";
+ File.WriteAllText(_versionFilePath, newVersionContent);
+ }
+
+ private VersionInfo? ReadVersionFile()
+ {
+ if (!File.Exists(_versionFilePath)) return null;
+
+ var lines = File.ReadAllLines(_versionFilePath);
+ if (lines.Length < 2) return null;
+
+ return new VersionInfo(lines[0], lines[1]);
+ }
+ }
+}