Add certificate viewer plugin

Introduces QuickLook.Plugin.CertViewer for viewing certificate files (.pfx, .cer, .pem, etc.) in QuickLook. The plugin loads and displays certificate details or raw content, and is integrated into the solution and project files.
This commit is contained in:
ema
2025-12-23 14:15:52 +08:00
parent 154ec05528
commit dba41ac890
8 changed files with 390 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
using System.Security.Cryptography.X509Certificates;
namespace QuickLook.Plugin.CertViewer;
internal sealed class CertLoadResult
{
public bool Success { get; }
public X509Certificate2 Certificate { get; }
public string Message { get; }
public string RawContent { get; }
public CertLoadResult(bool success, X509Certificate2 certificate, string message, string rawContent)
{
Success = success;
Certificate = certificate;
Message = message;
RawContent = rawContent;
}
public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent)
=> new CertLoadResult(success, certificate, message, rawContent);
}

View File

@@ -0,0 +1,111 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
namespace QuickLook.Plugin.CertViewer;
internal static class CertUtils
{
/// <summary>
/// TryLoadCertificate returns a <see cref="CertLoadResult"/> containing:
/// - Success: whether loading/parsing succeeded
/// - Certificate: the parsed X509Certificate2 (may be null)
/// - Message: an informational or error message
/// - RawContent: original file text or hex when parsing failed
/// </summary>
public static CertLoadResult TryLoadCertificate(string path)
{
try
{
var ext = Path.GetExtension(path)?.ToLowerInvariant();
if (ext == ".pfx" || ext == ".p12")
{
try
{
var cert = new X509Certificate2(path);
return new CertLoadResult(true, cert, string.Empty, null);
}
catch (Exception ex)
{
return new CertLoadResult(false, null, "Failed to load PFX/P12: " + ex.Message, null);
}
}
// Try DER/PEM style cert (.cer/.crt/.pem)
var text = File.ReadAllText(path);
const string begin = "-----BEGIN CERTIFICATE-----";
const string end = "-----END CERTIFICATE-----";
if (text.Contains(begin))
{
var startIdx = text.IndexOf(begin, StringComparison.Ordinal);
var endIdx = text.IndexOf(end, StringComparison.Ordinal);
if (startIdx >= 0 && endIdx > startIdx)
{
var b64 = text.Substring(startIdx + begin.Length, endIdx - (startIdx + begin.Length));
b64 = new string(b64.Where(c => !char.IsWhiteSpace(c)).ToArray());
try
{
var raw = Convert.FromBase64String(b64);
var cert = new X509Certificate2(raw);
return new CertLoadResult(true, cert, string.Empty, text);
}
catch (Exception ex)
{
return new CertLoadResult(false, null, "PEM decode failed: " + ex.Message, text);
}
}
}
// Try raw DER
try
{
var bytes = File.ReadAllBytes(path);
// heuristics: if starts with 0x30 (ASN.1 SEQUENCE) it's likely DER encoded
if (bytes.Length > 0 && bytes[0] == 0x30)
{
try
{
var cert = new X509Certificate2(bytes);
return new CertLoadResult(true, cert, string.Empty, null);
}
catch
{
// not a certificate DER
}
}
}
catch
{
}
// Unsupported or not parseable: return raw text or hex
try
{
var rawText = File.ReadAllText(path);
return new CertLoadResult(false, null, "Could not parse as certificate; showing raw content.", rawText);
}
catch
{
// fallback to hex
try
{
var bytes = File.ReadAllBytes(path);
var hex = BitConverter.ToString(bytes).Replace("-", " ");
return new CertLoadResult(false, null, "Could not parse as certificate; showing hex.", hex);
}
catch (Exception ex)
{
return new CertLoadResult(false, null, "Failed to read file: " + ex.Message, null);
}
}
}
catch (Exception ex)
{
return new CertLoadResult(false, null, "Internal error: " + ex.Message, null);
}
}
}

View File

@@ -0,0 +1,42 @@
<UserControl x:Class="QuickLook.Plugin.CertViewer.CertViewerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="600"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView x:Name="PropertyList"
Grid.Column="0"
Margin="8">
<ListView.View>
<GridView>
<GridViewColumn Width="150"
DisplayMemberBinding="{Binding Key}"
Header="Field" />
<GridViewColumn Width="300"
DisplayMemberBinding="{Binding Value}"
Header="Value" />
</GridView>
</ListView.View>
</ListView>
<TextBox x:Name="RawText"
Grid.Column="1"
Margin="8"
AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Windows.Controls;
namespace QuickLook.Plugin.CertViewer;
public partial class CertViewerControl : UserControl, IDisposable
{
public CertViewerControl()
{
InitializeComponent();
}
public void LoadCertificate(X509Certificate2 cert)
{
var items = new List<KeyValuePair<string, string>>
{
new("Subject", cert.Subject),
new("Issuer", cert.Issuer),
new("Thumbprint", cert.Thumbprint),
new("SerialNumber", cert.SerialNumber),
new("NotBefore", cert.NotBefore.ToString()),
new("NotAfter", cert.NotAfter.ToString()),
new("SignatureAlgorithm", cert.SignatureAlgorithm.FriendlyName ?? cert.SignatureAlgorithm.Value),
new("PublicKey", cert.PublicKey.Oid.FriendlyName ?? cert.PublicKey.Oid.Value),
};
PropertyList.ItemsSource = items;
RawText.Text = string.Empty;
}
public void LoadRaw(string path, string message, string content)
{
PropertyList.ItemsSource = new List<KeyValuePair<string, string>>
{
new("Path", path),
new("Info", message)
};
RawText.Text = content ?? "(No content to display)";
}
public void Dispose()
{
}
}

View File

@@ -0,0 +1,82 @@
using QuickLook.Common.Plugin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
namespace QuickLook.Plugin.CertViewer;
public class Plugin : IViewer
{
private static readonly HashSet<string> WellKnownExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".p12",
".pfx",
".cer",
".crt",
".pem",
".snk",
".pvk",
".spc",
".mobileprovision",
".certSigningRequest",
".csr",
".keystore",
};
private CertViewerControl _control;
private string _currentPath;
public int Priority => 0;
public void Init()
{
}
public bool CanHandle(string path)
{
if (Directory.Exists(path))
return false;
var ext = Path.GetExtension(path);
if (!string.IsNullOrEmpty(ext) && WellKnownExtensions.Contains(ext))
return true;
return false;
}
public void Prepare(string path, ContextObject context)
{
context.PreferredSize = new Size { Width = 800, Height = 600 };
}
public void View(string path, ContextObject context)
{
_currentPath = path;
context.IsBusy = true;
var result = CertUtils.TryLoadCertificate(path);
_control = new CertViewerControl();
if (result.Success && result.Certificate != null)
{
_control.LoadCertificate(result.Certificate);
}
else
{
_control.LoadRaw(path, result.Message, result.RawContent);
}
context.ViewerContent = _control;
context.Title = Path.GetFileName(path);
context.IsBusy = false;
}
public void Cleanup()
{
_control?.Dispose();
_control = null;
}
}

View File

@@ -0,0 +1,81 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net462</TargetFramework>
<RootNamespace>QuickLook.Plugin.CertViewer</RootNamespace>
<AssemblyName>QuickLook.Plugin.CertViewer</AssemblyName>
<FileAlignment>512</FileAlignment>
<SignAssembly>false</SignAssembly>
<UseWPF>true</UseWPF>
<LangVersion>preview</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<ProjectGuid>{C8B9F6D1-1234-4AAB-9C3D-ABCDEF123456}</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="WindowsBase" />
<Reference Include="System" />
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\QuickLook.Common\QuickLook.Common.csproj">
<Project>{85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}</Project>
<Name>QuickLook.Common</Name>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\GitVersion.cs">
<Link>Properties\GitVersion.cs</Link>
</Compile>
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
@@ -21,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.PdfViewer"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.TextViewer", "QuickLook.Plugin\QuickLook.Plugin.TextViewer\QuickLook.Plugin.TextViewer.csproj", "{AE041682-E3A1-44F6-8BB4-916A98D89FBE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.CertViewer", "QuickLook.Plugin\QuickLook.Plugin.CertViewer\QuickLook.Plugin.CertViewer.csproj", "{C8B9F6D1-1234-4AAB-9C3D-ABCDEF123456}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickLook.Plugin.VideoViewer", "QuickLook.Plugin\QuickLook.Plugin.VideoViewer\QuickLook.Plugin.VideoViewer.csproj", "{1B746D92-49A5-4A37-9D75-DCC490393290}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BAE81497-98FA-4A7A-A0FB-2B86C9694B9C}"

View File

@@ -27,6 +27,7 @@
<Project Path="QuickLook.Plugin/QuickLook.Plugin.PEViewer/QuickLook.Plugin.PEViewer.csproj" />
<Project Path="QuickLook.Plugin/QuickLook.Plugin.PluginInstaller/QuickLook.Plugin.PluginInstaller.csproj" />
<Project Path="QuickLook.Plugin/QuickLook.Plugin.TextViewer/QuickLook.Plugin.TextViewer.csproj" />
<Project Path="QuickLook.Plugin/QuickLook.Plugin.CertViewer/QuickLook.Plugin.CertViewer.csproj" />
<Project Path="QuickLook.Plugin/QuickLook.Plugin.ThumbnailViewer/QuickLook.Plugin.ThumbnailViewer.csproj" />
<Project Path="QuickLook.Plugin/QuickLook.Plugin.VideoViewer/QuickLook.Plugin.VideoViewer.csproj" />
</Folder>
@@ -57,6 +58,7 @@
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.PEViewer/QuickLook.Plugin.PEViewer.csproj" />
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.PluginInstaller/QuickLook.Plugin.PluginInstaller.csproj" />
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.TextViewer/QuickLook.Plugin.TextViewer.csproj" />
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.CertViewer/QuickLook.Plugin.CertViewer.csproj" />
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.ThumbnailViewer/QuickLook.Plugin.ThumbnailViewer.csproj" />
<BuildDependency Project="QuickLook.Plugin/QuickLook.Plugin.VideoViewer/QuickLook.Plugin.VideoViewer.csproj" />
<BuildDependency Project="QuickLook/QuickLook.csproj" />