Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
4 changes: 2 additions & 2 deletions module/PSCompression.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @()
Expand Down
33 changes: 33 additions & 0 deletions module/PSCompression.psm1
Original file line number Diff line number Diff line change
@@ -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))
}
}
73 changes: 73 additions & 0 deletions src/PSCompression.Shared/LoadContext.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
14 changes: 14 additions & 0 deletions src/PSCompression.Shared/PSCompression.Shared.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<AssemblyName>PSCompression.Shared</AssemblyName>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="PSCompression" />
</ItemGroup>

</Project>
23 changes: 23 additions & 0 deletions src/PSCompression.Shared/SharedUtil.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> data)
{
Assembly asm = type.Assembly;

data["Assembly"] = new Dictionary<string, object?>()
{
["Name"] = asm.GetName().FullName,
["ALC"] = AssemblyLoadContext.GetLoadContext(asm)?.Name,
["Location"] = asm.Location
};
}
}
2 changes: 2 additions & 0 deletions src/PSCompression/Abstractions/EntryBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace PSCompression.Abstractions;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/PSCompression/Abstractions/TarEntryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal FileSystemInfo ExtractTo(
}

FileInfo file = new(destination);
file.Directory.Create();
file.Directory?.Create();

using FileStream destStream = File.Open(
destination,
Expand Down
4 changes: 2 additions & 2 deletions src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -116,7 +116,7 @@ private void Traverse(DirectoryInfo dir, T archive)
{
_queue.Enqueue(dir);
IEnumerable<FileSystemInfo> enumerator;
int length = dir.Parent.FullName.Length + 1;
int length = dir.Parent!.FullName.Length + 1;

while (_queue.Count > 0)
{
Expand Down
11 changes: 7 additions & 4 deletions src/PSCompression/Abstractions/ZipEntryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;
}
}
3 changes: 1 addition & 2 deletions src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.IO;
using System.IO.Compression;
using System.Management.Automation;
using BrotliSharpLib;
using PSCompression.Abstractions;

namespace PSCompression.Commands;
Expand All @@ -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);
}
4 changes: 3 additions & 1 deletion src/PSCompression/Commands/ExpandTarArchiveCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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,
Expand Down
15 changes: 8 additions & 7 deletions src/PSCompression/Commands/NewZipEntryCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
public string[]? EntryPath
{
get => _entryPath;
set => _entryPath = [.. value.Select(e => e.NormalizePath())];
set => _entryPath = [.. value!.Select(e => e.NormalizePath())];
}

[Parameter]
Expand Down Expand Up @@ -194,17 +194,18 @@

foreach (ZipArchiveEntry entry in _entries)
{
if (!zip.TryGetEntry(entry.FullName, out ZipArchiveEntry? zipEntry))
{
continue;

Check warning on line 199 in src/PSCompression/Commands/NewZipEntryCommand.cs

View check run for this annotation

Codecov / codecov/patch

src/PSCompression/Commands/NewZipEntryCommand.cs#L198-L199

Added lines #L198 - L199 were not covered by tests
}

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();
Expand Down
12 changes: 7 additions & 5 deletions src/PSCompression/Extensions/CompressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/PSCompression/Extensions/PathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileInfo>(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);
Expand Down
4 changes: 2 additions & 2 deletions src/PSCompression/OnModuleImportAndRemove.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void OnRemove(PSModuleInfo module)
/// <returns></returns>
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<string> directoriesToSearch = [];

if (!string.IsNullOrEmpty(libDirectory))
Expand Down Expand Up @@ -85,6 +85,6 @@ public void OnRemove(PSModuleInfo module)
/// Determine if the current runtime is .NET Framework
/// </summary>
/// <returns></returns>
private bool IsNetFramework() => RuntimeInformation.FrameworkDescription
private static bool IsNetFramework() => RuntimeInformation.FrameworkDescription
.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
}
Loading