diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1893963..79676b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+## 07/02/2025
+
+- Added `AssemblyLoadContext` support for PowerShell 7 (.NET 8.0) to resolve DLL hell by isolating module dependencies. No support for PowerShell 5.1 (.NET Framework) due to lack of `AssemblyLoadContext` in that runtime.
+
## 06/23/2025
- Added commands supporting several algorithms to compress and decompress strings:
diff --git a/module/PSCompression.psd1 b/module/PSCompression.psd1
index 66574f7..ab55e7f 100644
--- a/module/PSCompression.psd1
+++ b/module/PSCompression.psd1
@@ -8,10 +8,10 @@
@{
# Script module or binary module file associated with this manifest.
- RootModule = 'bin/netstandard2.0/PSCompression.dll'
+ RootModule = 'PSCompression.psm1'
# Version number of this module.
- ModuleVersion = '3.0.0'
+ ModuleVersion = '3.0.1'
# Supported PSEditions
# CompatiblePSEditions = @()
diff --git a/module/PSCompression.psm1 b/module/PSCompression.psm1
new file mode 100644
index 0000000..b85482f
--- /dev/null
+++ b/module/PSCompression.psm1
@@ -0,0 +1,33 @@
+using namespace System.IO
+using namespace System.Reflection
+using namespace PSCompression.Shared
+
+$moduleName = [Path]::GetFileNameWithoutExtension($PSCommandPath)
+$frame = 'net8.0'
+
+if (-not $IsCoreCLR) {
+ $frame = 'netstandard2.0'
+ $asm = [Path]::Combine($PSScriptRoot, 'bin', $frame, "${moduleName}.dll")
+ Import-Module -Name $asm -ErrorAction Stop -PassThru
+ return
+}
+
+$context = [Path]::Combine($PSScriptRoot, 'bin', $frame, "${moduleName}.Shared.dll")
+$isReload = $true
+
+if (-not ("${moduleName}.Shared.LoadContext" -as [type])) {
+ $isReload = $false
+ Add-Type -Path $context
+}
+
+$mainModule = [LoadContext]::Initialize()
+$innerMod = Import-Module -Assembly $mainModule -PassThru:$isReload
+
+if ($innerMod) {
+ $addExportedCmdlet = [psmoduleinfo].GetMethod(
+ 'AddExportedCmdlet', [BindingFlags] 'Instance, NonPublic')
+
+ foreach ($cmd in $innerMod.ExportedCmdlets.Values) {
+ $addExportedCmdlet.Invoke($ExecutionContext.SessionState.Module, @($cmd))
+ }
+}
diff --git a/src/PSCompression.Shared/LoadContext.cs b/src/PSCompression.Shared/LoadContext.cs
new file mode 100644
index 0000000..048bb01
--- /dev/null
+++ b/src/PSCompression.Shared/LoadContext.cs
@@ -0,0 +1,73 @@
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace PSCompression.Shared;
+
+[ExcludeFromCodeCoverage]
+public sealed class LoadContext : AssemblyLoadContext
+{
+ private static LoadContext? _instance;
+
+ private readonly static object s_sync = new();
+
+ private readonly Assembly _thisAssembly;
+
+ private readonly AssemblyName _thisAssemblyName;
+
+ private readonly Assembly _moduleAssembly;
+
+ private readonly string _assemblyDir;
+
+ private LoadContext(string mainModulePathAssemblyPath)
+ : base(name: "PSCompression", isCollectible: false)
+ {
+ _assemblyDir = Path.GetDirectoryName(mainModulePathAssemblyPath) ?? "";
+ _thisAssembly = typeof(LoadContext).Assembly;
+ _thisAssemblyName = _thisAssembly.GetName();
+ _moduleAssembly = LoadFromAssemblyPath(mainModulePathAssemblyPath);
+ }
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (AssemblyName.ReferenceMatchesDefinition(_thisAssemblyName, assemblyName))
+ {
+ return _thisAssembly;
+ }
+
+ string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll");
+ if (File.Exists(asmPath))
+ {
+ return LoadFromAssemblyPath(asmPath);
+ }
+
+ return null;
+ }
+
+ public static Assembly Initialize()
+ {
+ LoadContext? instance = _instance;
+ if (instance is not null)
+ {
+ return instance._moduleAssembly;
+ }
+
+ lock (s_sync)
+ {
+ if (_instance is not null)
+ {
+ return _instance._moduleAssembly;
+ }
+
+ string assemblyPath = typeof(LoadContext).Assembly.Location;
+ string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
+ string moduleName = assemblyName[..^7];
+ string modulePath = Path.Combine(
+ Path.GetDirectoryName(assemblyPath)!,
+ $"{moduleName}.dll");
+
+ return new LoadContext(modulePath)._moduleAssembly;
+ }
+ }
+}
diff --git a/src/PSCompression.Shared/PSCompression.Shared.csproj b/src/PSCompression.Shared/PSCompression.Shared.csproj
new file mode 100644
index 0000000..fb4da02
--- /dev/null
+++ b/src/PSCompression.Shared/PSCompression.Shared.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ PSCompression.Shared
+ latest
+
+
+
+
+
+
+
diff --git a/src/PSCompression.Shared/SharedUtil.cs b/src/PSCompression.Shared/SharedUtil.cs
new file mode 100644
index 0000000..0706d93
--- /dev/null
+++ b/src/PSCompression.Shared/SharedUtil.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace PSCompression.Shared;
+
+[ExcludeFromCodeCoverage]
+internal sealed class SharedUtil
+{
+ public static void AddAssemblyInfo(Type type, Dictionary data)
+ {
+ Assembly asm = type.Assembly;
+
+ data["Assembly"] = new Dictionary()
+ {
+ ["Name"] = asm.GetName().FullName,
+ ["ALC"] = AssemblyLoadContext.GetLoadContext(asm)?.Name,
+ ["Location"] = asm.Location
+ };
+ }
+}
diff --git a/src/PSCompression/Abstractions/EntryBase.cs b/src/PSCompression/Abstractions/EntryBase.cs
index 78847fe..99a5b1f 100644
--- a/src/PSCompression/Abstractions/EntryBase.cs
+++ b/src/PSCompression/Abstractions/EntryBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace PSCompression.Abstractions;
@@ -11,6 +12,7 @@ public abstract class EntryBase(string source)
internal string? FormatDirectoryPath { get => _formatDirectoryPath ??= GetFormatDirectoryPath(); }
+ [MemberNotNullWhen(true, nameof(_stream))]
internal bool FromStream { get => _stream is not null; }
public string Source { get; } = source;
diff --git a/src/PSCompression/Abstractions/TarEntryBase.cs b/src/PSCompression/Abstractions/TarEntryBase.cs
index b83a4b4..bdb6bd7 100644
--- a/src/PSCompression/Abstractions/TarEntryBase.cs
+++ b/src/PSCompression/Abstractions/TarEntryBase.cs
@@ -36,7 +36,7 @@ internal FileSystemInfo ExtractTo(
}
FileInfo file = new(destination);
- file.Directory.Create();
+ file.Directory?.Create();
using FileStream destStream = File.Open(
destination,
diff --git a/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs b/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs
index 2338d13..90eefef 100644
--- a/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs
+++ b/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs
@@ -69,7 +69,7 @@ protected override void BeginProcessing()
try
{
- Directory.CreateDirectory(IOPath.GetDirectoryName(Destination));
+ Directory.CreateDirectory(IOPath.GetDirectoryName(Destination)!);
_destination = File.Open(Destination, FileMode);
_archive = CreateCompressionStream(_destination);
@@ -116,7 +116,7 @@ private void Traverse(DirectoryInfo dir, T archive)
{
_queue.Enqueue(dir);
IEnumerable enumerator;
- int length = dir.Parent.FullName.Length + 1;
+ int length = dir.Parent!.FullName.Length + 1;
while (_queue.Count > 0)
{
diff --git a/src/PSCompression/Abstractions/ZipEntryBase.cs b/src/PSCompression/Abstractions/ZipEntryBase.cs
index d4a4344..d6ecaf0 100644
--- a/src/PSCompression/Abstractions/ZipEntryBase.cs
+++ b/src/PSCompression/Abstractions/ZipEntryBase.cs
@@ -97,7 +97,7 @@ internal ZipArchive OpenZip(ZipArchiveMode mode) =>
public FileSystemInfo ExtractTo(string destination, bool overwrite)
{
- using ZipArchive zip = _stream is not null
+ using ZipArchive zip = FromStream
? new ZipArchive(_stream, ZipArchiveMode.Read, leaveOpen: true)
: ZipFile.OpenRead(Source);
@@ -120,10 +120,13 @@ internal FileSystemInfo ExtractTo(
}
FileInfo file = new(destination);
- file.Directory.Create();
+ file.Directory?.Create();
+
+ if (zip.TryGetEntry(RelativePath, out ZipArchiveEntry? entry))
+ {
+ entry.ExtractToFile(destination, overwrite);
+ }
- ZipArchiveEntry entry = zip.GetEntry(RelativePath);
- entry.ExtractToFile(destination, overwrite);
return file;
}
}
diff --git a/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs b/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs
index dfe29a7..7de4ec9 100644
--- a/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs
+++ b/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs
@@ -1,7 +1,6 @@
using System.IO;
using System.IO.Compression;
using System.Management.Automation;
-using BrotliSharpLib;
using PSCompression.Abstractions;
namespace PSCompression.Commands;
@@ -12,5 +11,5 @@ namespace PSCompression.Commands;
public sealed class ConvertFromBrotliStringCommand : FromCompressedStringCommandBase
{
protected override Stream CreateDecompressionStream(Stream inputStream) =>
- new BrotliStream(inputStream, CompressionMode.Decompress);
+ new BrotliSharpLib.BrotliStream(inputStream, CompressionMode.Decompress);
}
diff --git a/src/PSCompression/Commands/ExpandTarArchiveCommand.cs b/src/PSCompression/Commands/ExpandTarArchiveCommand.cs
index 669e9eb..8a1ee7d 100644
--- a/src/PSCompression/Commands/ExpandTarArchiveCommand.cs
+++ b/src/PSCompression/Commands/ExpandTarArchiveCommand.cs
@@ -113,6 +113,8 @@ private FileSystemInfo[] ExtractArchive(string path)
private FileSystemInfo ExtractEntry(TarEntry entry, TarInputStream tar)
{
+ Dbg.Assert(Destination is not null);
+
string destination = IO.Path.GetFullPath(
IO.Path.Combine(Destination, entry.Name));
@@ -124,7 +126,7 @@ private FileSystemInfo ExtractEntry(TarEntry entry, TarInputStream tar)
}
FileInfo file = new(destination);
- file.Directory.Create();
+ file.Directory?.Create();
using (FileStream destStream = File.Open(
destination,
diff --git a/src/PSCompression/Commands/NewZipEntryCommand.cs b/src/PSCompression/Commands/NewZipEntryCommand.cs
index 3de6656..770ee4a 100644
--- a/src/PSCompression/Commands/NewZipEntryCommand.cs
+++ b/src/PSCompression/Commands/NewZipEntryCommand.cs
@@ -40,7 +40,7 @@ public sealed class NewZipEntryCommand : PSCmdlet, IDisposable
public string[]? EntryPath
{
get => _entryPath;
- set => _entryPath = [.. value.Select(e => e.NormalizePath())];
+ set => _entryPath = [.. value!.Select(e => e.NormalizePath())];
}
[Parameter]
@@ -194,17 +194,18 @@ private IEnumerable GetResult()
foreach (ZipArchiveEntry entry in _entries)
{
+ if (!zip.TryGetEntry(entry.FullName, out ZipArchiveEntry? zipEntry))
+ {
+ continue;
+ }
+
if (string.IsNullOrEmpty(entry.Name))
{
- _result.Add(new ZipEntryDirectory(
- zip.GetEntry(entry.FullName),
- Destination));
+ _result.Add(new ZipEntryDirectory(zipEntry, Destination));
continue;
}
- _result.Add(new ZipEntryFile(
- zip.GetEntry(entry.FullName),
- Destination));
+ _result.Add(new ZipEntryFile(zipEntry, Destination));
}
return _result.ToEntrySort();
diff --git a/src/PSCompression/Extensions/CompressionExtensions.cs b/src/PSCompression/Extensions/CompressionExtensions.cs
index a1fe7ca..0480b14 100644
--- a/src/PSCompression/Extensions/CompressionExtensions.cs
+++ b/src/PSCompression/Extensions/CompressionExtensions.cs
@@ -5,7 +5,6 @@
using System.IO.Compression;
using System.Management.Automation;
using System.Text.RegularExpressions;
-using BrotliSharpLib;
using ICSharpCode.SharpZipLib.BZip2;
using ICSharpCode.SharpZipLib.Tar;
using SharpCompress.Compressors.LZMA;
@@ -54,7 +53,10 @@ internal static bool TryGetEntry(
this ZipArchive zip,
string path,
[NotNullWhen(true)] out ZipArchiveEntry? entry)
- => (entry = zip.GetEntry(path)) is not null;
+ {
+ entry = zip.GetEntry(path);
+ return entry is not null;
+ }
internal static string ChangeName(
this ZipEntryFile file,
@@ -91,7 +93,7 @@ internal static void WriteAllTextToPipeline(this StreamReader reader, PSCmdlet c
internal static void WriteLinesToPipeline(this StreamReader reader, PSCmdlet cmdlet)
{
- string line;
+ string? line;
while ((line = reader.ReadLine()) is not null)
{
cmdlet.WriteObject(line);
@@ -114,11 +116,11 @@ internal static void WriteContent(this StreamWriter writer, string[] lines)
}
}
- internal static BrotliStream AsBrotliCompressedStream(
+ internal static BrotliSharpLib.BrotliStream AsBrotliCompressedStream(
this Stream stream,
CompressionLevel compressionLevel)
{
- BrotliStream brotli = new(stream, CompressionMode.Compress);
+ BrotliSharpLib.BrotliStream brotli = new(stream, CompressionMode.Compress);
brotli.SetQuality(compressionLevel switch
{
CompressionLevel.NoCompression => 0,
diff --git a/src/PSCompression/Extensions/PathExtensions.cs b/src/PSCompression/Extensions/PathExtensions.cs
index 0a2e5d6..1cb40fe 100644
--- a/src/PSCompression/Extensions/PathExtensions.cs
+++ b/src/PSCompression/Extensions/PathExtensions.cs
@@ -83,14 +83,14 @@ internal static void Create(this DirectoryInfo dir, bool force)
internal static PSObject AppendPSProperties(this FileSystemInfo info)
{
- string parent = info is DirectoryInfo dir
- ? dir.Parent.FullName
+ string? parent = info is DirectoryInfo dir
+ ? dir.Parent?.FullName
: Unsafe.As(info).DirectoryName;
return info.AppendPSProperties(parent);
}
- internal static PSObject AppendPSProperties(this FileSystemInfo info, string parent)
+ internal static PSObject AppendPSProperties(this FileSystemInfo info, string? parent)
{
const string provider = @"Microsoft.PowerShell.Core\FileSystem::";
PSObject pso = PSObject.AsPSObject(info);
diff --git a/src/PSCompression/OnModuleImportAndRemove.cs b/src/PSCompression/OnModuleImportAndRemove.cs
index e100d8b..d507c49 100644
--- a/src/PSCompression/OnModuleImportAndRemove.cs
+++ b/src/PSCompression/OnModuleImportAndRemove.cs
@@ -44,7 +44,7 @@ public void OnRemove(PSModuleInfo module)
///
private static Assembly? MyResolveEventHandler(object? sender, ResolveEventArgs args)
{
- string libDirectory = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location);
+ string? libDirectory = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location);
List directoriesToSearch = [];
if (!string.IsNullOrEmpty(libDirectory))
@@ -85,6 +85,6 @@ public void OnRemove(PSModuleInfo module)
/// Determine if the current runtime is .NET Framework
///
///
- private bool IsNetFramework() => RuntimeInformation.FrameworkDescription
+ private static bool IsNetFramework() => RuntimeInformation.FrameworkDescription
.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
}
diff --git a/src/PSCompression/PSCompression.csproj b/src/PSCompression/PSCompression.csproj
index 4a12f39..e100fb4 100644
--- a/src/PSCompression/PSCompression.csproj
+++ b/src/PSCompression/PSCompression.csproj
@@ -1,7 +1,7 @@
- netstandard2.0
+ netstandard2.0;net8.0
enable
true
PSCompression
@@ -20,6 +20,10 @@
+
+
+
+
true
true
diff --git a/src/PSCompression/SortingOps.cs b/src/PSCompression/SortingOps.cs
index b275207..ae1d6ae 100644
--- a/src/PSCompression/SortingOps.cs
+++ b/src/PSCompression/SortingOps.cs
@@ -8,8 +8,8 @@ namespace PSCompression;
internal static class SortingOps
{
- private static string SortByParent(EntryBase entry) =>
- Path.GetDirectoryName(entry.RelativePath).NormalizeEntryPath();
+ private static string? SortByParent(EntryBase entry) =>
+ Path.GetDirectoryName(entry.RelativePath)?.NormalizeEntryPath();
private static int SortByLength(EntryBase entry) =>
entry.RelativePath.Count(e => e == '/');
diff --git a/src/PSCompression/TarEntryFile.cs b/src/PSCompression/TarEntryFile.cs
index 10773f7..fa847d3 100644
--- a/src/PSCompression/TarEntryFile.cs
+++ b/src/PSCompression/TarEntryFile.cs
@@ -30,7 +30,7 @@ internal TarEntryFile(TarEntry entry, Stream? stream, Algorithm algorithm)
}
protected override string GetFormatDirectoryPath() =>
- $"/{Path.GetDirectoryName(RelativePath).NormalizeEntryPath()}";
+ $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}";
internal bool GetContentStream(Stream destination)
{
diff --git a/src/PSCompression/ZipEntryCache.cs b/src/PSCompression/ZipEntryCache.cs
index c167cf3..3c14c7a 100644
--- a/src/PSCompression/ZipEntryCache.cs
+++ b/src/PSCompression/ZipEntryCache.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO.Compression;
using PSCompression.Abstractions;
+using PSCompression.Extensions;
namespace PSCompression;
@@ -41,13 +42,18 @@ internal IEnumerable GetEntries()
using ZipArchive zip = ZipFile.OpenRead(entry.Key);
foreach ((string path, EntryType type) in entry.Value)
{
+ if (!zip.TryGetEntry(path, out ZipArchiveEntry? zipEntry))
+ {
+ continue;
+ }
+
if (type is EntryType.Archive)
{
- yield return new ZipEntryFile(zip.GetEntry(path), entry.Key);
+ yield return new ZipEntryFile(zipEntry, entry.Key);
continue;
}
- yield return new ZipEntryDirectory(zip.GetEntry(path), entry.Key);
+ yield return new ZipEntryDirectory(zipEntry, entry.Key);
}
}
}
diff --git a/src/PSCompression/ZipEntryFile.cs b/src/PSCompression/ZipEntryFile.cs
index 7d7a7b5..f322517 100644
--- a/src/PSCompression/ZipEntryFile.cs
+++ b/src/PSCompression/ZipEntryFile.cs
@@ -55,11 +55,13 @@ internal void Refresh()
internal void Refresh(ZipArchive zip)
{
- ZipArchiveEntry entry = zip.GetEntry(RelativePath);
- Length = entry.Length;
- CompressedLength = entry.CompressedLength;
+ if (zip.TryGetEntry(RelativePath, out ZipArchiveEntry? entry))
+ {
+ Length = entry.Length;
+ CompressedLength = entry.CompressedLength;
+ }
}
protected override string GetFormatDirectoryPath() =>
- $"/{Path.GetDirectoryName(RelativePath).NormalizeEntryPath()}";
+ $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}";
}
diff --git a/tools/ProjectBuilder/Project.cs b/tools/ProjectBuilder/Project.cs
index 9afcd3a..a84530f 100644
--- a/tools/ProjectBuilder/Project.cs
+++ b/tools/ProjectBuilder/Project.cs
@@ -18,13 +18,22 @@ public sealed class Project
public string? TestFramework
{
- get => _info.PowerShellVersion is { Major: 5, Minor: 1 }
- ? TargetFrameworks
- .Where(e => Regex.Match(e, "^net(?:4|standard)").Success)
- .FirstOrDefault()
- : TargetFrameworks
- .Where(e => !e.StartsWith("net4"))
- .FirstOrDefault();
+ get
+ {
+ if (TargetFrameworks is { Length: 1 })
+ {
+ return TargetFrameworks.First();
+ }
+
+ return _info.PowerShellVersion is { Major: 5, Minor: 1 }
+ ? TargetFrameworks
+ .Where(e => Regex.Match(e, "^net(?:4|standard)").Success)
+ .FirstOrDefault()
+
+ : TargetFrameworks
+ .Where(e => !Regex.Match(e, "^net(?:4|standard)").Success)
+ .FirstOrDefault();
+ }
}
private Configuration Configuration { get => _info.Configuration; }