From 5d5e1a39cfd5cf577458f9a59cd1707925d50078 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 02:20:47 +0100 Subject: [PATCH 01/19] Enhance project structure and versioning - Updated version to 0.17 in version.json - Added new SourceGenerator project with FactoryGenerator - Improved .gitignore to exclude additional build outputs - Modified Directory.Build.props for versioning and compiler settings - Registered new LogicalVolumeFactory and VirtualDiskTransport instances - Enhanced logging in various classes for better traceability - Created generate-packages.ps1 script for automated package generation --- .gitignore | 16 ++ Directory.Build.props | 4 +- DiscUtils.slnx | 3 + Library/Directory.Build.props | 3 + Library/DiscUtils.Core/DiskImageBuilder.cs | 33 +-- Library/DiscUtils.Core/FileSystemManager.cs | 46 +++- .../Internal/LogicalVolumeFactory.cs | 2 +- .../Partitions/PartitionTable.cs | 20 +- Library/DiscUtils.Core/Setup/SetupHelper.cs | 2 + Library/DiscUtils.Core/VirtualDisk.cs | 8 +- Library/DiscUtils.Core/VirtualDiskManager.cs | 156 +++++++++++-- Library/DiscUtils.Core/VolumeManager.cs | 44 +++- Library/DiscUtils/SetupHelper.cs | 7 +- .../DiscUtils.SourceGenerator.csproj | 17 ++ SourceGenerator/FactoryGenerator.cs | 205 ++++++++++++++++++ generate-packages.ps1 | 23 ++ version.json | 2 +- 17 files changed, 517 insertions(+), 74 deletions(-) create mode 100644 SourceGenerator/DiscUtils.SourceGenerator.csproj create mode 100644 SourceGenerator/FactoryGenerator.cs create mode 100644 generate-packages.ps1 diff --git a/.gitignore b/.gitignore index b207fc71c..7d87a18bc 100644 --- a/.gitignore +++ b/.gitignore @@ -386,3 +386,19 @@ artifacts-dotnet-releaser/ # Verify *.received.* *# + +# Build outputs in Library folder +Library/net*/ +Library/netstandard*/ +net10.0/ + +# Build outputs in root +netstandard*/ + +# Build outputs in Tests folder +Tests/net*/ +Tests/netstandard*/ + +# Build outputs in Utilities folder +Utilities/net*/ +Utilities/netstandard*/ diff --git a/Directory.Build.props b/Directory.Build.props index beaf96375..90473f8b1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,11 +13,13 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.77 + 1.0.78 + 1.0.78 true CS1591;CS0649 + false diff --git a/DiscUtils.slnx b/DiscUtils.slnx index 29103d33c..461c48c61 100644 --- a/DiscUtils.slnx +++ b/DiscUtils.slnx @@ -86,4 +86,7 @@ + + + diff --git a/Library/Directory.Build.props b/Library/Directory.Build.props index 15793114e..e593a9b1f 100644 --- a/Library/Directory.Build.props +++ b/Library/Directory.Build.props @@ -26,6 +26,8 @@ $(MSBuildThisFileDirectory)../SigningKey.snk false + false + $(BaseIntermediateOutputPath)Generated false @@ -35,6 +37,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Library/DiscUtils.Core/DiskImageBuilder.cs b/Library/DiscUtils.Core/DiskImageBuilder.cs index 73663b373..b9fb00d58 100644 --- a/Library/DiscUtils.Core/DiskImageBuilder.cs +++ b/Library/DiscUtils.Core/DiskImageBuilder.cs @@ -34,8 +34,6 @@ namespace DiscUtils; /// public abstract class DiskImageBuilder { - private static Dictionary? _typeMap; - /// /// Gets or sets the geometry of this disk, as reported by the BIOS, will be implied from the content stream if not set. /// @@ -61,18 +59,7 @@ public abstract class DiskImageBuilder /// public virtual bool PreservesBiosGeometry => false; - private static Dictionary TypeMap - { - get - { - if (_typeMap == null) - { - InitializeMaps(); - } - - return _typeMap; - } - } + private static Dictionary TypeMap => VirtualDiskManager.TypeMap; /// /// Gets an instance that constructs the specified type (and variant) of virtual disk image. @@ -101,22 +88,4 @@ public static DiskImageBuilder GetBuilder(string type, string variant) /// to each logical file that comprises the disk image. For example, given a base name /// 'foo', the files 'foo.vmdk' and 'foo-flat.vmdk' could be returned. public abstract IEnumerable Build(string baseName); - - [MemberNotNull(nameof(_typeMap))] - private static void InitializeMaps() - { - var typeMap = new Dictionary(); - - foreach (var type in typeof(VirtualDisk).Assembly.GetTypes()) - { - var attr = type.GetCustomAttribute(false); - if (attr != null) - { - var factory = (VirtualDiskFactory)Activator.CreateInstance(type)!; - typeMap.Add(attr.Type, factory); - } - } - - _typeMap = typeMap; - } } \ No newline at end of file diff --git a/Library/DiscUtils.Core/FileSystemManager.cs b/Library/DiscUtils.Core/FileSystemManager.cs index 99a2e5d73..af78c4766 100644 --- a/Library/DiscUtils.Core/FileSystemManager.cs +++ b/Library/DiscUtils.Core/FileSystemManager.cs @@ -38,14 +38,19 @@ namespace DiscUtils; /// public static class FileSystemManager { - private static readonly List _factories; + private static List? _factories; - /// - /// Initializes a new instance of the FileSystemManager class. - /// - static FileSystemManager() + private static List Factories { - _factories = []; + get + { + if (_factories == null) + { + _factories = new List(); + RegisterFileSystems(typeof(FileSystemManager).Assembly); + } + return _factories; + } } /// @@ -54,6 +59,11 @@ static FileSystemManager() /// The detector for the new file systems. public static void RegisterFileSystems(VfsFileSystemFactory factory) { + if (_factories == null) + { + _factories = new List(); + } + lock (_factories) { _factories.Add(factory); @@ -70,6 +80,11 @@ public static void RegisterFileSystems(VfsFileSystemFactory factory) /// public static void RegisterFileSystems(Assembly assembly) { + if (_factories == null) + { + _factories = new List(); + } + lock (_factories) { _factories.AddRange(DetectFactories(assembly)); @@ -99,6 +114,7 @@ public static ReadOnlyCollection DetectFileSystems(Stream stream private static IEnumerable DetectFactories(Assembly assembly) { + Console.WriteLine($"FileSystemManager: Scanning assembly {assembly.FullName} for VfsFileSystemFactories"); foreach (var type in assembly.GetTypes()) { var attrib = type.GetCustomAttribute(false); @@ -107,7 +123,18 @@ private static IEnumerable DetectFactories(Assembly assemb continue; } - yield return (VfsFileSystemFactory)Activator.CreateInstance(type)!; + Console.WriteLine($"FileSystemManager: Found VfsFileSystemFactory: {type.FullName}"); + VfsFileSystemFactory? factory = null; + try + { + factory = (VfsFileSystemFactory)Activator.CreateInstance(type, true)!; + } + catch (Exception ex) + { + Console.WriteLine($"FileSystemManager: Error instantiating {type.FullName}: {ex}"); + throw; + } + yield return factory; } } @@ -116,9 +143,10 @@ private static ReadOnlyCollection DoDetect(Stream stream, Volume var detectStream = new BufferedStream(stream); var detected = new List(); - lock (_factories) + var factories = Factories; + lock (factories) { - foreach (var factory in _factories) + foreach (var factory in factories) { detected.AddRange(factory.Detect(detectStream, volume)); } diff --git a/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs b/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs index d813bb153..37f7ac924 100644 --- a/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs +++ b/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs @@ -24,7 +24,7 @@ namespace DiscUtils.Internal; -internal abstract class LogicalVolumeFactory +public abstract class LogicalVolumeFactory { public abstract bool HandlesPhysicalVolume(PhysicalVolumeInfo volume); diff --git a/Library/DiscUtils.Core/Partitions/PartitionTable.cs b/Library/DiscUtils.Core/Partitions/PartitionTable.cs index dd59c5dfb..9def19505 100644 --- a/Library/DiscUtils.Core/Partitions/PartitionTable.cs +++ b/Library/DiscUtils.Core/Partitions/PartitionTable.cs @@ -54,11 +54,13 @@ public abstract class PartitionTable /// public abstract Geometry? DiskGeometry { get; } + private static List? _factories; + private static List Factories { get { - if (field == null) + if (_factories == null) { var factories = new List(); @@ -66,17 +68,25 @@ private static List Factories { foreach (var attr in type.GetCustomAttributes(false)) { - factories.Add((PartitionTableFactory)Activator.CreateInstance(type)!); + factories.Add((PartitionTableFactory)Activator.CreateInstance(type, true)!); } } - field = factories; + _factories = factories; } - return field; + return _factories; + } + } + + internal static void RegisterPartitionTableFactory(PartitionTableFactory factory) + { + if (_factories == null) + { + _factories = new List(); } - set; + _factories.Add(factory); } /// diff --git a/Library/DiscUtils.Core/Setup/SetupHelper.cs b/Library/DiscUtils.Core/Setup/SetupHelper.cs index 8088ba71e..6543630dc 100644 --- a/Library/DiscUtils.Core/Setup/SetupHelper.cs +++ b/Library/DiscUtils.Core/Setup/SetupHelper.cs @@ -25,10 +25,12 @@ static SetupHelper() /// public static void RegisterAssembly(Assembly assembly) { + Console.WriteLine($"SetupHelper: Registering assembly {assembly.FullName}"); lock (_alreadyLoaded) { if (!_alreadyLoaded.Add(assembly.FullName ?? assembly.GetName().FullName)) { + Console.WriteLine($"SetupHelper: Assembly {assembly.FullName} already registered."); return; } diff --git a/Library/DiscUtils.Core/VirtualDisk.cs b/Library/DiscUtils.Core/VirtualDisk.cs index ee4cfe228..9ff177862 100644 --- a/Library/DiscUtils.Core/VirtualDisk.cs +++ b/Library/DiscUtils.Core/VirtualDisk.cs @@ -375,12 +375,12 @@ public static VirtualDisk CreateDisk(DiscFileSystem fileSystem, string type, str var uri = PathToUri(path); VirtualDisk result; - if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme, out var transportType)) + if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme, out var transportFactory)) { throw new FileNotFoundException($"Unable to parse path '{path}'", path); } - var transport = (VirtualDiskTransport)Activator.CreateInstance(transportType)!; + var transport = transportFactory(); try { @@ -499,12 +499,12 @@ public static VirtualDisk CreateDisk(DiscFileSystem fileSystem, string type, str var uri = PathToUri(path); VirtualDisk? result = null; - if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme, out var transportType)) + if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme, out var transportFactory)) { throw new FileNotFoundException($"Unable to parse path '{uri}'", path); } - var transport = (VirtualDiskTransport)Activator.CreateInstance(transportType)!; + var transport = transportFactory(); try { diff --git a/Library/DiscUtils.Core/VirtualDiskManager.cs b/Library/DiscUtils.Core/VirtualDiskManager.cs index b6179de34..5191decd7 100644 --- a/Library/DiscUtils.Core/VirtualDiskManager.cs +++ b/Library/DiscUtils.Core/VirtualDiskManager.cs @@ -10,15 +10,33 @@ namespace DiscUtils; /// public static class VirtualDiskManager { - static VirtualDiskManager() + private static Dictionary? _extensionMap; + private static Dictionary? _typeMap; + private static Dictionary>? _diskTransports; + + internal static Dictionary> DiskTransports { - ExtensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - TypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - DiskTransports = new Dictionary(StringComparer.OrdinalIgnoreCase); + get + { + if (_diskTransports == null) + { + InitializeMaps(); + } + return _diskTransports!; + } } - internal static Dictionary DiskTransports { get; } - internal static Dictionary ExtensionMap { get; } + internal static Dictionary ExtensionMap + { + get + { + if (_extensionMap == null) + { + InitializeMaps(); + } + return _extensionMap!; + } + } /// /// Gets the set of disk formats supported as an array of file extensions. @@ -30,32 +48,144 @@ static VirtualDiskManager() /// public static ICollection SupportedDiskTypes => TypeMap.Keys; - internal static Dictionary TypeMap { get; } + internal static Dictionary TypeMap + { + get + { + if (_typeMap == null) + { + InitializeMaps(); + } + return _typeMap!; + } + } + + private static void InitializeMaps() + { + if (_typeMap == null) + { + _extensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _typeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _diskTransports = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Auto-scan Core assembly if not initialized + RegisterVirtualDiskTypes(typeof(VirtualDiskManager).Assembly); + } + } + + private static void EnsureInitialized() + { + if (_typeMap == null) + { + _extensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _typeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _diskTransports = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + } + + /// + /// Registers a VirtualDiskFactory instance. + /// + /// The factory to register. + public static void RegisterVirtualDiskFactory(VirtualDiskFactory factory) + { + EnsureInitialized(); + + var type = factory.GetType(); + var diskFactoryAttribute = type.GetCustomAttribute(false); + if (diskFactoryAttribute != null) + { + if (!TypeMap.ContainsKey(diskFactoryAttribute.Type)) + { + TypeMap.Add(diskFactoryAttribute.Type, factory); + } + + foreach (var extension in diskFactoryAttribute.FileExtensions) + { + if (!ExtensionMap.ContainsKey(extension)) + { + ExtensionMap.Add(extension, factory); + } + } + } + } + + /// + /// Registers a VirtualDiskTransport type. + /// + /// The URI scheme. + /// The type implementing VirtualDiskTransport. + public static void RegisterVirtualDiskTransport(string scheme, Type type) + { + EnsureInitialized(); + if (!DiskTransports.ContainsKey(scheme)) + { + DiskTransports.Add(scheme, () => (VirtualDiskTransport)Activator.CreateInstance(type, true)!); + } + } + + /// + /// Registers a VirtualDiskTransport factory. + /// + /// The URI scheme. + /// The factory method. + internal static void RegisterVirtualDiskTransport(string scheme, Func factory) + { + EnsureInitialized(); + if (!DiskTransports.ContainsKey(scheme)) + { + DiskTransports.Add(scheme, factory); + } + } /// - /// Locates VirtualDiskFactory factories attributed with VirtualDiskFactoryAttribute, and types marked with VirtualDiskTransportAttribute, that are able to work with Virtual Disk types. + /// Locates VirtualDiskFactory factories attributed with VirtualDiskFactoryAttribute, and types marked with VirtualDiskTransportAttribute, + /// that are able to work with Virtual Disk types. /// /// An assembly to scan public static void RegisterVirtualDiskTypes(Assembly assembly) { + Console.WriteLine($"VirtualDiskManager: Scanning assembly {assembly.FullName} for VirtualDiskTypes"); + EnsureInitialized(); foreach (var type in assembly.GetTypes()) { var diskFactoryAttribute = type.GetCustomAttribute(false); if (diskFactoryAttribute != null) { - var factory = (VirtualDiskFactory)Activator.CreateInstance(type)!; - TypeMap.Add(diskFactoryAttribute.Type, factory); + Console.WriteLine($"VirtualDiskManager: Found VirtualDiskFactory: {type.FullName}"); + try + { + var factory = (VirtualDiskFactory)Activator.CreateInstance(type, true)!; + TypeMap.Add(diskFactoryAttribute.Type, factory); - foreach (var extension in diskFactoryAttribute.FileExtensions) + foreach (var extension in diskFactoryAttribute.FileExtensions) + { + ExtensionMap.Add(extension, factory); + } + } + catch (Exception ex) { - ExtensionMap.Add(extension, factory); + Console.WriteLine($"VirtualDiskManager: Error instantiating {type.FullName}: {ex}"); + throw; } } var diskTransportAttribute = type.GetCustomAttribute(false); if (diskTransportAttribute != null) { - DiskTransports.Add(diskTransportAttribute.Scheme, type); + Console.WriteLine($"VirtualDiskManager: Found VirtualDiskTransport: {type.FullName} for scheme {diskTransportAttribute.Scheme}"); + DiskTransports.Add(diskTransportAttribute.Scheme, () => + { + try + { + return (VirtualDiskTransport)Activator.CreateInstance(type, true)!; + } + catch (Exception ex) + { + Console.WriteLine($"VirtualDiskManager: Error instantiating transport {type.FullName}: {ex}"); + throw; + } + }); } } } diff --git a/Library/DiscUtils.Core/VolumeManager.cs b/Library/DiscUtils.Core/VolumeManager.cs index 417f09fc3..a296cb266 100644 --- a/Library/DiscUtils.Core/VolumeManager.cs +++ b/Library/DiscUtils.Core/VolumeManager.cs @@ -82,26 +82,45 @@ public VolumeManager(Stream initialDiskContent) private static readonly object _syncObj = new(); + private static ConcurrentBag? _logicalVolumeFactories; + private static ConcurrentBag LogicalVolumeFactories { get { - if (field == null) + if (_logicalVolumeFactories == null) { lock (_syncObj) { - if (field == null) + if (_logicalVolumeFactories == null) { - var factories = new ConcurrentBag(GetLogicalVolumeFactories(_coreAssembly)); - field = factories; + _logicalVolumeFactories = new ConcurrentBag(GetLogicalVolumeFactories(_coreAssembly)); } } } - return field; + return _logicalVolumeFactories; + } + } + + /// + /// Register a new LogicalVolumeFactory instance. + /// + /// The factory to register. + public static void RegisterLogicalVolumeFactory(LogicalVolumeFactory factory) + { + if (_logicalVolumeFactories == null) + { + lock (_syncObj) + { + if (_logicalVolumeFactories == null) + { + _logicalVolumeFactories = new ConcurrentBag(); + } + } } - set; + _logicalVolumeFactories.Add(factory); } private static IEnumerable GetLogicalVolumeFactories(Assembly assembly) @@ -110,7 +129,18 @@ private static IEnumerable GetLogicalVolumeFactories(Assem { foreach (var attr in type.GetCustomAttributes(false)) { - yield return (LogicalVolumeFactory)Activator.CreateInstance(type)!; + Console.WriteLine($"VolumeManager: Found LogicalVolumeFactory {type.FullName} in {assembly.FullName}"); + LogicalVolumeFactory? factory = null; + try + { + factory = (LogicalVolumeFactory)Activator.CreateInstance(type, true)!; + } + catch (Exception ex) + { + Console.WriteLine($"VolumeManager: Error instantiating {type.FullName}: {ex}"); + throw; + } + yield return factory; } } } diff --git a/Library/DiscUtils/SetupHelper.cs b/Library/DiscUtils/SetupHelper.cs index 9e68e892d..e040eab03 100644 --- a/Library/DiscUtils/SetupHelper.cs +++ b/Library/DiscUtils/SetupHelper.cs @@ -6,6 +6,7 @@ using DiscUtils.Fat; using DiscUtils.HfsPlus; using DiscUtils.Iso9660; +using DiscUtils.Lvm; using DiscUtils.Nfs; using DiscUtils.Ntfs; using DiscUtils.OpticalDisk; @@ -22,7 +23,7 @@ namespace DiscUtils.Complete; public static class SetupHelper { - public static void SetupComplete() + public static void SetupComplete() { Setup.SetupHelper.RegisterAssembly(typeof(Store).Assembly); Setup.SetupHelper.RegisterAssembly(typeof(Disk).Assembly); @@ -53,4 +54,8 @@ public static void SetupComplete() Setup.SetupHelper.RegisterAssembly(typeof(Xva.Disk).Assembly); Setup.SetupHelper.RegisterAssembly(typeof(Lvm.LogicalVolumeManager).Assembly); } + public static void SetupCompleteAot() + { + DiscUtils.Setup.GeneratedSetupHelper.RegisterFactories(); + } } \ No newline at end of file diff --git a/SourceGenerator/DiscUtils.SourceGenerator.csproj b/SourceGenerator/DiscUtils.SourceGenerator.csproj new file mode 100644 index 000000000..11e06ed46 --- /dev/null +++ b/SourceGenerator/DiscUtils.SourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + true + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/SourceGenerator/FactoryGenerator.cs b/SourceGenerator/FactoryGenerator.cs new file mode 100644 index 000000000..0d76401f2 --- /dev/null +++ b/SourceGenerator/FactoryGenerator.cs @@ -0,0 +1,205 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +namespace DiscUtils.SourceGenerator +{ + [Generator] + public class FactoryGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Debugging helper - uncomment to attach debugger + // if (!System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Launch(); + + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsSyntaxTargetForGeneration, + transform: GetSemanticTargetForGeneration) + .Where(m => m != null); + + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + + context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(spc, source.Left, source.Right)); + } + + private bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken cancellationToken) + { + return node is ClassDeclarationSyntax cds && cds.BaseList != null; + } + + private INamedTypeSymbol? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol; + + if (symbol == null) return null; + + if (InheritsFrom(symbol, "DiscUtils.Vfs.VfsFileSystemFactory") || + InheritsFrom(symbol, "DiscUtils.Internal.VirtualDiskFactory") || + InheritsFrom(symbol, "DiscUtils.Internal.LogicalVolumeFactory") || + InheritsFrom(symbol, "DiscUtils.Internal.VirtualDiskTransport") || + InheritsFrom(symbol, "DiscUtils.Partitions.PartitionTableFactory")) + { + return symbol; + } + + return null; + } + + private bool InheritsFrom(INamedTypeSymbol symbol, string typeName) + { + var current = symbol.BaseType; + while (current != null) + { + if (current.ToDisplayString() == typeName) + { + return true; + } + current = current.BaseType; + } + return false; + } + + private void Execute(SourceProductionContext context, Compilation compilation, System.Collections.Immutable.ImmutableArray classes) + { + try + { + if (classes.IsDefaultOrEmpty && compilation.AssemblyName != "DiscUtils" && compilation.AssemblyName != "LTRData.DiscUtils") + { + return; + } + + var distinctClasses = classes.Where(c => c != null).Distinct(SymbolEqualityComparer.Default).Cast().ToList(); + + // Generate AssemblyRegistration for libraries + if (distinctClasses.Any()) + { + GenerateAssemblyRegistration(context, compilation, distinctClasses); + } + + // If this is the main DiscUtils assembly, generate the aggregator + if (compilation.AssemblyName == "DiscUtils" || compilation.AssemblyName == "LTRData.DiscUtils") + { + GenerateSetupHelper(context, compilation); + } + } + catch (Exception ex) + { + // Generate a file with the error so we can see it + context.AddSource("GeneratorError.g.cs", SourceText.From($"/* Error: {ex.ToString()} */", Encoding.UTF8)); + } + } + + private void GenerateAssemblyRegistration(SourceProductionContext context, Compilation compilation, List classes) + { + var sb = new StringBuilder(); + var assemblyName = compilation.AssemblyName?.Replace(".", "_"); + + sb.AppendLine("using System;"); + sb.AppendLine("using DiscUtils.Core;"); + sb.AppendLine(); + sb.AppendLine($"namespace {compilation.AssemblyName}"); + sb.AppendLine("{"); + sb.AppendLine($" public static class AssemblyRegistration_{assemblyName}"); + sb.AppendLine(" {"); + sb.AppendLine(" public static void Register()"); + sb.AppendLine(" {"); + sb.AppendLine($" Console.WriteLine(\"Registering assembly: {assemblyName}\");"); + + foreach (var classSymbol in classes) + { + if (InheritsFrom(classSymbol, "DiscUtils.Vfs.VfsFileSystemFactory")) + { + sb.AppendLine($" Console.WriteLine(\"Registering FileSystemFactory: {classSymbol.ToDisplayString()}\");"); + sb.AppendLine($" DiscUtils.FileSystemManager.RegisterFileSystems(new {classSymbol.ToDisplayString()}());"); + } + else if (InheritsFrom(classSymbol, "DiscUtils.Internal.VirtualDiskFactory")) + { + sb.AppendLine($" Console.WriteLine(\"Registering VirtualDiskFactory: {classSymbol.ToDisplayString()}\");"); + sb.AppendLine($" DiscUtils.VirtualDiskManager.RegisterVirtualDiskFactory(new {classSymbol.ToDisplayString()}());"); + } + else if (InheritsFrom(classSymbol, "DiscUtils.Internal.LogicalVolumeFactory")) + { + sb.AppendLine($" Console.WriteLine(\"Registering LogicalVolumeFactory: {classSymbol.ToDisplayString()}\");"); + sb.AppendLine($" DiscUtils.VolumeManager.RegisterLogicalVolumeFactory(new {classSymbol.ToDisplayString()}());"); + } + else if (InheritsFrom(classSymbol, "DiscUtils.Partitions.PartitionTableFactory")) + { + sb.AppendLine($" Console.WriteLine(\"Registering PartitionTableFactory: {classSymbol.ToDisplayString()}\");"); + sb.AppendLine($" DiscUtils.Partitions.PartitionTable.RegisterPartitionTableFactory(new {classSymbol.ToDisplayString()}());"); + } + else if (InheritsFrom(classSymbol, "DiscUtils.Internal.VirtualDiskTransport")) + { + var attributes = classSymbol.GetAttributes().Where(ad => ad.AttributeClass?.ToDisplayString() == "DiscUtils.Internal.VirtualDiskTransportAttribute"); + foreach(var attr in attributes) + { + if (attr.ConstructorArguments.Length > 0) + { + string scheme = attr.ConstructorArguments[0].Value?.ToString(); + if (!string.IsNullOrEmpty(scheme)) + { + sb.AppendLine($" Console.WriteLine(\"Registering VirtualDiskTransport: {classSymbol.ToDisplayString()} for scheme {scheme}\");"); + sb.AppendLine($" DiscUtils.VirtualDiskManager.RegisterVirtualDiskTransport(\"{scheme}\", () => new {classSymbol.ToDisplayString()}());"); + } + } + } + } + } + + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + context.AddSource($"AssemblyRegistration_{assemblyName}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + + private void GenerateSetupHelper(SourceProductionContext context, Compilation compilation) + { + var sb = new StringBuilder(); + sb.AppendLine("using System;"); + sb.AppendLine("using System.Reflection;"); + sb.AppendLine(); + sb.AppendLine("namespace DiscUtils.Setup"); + sb.AppendLine("{"); + sb.AppendLine(" public static class GeneratedSetupHelper"); + sb.AppendLine(" {"); + sb.AppendLine(" public static void RegisterFactories()"); + sb.AppendLine(" {"); + sb.AppendLine(" Console.WriteLine(\"GeneratedSetupHelper.RegisterFactories() called.\");"); + + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol) + { + var assemblyName = assemblySymbol.Name; + if ((assemblyName.StartsWith("DiscUtils.") || assemblyName.StartsWith("LTRData.DiscUtils.")) && !assemblyName.EndsWith("SourceGenerator")) + { + var safeAssemblyName = assemblyName.Replace(".", "_"); + var typeName = $"{assemblyName}.AssemblyRegistration_{safeAssemblyName}"; + + var typeSymbol = compilation.GetTypeByMetadataName(typeName); + + if (typeSymbol != null) + { + sb.AppendLine($" Console.WriteLine(\"Calling {typeName}.Register()\");"); + sb.AppendLine($" {typeName}.Register();"); + } + } + } + } + + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + + context.AddSource("GeneratedSetupHelper.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + } +} + diff --git a/generate-packages.ps1 b/generate-packages.ps1 new file mode 100644 index 000000000..30b4e2a18 --- /dev/null +++ b/generate-packages.ps1 @@ -0,0 +1,23 @@ +$outputDir = "C:\XGitPrivate\DevePXEBootStuff\DevePXEBootClean\DiscUtils\nupkgs" + +Write-Host "Cleaning output directory: $outputDir" +if (Test-Path -Path $outputDir) { + Get-ChildItem -Path $outputDir -Filter *.nupkg | Remove-Item -Force + Get-ChildItem -Path $outputDir -Filter *.snupkg | Remove-Item -Force +} else { + New-Item -ItemType Directory -Path $outputDir | Out-Null +} + +Write-Host "Cleaning solution..." +dotnet clean --configuration Release + +Write-Host "Restoring..." +dotnet restore + +Write-Host "Building..." +dotnet build --configuration Release + +Write-Host "Packing..." +dotnet pack --configuration Release --no-build --output $outputDir + +Write-Host "Done. Packages are in $outputDir" diff --git a/version.json b/version.json index 8954ca26f..b709c48bc 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.16", + "version": "0.17", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/develop$", From 1408dd409736be158510f86890ef0b70765d1997 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 20:32:06 +0100 Subject: [PATCH 02/19] Refactor plugin registration for AoT compatibility Modernize and refactor plugin/registration system for disk image and file system types: - Bump version to 1.0.85 - Add AoT-friendly registration path for DiskImageBuilder - Simplify and streamline FileSystemManager and VirtualDiskManager initialization - Remove console logging and exception handling from factory discovery - Make LogicalVolumeFactory internal - Make VolumeManager registration internal and direct - Add SetupCompleteAot for AoT environments - General cleanup for improved startup and reliability --- Directory.Build.props | 4 +- Library/DiscUtils.Core/DiskImageBuilder.cs | 42 ++++++- Library/DiscUtils.Core/FileSystemManager.cs | 46 ++----- .../Internal/LogicalVolumeFactory.cs | 2 +- Library/DiscUtils.Core/Setup/SetupHelper.cs | 2 - Library/DiscUtils.Core/VirtualDiskManager.cs | 119 +++--------------- Library/DiscUtils.Core/VolumeManager.cs | 15 +-- Library/DiscUtils/SetupHelper.cs | 1 + 8 files changed, 70 insertions(+), 161 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 90473f8b1..f90666b32 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,8 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.78 - 1.0.78 + 1.0.85 + 1.0.85 true diff --git a/Library/DiscUtils.Core/DiskImageBuilder.cs b/Library/DiscUtils.Core/DiskImageBuilder.cs index b9fb00d58..cf12403f9 100644 --- a/Library/DiscUtils.Core/DiskImageBuilder.cs +++ b/Library/DiscUtils.Core/DiskImageBuilder.cs @@ -34,6 +34,8 @@ namespace DiscUtils; /// public abstract class DiskImageBuilder { + private static Dictionary? _typeMap; + /// /// Gets or sets the geometry of this disk, as reported by the BIOS, will be implied from the content stream if not set. /// @@ -59,7 +61,18 @@ public abstract class DiskImageBuilder /// public virtual bool PreservesBiosGeometry => false; - private static Dictionary TypeMap => VirtualDiskManager.TypeMap; + private static Dictionary TypeMap + { + get + { + if (_typeMap == null) + { + InitializeMaps(); + } + + return _typeMap; + } + } /// /// Gets an instance that constructs the specified type (and variant) of virtual disk image. @@ -88,4 +101,31 @@ public static DiskImageBuilder GetBuilder(string type, string variant) /// to each logical file that comprises the disk image. For example, given a base name /// 'foo', the files 'foo.vmdk' and 'foo-flat.vmdk' could be returned. public abstract IEnumerable Build(string baseName); + + // Set this to true to enable AoT compatiblity + public static bool ShouldUseVirtualDiskManagerTypeMap { get; set; } + + [MemberNotNull(nameof(_typeMap))] + private static void InitializeMaps() + { + if (ShouldUseVirtualDiskManagerTypeMap) + { + _typeMap = VirtualDiskManager.TypeMap; + return; + } + + var typeMap = new Dictionary(); + + foreach (var type in typeof(VirtualDisk).Assembly.GetTypes()) + { + var attr = type.GetCustomAttribute(false); + if (attr != null) + { + var factory = (VirtualDiskFactory)Activator.CreateInstance(type)!; + typeMap.Add(attr.Type, factory); + } + } + + _typeMap = typeMap; + } } \ No newline at end of file diff --git a/Library/DiscUtils.Core/FileSystemManager.cs b/Library/DiscUtils.Core/FileSystemManager.cs index af78c4766..99a2e5d73 100644 --- a/Library/DiscUtils.Core/FileSystemManager.cs +++ b/Library/DiscUtils.Core/FileSystemManager.cs @@ -38,19 +38,14 @@ namespace DiscUtils; /// public static class FileSystemManager { - private static List? _factories; + private static readonly List _factories; - private static List Factories + /// + /// Initializes a new instance of the FileSystemManager class. + /// + static FileSystemManager() { - get - { - if (_factories == null) - { - _factories = new List(); - RegisterFileSystems(typeof(FileSystemManager).Assembly); - } - return _factories; - } + _factories = []; } /// @@ -59,11 +54,6 @@ private static List Factories /// The detector for the new file systems. public static void RegisterFileSystems(VfsFileSystemFactory factory) { - if (_factories == null) - { - _factories = new List(); - } - lock (_factories) { _factories.Add(factory); @@ -80,11 +70,6 @@ public static void RegisterFileSystems(VfsFileSystemFactory factory) /// public static void RegisterFileSystems(Assembly assembly) { - if (_factories == null) - { - _factories = new List(); - } - lock (_factories) { _factories.AddRange(DetectFactories(assembly)); @@ -114,7 +99,6 @@ public static ReadOnlyCollection DetectFileSystems(Stream stream private static IEnumerable DetectFactories(Assembly assembly) { - Console.WriteLine($"FileSystemManager: Scanning assembly {assembly.FullName} for VfsFileSystemFactories"); foreach (var type in assembly.GetTypes()) { var attrib = type.GetCustomAttribute(false); @@ -123,18 +107,7 @@ private static IEnumerable DetectFactories(Assembly assemb continue; } - Console.WriteLine($"FileSystemManager: Found VfsFileSystemFactory: {type.FullName}"); - VfsFileSystemFactory? factory = null; - try - { - factory = (VfsFileSystemFactory)Activator.CreateInstance(type, true)!; - } - catch (Exception ex) - { - Console.WriteLine($"FileSystemManager: Error instantiating {type.FullName}: {ex}"); - throw; - } - yield return factory; + yield return (VfsFileSystemFactory)Activator.CreateInstance(type)!; } } @@ -143,10 +116,9 @@ private static ReadOnlyCollection DoDetect(Stream stream, Volume var detectStream = new BufferedStream(stream); var detected = new List(); - var factories = Factories; - lock (factories) + lock (_factories) { - foreach (var factory in factories) + foreach (var factory in _factories) { detected.AddRange(factory.Detect(detectStream, volume)); } diff --git a/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs b/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs index 37f7ac924..d813bb153 100644 --- a/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs +++ b/Library/DiscUtils.Core/Internal/LogicalVolumeFactory.cs @@ -24,7 +24,7 @@ namespace DiscUtils.Internal; -public abstract class LogicalVolumeFactory +internal abstract class LogicalVolumeFactory { public abstract bool HandlesPhysicalVolume(PhysicalVolumeInfo volume); diff --git a/Library/DiscUtils.Core/Setup/SetupHelper.cs b/Library/DiscUtils.Core/Setup/SetupHelper.cs index 6543630dc..8088ba71e 100644 --- a/Library/DiscUtils.Core/Setup/SetupHelper.cs +++ b/Library/DiscUtils.Core/Setup/SetupHelper.cs @@ -25,12 +25,10 @@ static SetupHelper() /// public static void RegisterAssembly(Assembly assembly) { - Console.WriteLine($"SetupHelper: Registering assembly {assembly.FullName}"); lock (_alreadyLoaded) { if (!_alreadyLoaded.Add(assembly.FullName ?? assembly.GetName().FullName)) { - Console.WriteLine($"SetupHelper: Assembly {assembly.FullName} already registered."); return; } diff --git a/Library/DiscUtils.Core/VirtualDiskManager.cs b/Library/DiscUtils.Core/VirtualDiskManager.cs index 5191decd7..a25858960 100644 --- a/Library/DiscUtils.Core/VirtualDiskManager.cs +++ b/Library/DiscUtils.Core/VirtualDiskManager.cs @@ -1,7 +1,7 @@ -using System; +using DiscUtils.Internal; +using System; using System.Collections.Generic; using System.Reflection; -using DiscUtils.Internal; namespace DiscUtils; @@ -10,33 +10,15 @@ namespace DiscUtils; /// public static class VirtualDiskManager { - private static Dictionary? _extensionMap; - private static Dictionary? _typeMap; - private static Dictionary>? _diskTransports; - - internal static Dictionary> DiskTransports + static VirtualDiskManager() { - get - { - if (_diskTransports == null) - { - InitializeMaps(); - } - return _diskTransports!; - } + ExtensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + TypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + DiskTransports = new Dictionary>(StringComparer.OrdinalIgnoreCase); } - internal static Dictionary ExtensionMap - { - get - { - if (_extensionMap == null) - { - InitializeMaps(); - } - return _extensionMap!; - } - } + internal static Dictionary> DiskTransports { get; } + internal static Dictionary ExtensionMap { get; } /// /// Gets the set of disk formats supported as an array of file extensions. @@ -48,40 +30,7 @@ internal static Dictionary ExtensionMap /// public static ICollection SupportedDiskTypes => TypeMap.Keys; - internal static Dictionary TypeMap - { - get - { - if (_typeMap == null) - { - InitializeMaps(); - } - return _typeMap!; - } - } - - private static void InitializeMaps() - { - if (_typeMap == null) - { - _extensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - _typeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - _diskTransports = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - // Auto-scan Core assembly if not initialized - RegisterVirtualDiskTypes(typeof(VirtualDiskManager).Assembly); - } - } - - private static void EnsureInitialized() - { - if (_typeMap == null) - { - _extensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - _typeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - _diskTransports = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } - } + internal static Dictionary TypeMap { get; } /// /// Registers a VirtualDiskFactory instance. @@ -89,8 +38,6 @@ private static void EnsureInitialized() /// The factory to register. public static void RegisterVirtualDiskFactory(VirtualDiskFactory factory) { - EnsureInitialized(); - var type = factory.GetType(); var diskFactoryAttribute = type.GetCustomAttribute(false); if (diskFactoryAttribute != null) @@ -110,20 +57,6 @@ public static void RegisterVirtualDiskFactory(VirtualDiskFactory factory) } } - /// - /// Registers a VirtualDiskTransport type. - /// - /// The URI scheme. - /// The type implementing VirtualDiskTransport. - public static void RegisterVirtualDiskTransport(string scheme, Type type) - { - EnsureInitialized(); - if (!DiskTransports.ContainsKey(scheme)) - { - DiskTransports.Add(scheme, () => (VirtualDiskTransport)Activator.CreateInstance(type, true)!); - } - } - /// /// Registers a VirtualDiskTransport factory. /// @@ -131,7 +64,6 @@ public static void RegisterVirtualDiskTransport(string scheme, Type type) /// The factory method. internal static void RegisterVirtualDiskTransport(string scheme, Func factory) { - EnsureInitialized(); if (!DiskTransports.ContainsKey(scheme)) { DiskTransports.Add(scheme, factory); @@ -145,47 +77,24 @@ internal static void RegisterVirtualDiskTransport(string scheme, FuncAn assembly to scan public static void RegisterVirtualDiskTypes(Assembly assembly) { - Console.WriteLine($"VirtualDiskManager: Scanning assembly {assembly.FullName} for VirtualDiskTypes"); - EnsureInitialized(); foreach (var type in assembly.GetTypes()) { var diskFactoryAttribute = type.GetCustomAttribute(false); if (diskFactoryAttribute != null) { - Console.WriteLine($"VirtualDiskManager: Found VirtualDiskFactory: {type.FullName}"); - try - { - var factory = (VirtualDiskFactory)Activator.CreateInstance(type, true)!; - TypeMap.Add(diskFactoryAttribute.Type, factory); + var factory = (VirtualDiskFactory)Activator.CreateInstance(type)!; + TypeMap.Add(diskFactoryAttribute.Type, factory); - foreach (var extension in diskFactoryAttribute.FileExtensions) - { - ExtensionMap.Add(extension, factory); - } - } - catch (Exception ex) + foreach (var extension in diskFactoryAttribute.FileExtensions) { - Console.WriteLine($"VirtualDiskManager: Error instantiating {type.FullName}: {ex}"); - throw; + ExtensionMap.Add(extension, factory); } } var diskTransportAttribute = type.GetCustomAttribute(false); if (diskTransportAttribute != null) { - Console.WriteLine($"VirtualDiskManager: Found VirtualDiskTransport: {type.FullName} for scheme {diskTransportAttribute.Scheme}"); - DiskTransports.Add(diskTransportAttribute.Scheme, () => - { - try - { - return (VirtualDiskTransport)Activator.CreateInstance(type, true)!; - } - catch (Exception ex) - { - Console.WriteLine($"VirtualDiskManager: Error instantiating transport {type.FullName}: {ex}"); - throw; - } - }); + DiskTransports.Add(diskTransportAttribute.Scheme, () => (VirtualDiskTransport)Activator.CreateInstance(type)!); } } } diff --git a/Library/DiscUtils.Core/VolumeManager.cs b/Library/DiscUtils.Core/VolumeManager.cs index a296cb266..90bfa2a5b 100644 --- a/Library/DiscUtils.Core/VolumeManager.cs +++ b/Library/DiscUtils.Core/VolumeManager.cs @@ -107,7 +107,7 @@ private static ConcurrentBag LogicalVolumeFactories /// Register a new LogicalVolumeFactory instance. /// /// The factory to register. - public static void RegisterLogicalVolumeFactory(LogicalVolumeFactory factory) + internal static void RegisterLogicalVolumeFactory(LogicalVolumeFactory factory) { if (_logicalVolumeFactories == null) { @@ -129,18 +129,7 @@ private static IEnumerable GetLogicalVolumeFactories(Assem { foreach (var attr in type.GetCustomAttributes(false)) { - Console.WriteLine($"VolumeManager: Found LogicalVolumeFactory {type.FullName} in {assembly.FullName}"); - LogicalVolumeFactory? factory = null; - try - { - factory = (LogicalVolumeFactory)Activator.CreateInstance(type, true)!; - } - catch (Exception ex) - { - Console.WriteLine($"VolumeManager: Error instantiating {type.FullName}: {ex}"); - throw; - } - yield return factory; + yield return (LogicalVolumeFactory)Activator.CreateInstance(type)!; } } } diff --git a/Library/DiscUtils/SetupHelper.cs b/Library/DiscUtils/SetupHelper.cs index e040eab03..4d53c4c20 100644 --- a/Library/DiscUtils/SetupHelper.cs +++ b/Library/DiscUtils/SetupHelper.cs @@ -56,6 +56,7 @@ public static void SetupComplete() } public static void SetupCompleteAot() { + DiskImageBuilder.ShouldUseVirtualDiskManagerTypeMap = true; DiscUtils.Setup.GeneratedSetupHelper.RegisterFactories(); } } \ No newline at end of file From 88fcbb3be28d518874dc073f858c821c9774e6f8 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 20:38:46 +0100 Subject: [PATCH 03/19] Refactor SetupComplete method and remove console logging from FactoryGenerator --- Library/DiscUtils/SetupHelper.cs | 3 ++- SourceGenerator/FactoryGenerator.cs | 6 ------ version.json | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Library/DiscUtils/SetupHelper.cs b/Library/DiscUtils/SetupHelper.cs index 4d53c4c20..895416adf 100644 --- a/Library/DiscUtils/SetupHelper.cs +++ b/Library/DiscUtils/SetupHelper.cs @@ -23,7 +23,7 @@ namespace DiscUtils.Complete; public static class SetupHelper { - public static void SetupComplete() + public static void SetupComplete() { Setup.SetupHelper.RegisterAssembly(typeof(Store).Assembly); Setup.SetupHelper.RegisterAssembly(typeof(Disk).Assembly); @@ -54,6 +54,7 @@ public static void SetupComplete() Setup.SetupHelper.RegisterAssembly(typeof(Xva.Disk).Assembly); Setup.SetupHelper.RegisterAssembly(typeof(Lvm.LogicalVolumeManager).Assembly); } + public static void SetupCompleteAot() { DiskImageBuilder.ShouldUseVirtualDiskManagerTypeMap = true; diff --git a/SourceGenerator/FactoryGenerator.cs b/SourceGenerator/FactoryGenerator.cs index 0d76401f2..c9b57a250 100644 --- a/SourceGenerator/FactoryGenerator.cs +++ b/SourceGenerator/FactoryGenerator.cs @@ -110,28 +110,23 @@ private void GenerateAssemblyRegistration(SourceProductionContext context, Compi sb.AppendLine(" {"); sb.AppendLine(" public static void Register()"); sb.AppendLine(" {"); - sb.AppendLine($" Console.WriteLine(\"Registering assembly: {assemblyName}\");"); foreach (var classSymbol in classes) { if (InheritsFrom(classSymbol, "DiscUtils.Vfs.VfsFileSystemFactory")) { - sb.AppendLine($" Console.WriteLine(\"Registering FileSystemFactory: {classSymbol.ToDisplayString()}\");"); sb.AppendLine($" DiscUtils.FileSystemManager.RegisterFileSystems(new {classSymbol.ToDisplayString()}());"); } else if (InheritsFrom(classSymbol, "DiscUtils.Internal.VirtualDiskFactory")) { - sb.AppendLine($" Console.WriteLine(\"Registering VirtualDiskFactory: {classSymbol.ToDisplayString()}\");"); sb.AppendLine($" DiscUtils.VirtualDiskManager.RegisterVirtualDiskFactory(new {classSymbol.ToDisplayString()}());"); } else if (InheritsFrom(classSymbol, "DiscUtils.Internal.LogicalVolumeFactory")) { - sb.AppendLine($" Console.WriteLine(\"Registering LogicalVolumeFactory: {classSymbol.ToDisplayString()}\");"); sb.AppendLine($" DiscUtils.VolumeManager.RegisterLogicalVolumeFactory(new {classSymbol.ToDisplayString()}());"); } else if (InheritsFrom(classSymbol, "DiscUtils.Partitions.PartitionTableFactory")) { - sb.AppendLine($" Console.WriteLine(\"Registering PartitionTableFactory: {classSymbol.ToDisplayString()}\");"); sb.AppendLine($" DiscUtils.Partitions.PartitionTable.RegisterPartitionTableFactory(new {classSymbol.ToDisplayString()}());"); } else if (InheritsFrom(classSymbol, "DiscUtils.Internal.VirtualDiskTransport")) @@ -144,7 +139,6 @@ private void GenerateAssemblyRegistration(SourceProductionContext context, Compi string scheme = attr.ConstructorArguments[0].Value?.ToString(); if (!string.IsNullOrEmpty(scheme)) { - sb.AppendLine($" Console.WriteLine(\"Registering VirtualDiskTransport: {classSymbol.ToDisplayString()} for scheme {scheme}\");"); sb.AppendLine($" DiscUtils.VirtualDiskManager.RegisterVirtualDiskTransport(\"{scheme}\", () => new {classSymbol.ToDisplayString()}());"); } } diff --git a/version.json b/version.json index b709c48bc..8954ca26f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.17", + "version": "0.16", "publicReleaseRefSpec": [ "^refs/heads/master$", "^refs/heads/develop$", From b31443d584f8d83833e8c6af9fd8e733752ef018 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 21:07:04 +0100 Subject: [PATCH 04/19] Refactor source generator structure and update project references --- DiscUtils.slnx | 4 +--- Library/Directory.Build.props | 5 ++++- .../DiscUtils.SourceGenerator.csproj | 9 +++++++-- .../DiscUtils.SourceGenerator}/FactoryGenerator.cs | 0 4 files changed, 12 insertions(+), 6 deletions(-) rename {SourceGenerator => Library/DiscUtils.SourceGenerator}/DiscUtils.SourceGenerator.csproj (62%) rename {SourceGenerator => Library/DiscUtils.SourceGenerator}/FactoryGenerator.cs (100%) diff --git a/DiscUtils.slnx b/DiscUtils.slnx index 461c48c61..af19f17d9 100644 --- a/DiscUtils.slnx +++ b/DiscUtils.slnx @@ -48,6 +48,7 @@ + @@ -86,7 +87,4 @@ - - - diff --git a/Library/Directory.Build.props b/Library/Directory.Build.props index e593a9b1f..ec66632c9 100644 --- a/Library/Directory.Build.props +++ b/Library/Directory.Build.props @@ -37,7 +37,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + diff --git a/SourceGenerator/DiscUtils.SourceGenerator.csproj b/Library/DiscUtils.SourceGenerator/DiscUtils.SourceGenerator.csproj similarity index 62% rename from SourceGenerator/DiscUtils.SourceGenerator.csproj rename to Library/DiscUtils.SourceGenerator/DiscUtils.SourceGenerator.csproj index 11e06ed46..35a9841d6 100644 --- a/SourceGenerator/DiscUtils.SourceGenerator.csproj +++ b/Library/DiscUtils.SourceGenerator/DiscUtils.SourceGenerator.csproj @@ -1,9 +1,14 @@ + - net10.0 + netstandard2.0 + + true + Source Generator for DiscUtils true - true + DiscUtils;SourceGenerator;Analyzer + enable diff --git a/SourceGenerator/FactoryGenerator.cs b/Library/DiscUtils.SourceGenerator/FactoryGenerator.cs similarity index 100% rename from SourceGenerator/FactoryGenerator.cs rename to Library/DiscUtils.SourceGenerator/FactoryGenerator.cs From 8fc75f5ee339d8678e7966446100d04cd8a3af2f Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 21:19:10 +0100 Subject: [PATCH 05/19] Wip --- .github/workflows/publish-nuget.yml | 21 +++++++-------------- Directory.Build.props | 3 +-- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 206ae6f71..e0f29e3dc 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -1,9 +1,7 @@ name: NuGet push (tag) -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' +on: + workflow_dispatch: env: DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -11,19 +9,14 @@ env: jobs: build: - runs-on: windows-2019 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 + - name: Setup .NET Core + uses: actions/setup-dotnet@v5 with: - fetch-depth: 50 - lfs: 'true' - # We do not need to fetch tags, as we're already at a tagged build - it should be available automatically - - - name: Setup .NET Core 7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '7.0.x' + dotnet-version: 10.0.x - name: Pack run: dotnet pack -c Release -o ${{ github.workspace }}/build diff --git a/Directory.Build.props b/Directory.Build.props index f90666b32..fba138ec6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,7 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.85 - 1.0.85 + 1.0.100 true From 34da27d564a50db6373565e6d3154ee2a2d6feae Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 21:21:38 +0100 Subject: [PATCH 06/19] Update package project URLs and identifiers to reflect correct ownership --- Directory.Build.props | 2 +- Library/Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index fba138ec6..35403b043 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ portable - https://github.com/LTRData/DiscUtils + https://github.com/Devedse/DiscUtils MIT git diff --git a/Library/Directory.Build.props b/Library/Directory.Build.props index ec66632c9..3c0fe7c12 100644 --- a/Library/Directory.Build.props +++ b/Library/Directory.Build.props @@ -5,7 +5,7 @@ netstandard2.0;netstandard2.1;net46;net48;net8.0;net9.0;net10.0 true - LTRData.$(MSBuildProjectName) + Devedse.$(MSBuildProjectName) $(LocalNuGetPath) $(FileVersion) README.md From b9044a399903ce710eaa1a2cd8ad09c9deab41dd Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 18 Dec 2025 21:28:18 +0100 Subject: [PATCH 07/19] Refactor publish-nuget workflow to build before packing --- .github/workflows/publish-nuget.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index e0f29e3dc..8b4ec41c7 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -18,8 +18,11 @@ jobs: with: dotnet-version: 10.0.x + - name: Build + run: dotnet build -c Release + - name: Pack - run: dotnet pack -c Release -o ${{ github.workspace }}/build + run: dotnet pack -c Release -o ${{ github.workspace }}/build --no-build - name: NuGet push run: dotnet nuget push *.nupkg --skip-duplicate -k ${{secrets.NUGET_KEY}} -s https://api.nuget.org/v3/index.json From 808bc0813cc5eb53b323983391e71128cca0e5e2 Mon Sep 17 00:00:00 2001 From: Devedse Date: Tue, 30 Dec 2025 19:33:36 +0100 Subject: [PATCH 08/19] Update to version 101 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 35403b043..2b5a35485 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.100 + 1.0.101 true From 5e3e244b7aa3b7fd06c88fef071ae264bf1f17c2 Mon Sep 17 00:00:00 2001 From: Devedse Date: Wed, 31 Dec 2025 01:43:50 +0100 Subject: [PATCH 09/19] Update file version to 1.0.102 and add tests for handling large data cells --- Directory.Build.props | 2 +- Library/DiscUtils.Registry/Bin.cs | 28 +++++++++++++++++-- .../LibraryTests/Registry/RegistryKeyTest.cs | 6 ++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2b5a35485..6429d7843 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,7 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.101 + 1.0.102 true diff --git a/Library/DiscUtils.Registry/Bin.cs b/Library/DiscUtils.Registry/Bin.cs index 1feb13cb8..e81d9b67a 100644 --- a/Library/DiscUtils.Registry/Bin.cs +++ b/Library/DiscUtils.Registry/Bin.cs @@ -152,14 +152,36 @@ public Span ReadRawCellData(int cellIndex, Span maxBytes) { var index = cellIndex - _header.FileOffset; var len = Math.Abs(EndianUtilities.ToInt32LittleEndian(_buffer, index)); - + // Check if this is a "big data" cell (signature "db") // Big data cells are used for values larger than ~16KB if (len >= 6 && _buffer[index + 4] == 0x64 && _buffer[index + 5] == 0x62) // "db" { - throw new NotSupportedException("Big data cells are not supported in this implementation"); + // Big data format: + // 0x00: signature "db" (2 bytes) + // 0x02: number of segments (2 bytes) + // 0x04: offset to list of cell indices (4 bytes) + var numSegments = EndianUtilities.ToUInt16LittleEndian(_buffer.AsSpan(index + 6)); + var listOffset = EndianUtilities.ToInt32LittleEndian(_buffer.AsSpan(index + 8)); + + // Read the list of cell indices + var listIndex = listOffset - _header.FileOffset; + var bytesWritten = 0; + + for (var i = 0; i < numSegments && bytesWritten < maxBytes.Length; i++) + { + var segmentCellIndex = EndianUtilities.ToInt32LittleEndian(_buffer.AsSpan(listIndex + i * 4)); + var segmentIndex = segmentCellIndex - _header.FileOffset; + var segmentLen = Math.Abs(EndianUtilities.ToInt32LittleEndian(_buffer, segmentIndex)) - 4; + + var bytesToCopy = Math.Min(segmentLen, maxBytes.Length - bytesWritten); + _buffer.AsSpan(segmentIndex + 4, bytesToCopy).CopyTo(maxBytes.Slice(bytesWritten)); + bytesWritten += bytesToCopy; + } + + return maxBytes.Slice(0, bytesWritten); } - + // Regular cell data var result = maxBytes.Slice(0, Math.Min(len - 4, maxBytes.Length)); _buffer.AsSpan(index + 4, result.Length).CopyTo(result); diff --git a/Tests/LibraryTests/Registry/RegistryKeyTest.cs b/Tests/LibraryTests/Registry/RegistryKeyTest.cs index 524efd1c6..86e60bd5b 100644 --- a/Tests/LibraryTests/Registry/RegistryKeyTest.cs +++ b/Tests/LibraryTests/Registry/RegistryKeyTest.cs @@ -83,14 +83,14 @@ public void SetVeryLargeValue_BigDataCell() // Test big data cells (used for values >~16KB) // This mimics real-world scenarios like Windows registry ProductPolicy values var buffer = new byte[80 * 1024]; // 80KB - larger than big data threshold - + // Set some distinctive bytes at various positions buffer[0] = 0x12; buffer[100] = 0x34; buffer[16384] = 0x56; // Past first 16KB boundary buffer[32768] = 0x78; // Past second 16KB boundary buffer[buffer.Length - 1] = 0x9A; - + hive.Root.SetValue("verybigvalue", buffer); var readVal = (byte[])hive.Root.GetValue("verybigvalue"); @@ -100,7 +100,7 @@ public void SetVeryLargeValue_BigDataCell() Assert.Equal(0x56, readVal[16384]); Assert.Equal(0x78, readVal[32768]); Assert.Equal(0x9A, readVal[buffer.Length - 1]); - + // Verify entire buffer matches Assert.Equal(buffer, readVal); } From 0937a4ede8eb31a6b7888909e4e6838eb9c7c8fc Mon Sep 17 00:00:00 2001 From: Devedse Date: Wed, 15 Apr 2026 11:59:58 +0200 Subject: [PATCH 10/19] to 103 --- Directory.Build.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6429d7843..c392b16ef 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,7 +13,8 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.102 + 1.0.103 + 1.0.103 true From a63dbb77eba6424d509694fcd43a7dfdc17ced31 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 16 Apr 2026 14:57:59 +0200 Subject: [PATCH 11/19] iSCSI: Add NOP-Out keepalive and handle target-initiated NOP-In + bump to 1.0.104 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During idle periods an iSCSI target can close the session due to the DefaultTime2Retain/DefaultTime2Wait timeout expiring while no traffic is flowing. This causes the next I/O to fail with a broken connection rather than a clean retry. Changes in Connection.cs: - SemaphoreSlim (_streamSemaphore) serialises all PDU traffic so the keepalive timer and the main Send/SendAsync/Logout paths never interleave on the NetworkStream. - Timer (_keepAliveTimer) fires every 10 s after login; skips ticks when a real command is in-flight. - SendNopOut: ITT=0xFFFFFFFF / TTT=0xFFFFFFFF so no NOP-In response is expected (RFC 3720 §10.18). - HandleNopIn: responds to target-initiated NOP-In (TTT!=0xFFFFFFFF) without advancing ExpectedStatusSequenceNumber (RFC 3720 §10.19). - ReadPdu/ReadPduAsync: loop over consecutive NOP-In PDUs (max 16) before returning the next real response PDU. Version bump: 1.0.103 -> 1.0.104 --- Directory.Build.props | 4 +- Library/DiscUtils.Iscsi/Connection.cs | 229 +++++++++++++++++++++++--- 2 files changed, 208 insertions(+), 25 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c392b16ef..e01c7dad1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,8 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.103 - 1.0.103 + 1.0.104 + 1.0.104 true diff --git a/Library/DiscUtils.Iscsi/Connection.cs b/Library/DiscUtils.Iscsi/Connection.cs index 0b4abff83..4042fc0ea 100644 --- a/Library/DiscUtils.Iscsi/Connection.cs +++ b/Library/DiscUtils.Iscsi/Connection.cs @@ -46,6 +46,17 @@ internal sealed class Connection : IDisposable private readonly NetworkStream _stream; + /// + /// Semaphore to synchronize access to the network stream. NOP-Out responses from the + /// keepalive timer and normal Send/ReadPdu traffic must not interleave. + /// + private readonly SemaphoreSlim _streamSemaphore = new(1, 1); + + /// + /// Timer that periodically sends NOP-Out pings to keep the iSCSI session alive. + /// + private Timer _keepAliveTimer; + public Connection(Session session, TargetAddress address, Authenticator[] authenticators) { Session = session; @@ -80,6 +91,11 @@ public Connection(Session session, TargetAddress address, Authenticator[] authen _negotiatedParameters = []; NegotiateSecurity(); NegotiateFeatures(); + + // Start keepalive timer after login completes. + // Send a NOP-Out every 10 seconds to prevent the target from + // timing out the session during idle periods. + _keepAliveTimer = new Timer(KeepAliveCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); } internal LoginStages CurrentLoginStage { get; private set; } = LoginStages.SecurityNegotiation; @@ -106,17 +122,30 @@ public void Close(LogoutReason reason) { try { - var req = new LogoutRequest(this); - var packet = req.GetBytes(reason); - _stream.Write(packet, 0, packet.Length); - _stream.Flush(); + _streamSemaphore.Wait(); + try + { + // Stop the keepalive timer while holding the semaphore + // to ensure no callback is in-flight. + _keepAliveTimer?.Dispose(); + _keepAliveTimer = null; - var pdu = ReadPdu(); - var resp = ParseResponse(pdu); + var req = new LogoutRequest(this); + var packet = req.GetBytes(reason); + _stream.Write(packet, 0, packet.Length); + _stream.Flush(); + + var pdu = ReadPdu(); + var resp = ParseResponse(pdu); - if (resp.Response != LogoutResponseCode.ClosedSuccessfully) + if (resp.Response != LogoutResponseCode.ClosedSuccessfully) + { + throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}"); + } + } + finally { - throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}"); + _streamSemaphore.Release(); } } catch (EndOfStreamException) @@ -133,6 +162,61 @@ public void Close(LogoutReason reason) } } + /// + /// Timer callback: sends a NOP-Out to keep the iSCSI session alive. + /// Uses ITT=0xFFFFFFFF and TTT=0xFFFFFFFF so the target does not send a NOP-In response + /// (RFC 3720 §10.18), avoiding read-side contention with the main Send path. + /// + private void KeepAliveCallback(object state) + { + if (!_streamSemaphore.Wait(0)) + { + // A Send or Close is in progress; skip this ping and retry on the next timer tick. + return; + } + + try + { + SendNopOut(); + } + catch (IOException) + { + // Connection is dead — the next Send will surface the error. + } + catch (ObjectDisposedException) + { + // Stream was disposed — connection is shutting down. + } + finally + { + _streamSemaphore.Release(); + } + } + + /// + /// Sends an initiator-initiated NOP-Out with ITT=0xFFFFFFFF and TTT=0xFFFFFFFF. + /// This is a "ping" that keeps the iSCSI session alive without requiring a response. + /// + private void SendNopOut() + { + var nopOut = new byte[48]; + // Byte 0: Immediate (0x40) | OpCode NopOut (0x00) + nopOut[0] = 0x40; + // Byte 1: Final bit + nopOut[1] = 0x80; + // Bytes 16-19: ITT = 0xFFFFFFFF (no response expected) + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); + // Bytes 20-23: TTT = 0xFFFFFFFF + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 20); + // Bytes 24-27: CmdSN (not incremented for immediate PDUs) + EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24); + // Bytes 28-31: ExpStatSN + EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28); + + _stream.Write(nopOut, 0, nopOut.Length); + _stream.Flush(); + } + /// /// Sends an SCSI command (aka task) to a LUN via the connected target. /// @@ -142,6 +226,9 @@ public void Close(LogoutReason reason) /// The number of bytes received. public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuffer) { + _streamSemaphore.Wait(); + try + { for (var i = 0; ; i++) { try @@ -281,6 +368,11 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff { } } + } + finally + { + _streamSemaphore.Release(); + } } /// @@ -293,6 +385,9 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff /// The number of bytes received. public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outBuffer, Memory inBuffer, CancellationToken cancellationToken) { + await _streamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { for (var i = 0; ; i++) { try @@ -410,6 +505,11 @@ public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outB { } } + } + finally + { + _streamSemaphore.Release(); + } } public T Send(ScsiCommand cmd, ReadOnlySpan buffer, int expected) @@ -816,32 +916,115 @@ private void NegotiateFeatures() private ProtocolDataUnit ReadPdu() { - var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None); - - if (pdu.OpCode == OpCode.Reject) + const int MaxNopInRetries = 16; + for (var nopCount = 0; ; nopCount++) { - var pkt = new RejectPacket(); - pkt.Parse(pdu); + var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None); - throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); - } + if (pdu.OpCode == OpCode.Reject) + { + var pkt = new RejectPacket(); + pkt.Parse(pdu); + + throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + } - return pdu; + // RFC 3720 §10.19: Handle target-initiated NOP-In (ping). + // Respond with NOP-Out echoing the Target Transfer Tag, then + // continue reading the next PDU. + if (pdu.OpCode == OpCode.NopIn) + { + if (nopCount >= MaxNopInRetries) + { + throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response"); + } + + HandleNopIn(pdu); + continue; + } + + return pdu; + } } private async ValueTask ReadPduAsync(CancellationToken cancellationToken) { - var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false); - - if (pdu.OpCode == OpCode.Reject) + const int MaxNopInRetries = 16; + for (var nopCount = 0; ; nopCount++) { - var pkt = new RejectPacket(); - pkt.Parse(pdu); + var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false); + + if (pdu.OpCode == OpCode.Reject) + { + var pkt = new RejectPacket(); + pkt.Parse(pdu); + + throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + } + + if (pdu.OpCode == OpCode.NopIn) + { + if (nopCount >= MaxNopInRetries) + { + throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response"); + } + + HandleNopIn(pdu); + continue; + } - throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + return pdu; } + } + + /// + /// Responds to a target-initiated NOP-In with a NOP-Out (RFC 3720 §10.18/10.19). + /// Target-initiated NOP-In has TTT != 0xFFFFFFFF and requires a NOP-Out reply. + /// The StatSN in a target-initiated NOP-In is informational and does NOT consume + /// a sequence number, so we must NOT call SeenStatusSequenceNumber here. + /// + private void HandleNopIn(ProtocolDataUnit pdu) + { + var headerData = pdu.HeaderData; - return pdu; + // Target Transfer Tag at offset 20 + var targetTransferTag = EndianUtilities.ToUInt32BigEndian(headerData.AsSpan(20)); + + // If TTT is 0xFFFFFFFF, this is a response to our own NOP-Out (unsolicited); + // no reply needed. + if (targetTransferTag == 0xFFFFFFFF) + { + return; + } + + // LUN at offset 8 + var lun = EndianUtilities.ToUInt64BigEndian(headerData.AsSpan(8)); + + // NOTE: We intentionally do NOT call SeenStatusSequenceNumber() here. + // RFC 3720 §10.19: A target-initiated NOP-In carries a StatSN, but this + // StatSN is NOT consumed — it's the same value the target will use for the + // next real command response. Advancing ExpectedStatusSequenceNumber here + // would cause the next ParseResponse to fail with a sequence mismatch. + + // Build NOP-Out response (RFC 3720 §10.18) + var nopOut = new byte[48]; + // Byte 0: Immediate bit (0x40) | OpCode NopOut (0x00) + nopOut[0] = 0x40; + // Byte 1: Final bit (0x80) + nopOut[1] = 0x80; + // Bytes 8-15: LUN (copy from NOP-In) + EndianUtilities.WriteBytesBigEndian(lun, nopOut, 8); + // Bytes 16-19: Initiator Task Tag = 0xFFFFFFFF (response to target ping) + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); + // Bytes 20-23: Target Transfer Tag (echo from NOP-In) + EndianUtilities.WriteBytesBigEndian(targetTransferTag, nopOut, 20); + // Bytes 24-27: CmdSN (not consumed for immediate PDUs with ITT=0xFFFFFFFF) + EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24); + // Bytes 28-31: ExpStatSN + EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28); + + _stream.Write(nopOut, 0, nopOut.Length); + _stream.Flush(); } private void GetParametersToNegotiate(TextBuffer parameters, KeyUsagePhase phase, SessionType sessionType) From e587fa1a2633af8502d6542fc186d1c626961f07 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 13:24:05 +0200 Subject: [PATCH 12/19] Fix InvalidCastException in SubKeyIndirectListCell write paths for mixed ri/li lists SubKeyIndirectListCell backs both "ri" index-root lists (entries are subordinate lists) and "li" leaf-index lists (entries are key nodes). The read paths (Count, EnumerateKeyNames, EnumerateKeys, DoFindKey, KeyFinder) already tolerate a single list whose entries are a mix of sublists and key nodes, by dispatching on the actual cell type. But LinkSubKey/UnlinkSubKey still branched on ListType and hard-cast every entry (GetCell for "ri", GetCell for "li"). On a real Windows SYSTEM hive whose "ri" list contains a direct key-node entry, deleting a subkey (UnlinkSubKey) threw: System.InvalidCastException: Unable to cast object of type 'DiscUtils.Registry.KeyNodeCell' to type 'DiscUtils.Registry.ListCell'. Reading/enumerating the same key worked (those paths were already hardened); only add/delete crashed. Fix: LinkSubKey/UnlinkSubKey now dispatch on the actual cell type per entry (GetCell + "is ListCell"), exactly as the read paths do. Behaviour for homogeneous "ri"/"li" lists is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../SubKeyIndirectListCell.cs | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/Library/DiscUtils.Registry/SubKeyIndirectListCell.cs b/Library/DiscUtils.Registry/SubKeyIndirectListCell.cs index 69ceae2ea..28dece1ce 100644 --- a/Library/DiscUtils.Registry/SubKeyIndirectListCell.cs +++ b/Library/DiscUtils.Registry/SubKeyIndirectListCell.cs @@ -160,33 +160,22 @@ internal override IEnumerable EnumerateKeys() internal override int LinkSubKey(string name, int cellIndex) { - // Look for the first sublist that has a subkey name greater than name - if (ListType == "ri") + // As in UnlinkSubKey, a list's entries can be a mix of sublists ("ri") and key nodes ("li"), so dispatch + // on the actual cell type rather than on ListType (avoids a KeyNodeCell -> ListCell InvalidCastException). + // Look for the first sublist/key node whose name is greater than the new name. + for (var i = 0; i < CellIndexes.Count; ++i) { - if (CellIndexes.Count == 0) - { - throw new NotImplementedException("Empty indirect list"); - } - - for (var i = 0; i < CellIndexes.Count - 1; ++i) + var cell = _hive.GetCell(CellIndexes[i]); + if (cell is ListCell listCell) { - var cell = _hive.GetCell(CellIndexes[i]); - if (cell.FindKey(name, out var tempIndex) <= 0) + // Descend into the last sublist, or the first whose range can already hold the new name. + if (i == CellIndexes.Count - 1 || listCell.FindKey(name, out _) <= 0) { - CellIndexes[i] = cell.LinkSubKey(name, cellIndex); + CellIndexes[i] = listCell.LinkSubKey(name, cellIndex); return _hive.UpdateCell(this, false); } } - - var lastCell = _hive.GetCell(CellIndexes[CellIndexes.Count - 1]); - CellIndexes[CellIndexes.Count - 1] = lastCell.LinkSubKey(name, cellIndex); - return _hive.UpdateCell(this, false); - } - - for (var i = 0; i < CellIndexes.Count; ++i) - { - var cell = _hive.GetCell(CellIndexes[i]); - if (string.Compare(name, cell.Name, StringComparison.OrdinalIgnoreCase) < 0) + else if (string.Compare(name, ((KeyNodeCell)cell).Name, StringComparison.OrdinalIgnoreCase) < 0) { CellIndexes.Insert(i, cellIndex); return _hive.UpdateCell(this, true); @@ -199,20 +188,19 @@ internal override int LinkSubKey(string name, int cellIndex) internal override int UnlinkSubKey(string name) { - if (ListType == "ri") + // A "ri" list references sublists and a "li" list references key nodes, but in practice the entries of a + // single list can be a mix of both (the read paths - Count/EnumerateKeyNames/EnumerateKeys/DoFindKey - + // already tolerate this). Dispatch on the actual cell type instead of on ListType, otherwise deleting a + // key whose containing list has a mismatched entry throws InvalidCastException (KeyNodeCell -> ListCell). + for (var i = 0; i < CellIndexes.Count; ++i) { - if (CellIndexes.Count == 0) - { - throw new NotImplementedException("Empty indirect list"); - } - - for (var i = 0; i < CellIndexes.Count; ++i) + var cell = _hive.GetCell(CellIndexes[i]); + if (cell is ListCell listCell) { - var cell = _hive.GetCell(CellIndexes[i]); - if (cell.FindKey(name, out var tempIndex) <= 0) + if (listCell.FindKey(name, out _) <= 0) { - CellIndexes[i] = cell.UnlinkSubKey(name); - if (cell.Count == 0) + CellIndexes[i] = listCell.UnlinkSubKey(name); + if (listCell.Count == 0) { _hive.FreeCell(CellIndexes[i]); CellIndexes.RemoveAt(i); @@ -221,17 +209,10 @@ internal override int UnlinkSubKey(string name) return _hive.UpdateCell(this, false); } } - } - else - { - for (var i = 0; i < CellIndexes.Count; ++i) + else if (string.Equals(name, ((KeyNodeCell)cell).Name, StringComparison.OrdinalIgnoreCase)) { - var cell = _hive.GetCell(CellIndexes[i]); - if (string.Equals(name, cell.Name, StringComparison.OrdinalIgnoreCase)) - { - CellIndexes.RemoveAt(i); - return _hive.UpdateCell(this, true); - } + CellIndexes.RemoveAt(i); + return _hive.UpdateCell(this, true); } } From aa72b3f3ea37e2d149c9c51e22a673e79a8f0925 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 14:25:44 +0200 Subject: [PATCH 13/19] Bump version to 1.0.105 Includes the cherry-picked SubKeyIndirectListCell fix (InvalidCastException on deleting/adding keys in mixed ri/li subkey lists). Triggers a fork build so DevePXEBoot can consume the registry delete fix before it lands upstream (PR #69). Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e01c7dad1..dedf5c6bc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,8 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.104 - 1.0.104 + 1.0.105 + 1.0.105 true From 2f855a12ec4d4cdea41f6487d9bcf72b7d4c19e1 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 14:52:31 +0200 Subject: [PATCH 14/19] Fix Bin.AllocateCell 8-byte alignment check (regressed to % 0x7 by upstream) Registry hive cells are 8-byte aligned. The check must reject sizes whose low 3 bits are set: (size & 0x7) != 0. Upstream LTRData.DiscUtils-initial had regressed this to (size % 0x7) != 0 (mod 7), which rejects ~6 of every 7 valid sizes - including the ~80-byte root key cell - so RegistryHive.Create() itself throws "Invalid cell size" and every registry/BootConfig test fails (and the build hangs). The fork's pre-merge code used (size % 8); this merge pulled in the regression. Co-Authored-By: Claude Opus 4.8 (1M context) --- Library/DiscUtils.Registry/Bin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/DiscUtils.Registry/Bin.cs b/Library/DiscUtils.Registry/Bin.cs index 2db98e7b1..87f631a5e 100644 --- a/Library/DiscUtils.Registry/Bin.cs +++ b/Library/DiscUtils.Registry/Bin.cs @@ -209,7 +209,7 @@ internal bool WriteRawCellData(int cellIndex, ReadOnlySpan data) internal int AllocateCell(int size) { - if (size < 8 || (size % 0x7) != 0) + if (size < 8 || (size & 0x7) != 0) { throw new ArgumentException("Invalid cell size"); } From dd629afce4e4e79c39de40fc8470ab2709b05361 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 15:25:56 +0200 Subject: [PATCH 15/19] Fix NtfsFileSystem.GetFileLength ignoring FileLengthFromDirectoryEntries (regressed in a5516351) GetFileLength must honor the NtfsOptions.FileLengthFromDirectoryEntries option: when set (the default), a default-data-stream length request returns the directory entry's cached RealSize. Commit a5516351132e259b5149030e000f4789f78894d2 ("Bugfixes for FileExist and GetFileLength") removed that shortcut, so GetFileLength always looked up the data attribute and the option became a no-op - breaking NtfsFileSystemTest.GetFileLength (and the async variant), which expects a hardlink's stale directory-entry size (14325) by default and the live data-attribute size (50) only when the option is disabled. Restores the shortcut. The FileExists fix in that commit is correct and left intact. Co-Authored-By: Claude Opus 4.8 (1M context) --- Library/DiscUtils.Ntfs/NtfsFileSystem.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs index 34e7d6ecb..abb0149fd 100644 --- a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs +++ b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs @@ -930,6 +930,13 @@ public override long GetFileLength(string path) var dirEntry = GetDirectoryEntry(dirEntryPath) ?? throw new FileNotFoundException("File not found", path); + // Ordinary file length request, use info from the directory entry for efficiency - if allowed. + if (NtfsOptions.FileLengthFromDirectoryEntries && attributeName == null && + attributeType == AttributeType.Data) + { + return (long)dirEntry.Details.RealSize; + } + var file = GetFile(dirEntry.Reference); var attr = file.GetAttribute(attributeType, attributeName) ?? throw new FileNotFoundException($"No such attribute '{attributeName}({attributeType})'"); From 950ed001f1cc13a202e3227d28d2ddd21d10b9d7 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 16:19:05 +0200 Subject: [PATCH 16/19] Fix infinite loop in DynamicStream sector scan (regressed in f7bf7550) FindNextPresentSector/FindNextAbsentSector advance with pos += (8 - sectorInBlock & 0x7) * Sizes.Sector. C# precedence makes this (8 - sectorInBlock) & 0x7, not 8 - (sectorInBlock & 0x7): when sectorInBlock is byte-aligned and the block-bitmap byte is empty, the original (8 - sectorInBlock % 8) advanced by 8 sectors, but the rewrite yields 8 & 7 = 0, so pos never advances and the extent enumeration (LayerExtents/Extents and the dynamic VHD/VMDK builders) loops forever. Commit f7bf7550 ("Minor code cleanups and optimizations") introduced this by rewriting % 8 to & 0x7 without parentheses. Parenthesized so the mask applies to sectorInBlock (the 7 - x & 0x7 mask forms are coincidentally equivalent but were parenthesized too for clarity). Co-Authored-By: Claude Opus 4.8 (1M context) --- Library/DiscUtils.Vhd/DynamicStream.cs | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Library/DiscUtils.Vhd/DynamicStream.cs b/Library/DiscUtils.Vhd/DynamicStream.cs index ed05fff85..021f69acf 100644 --- a/Library/DiscUtils.Vhd/DynamicStream.cs +++ b/Library/DiscUtils.Vhd/DynamicStream.cs @@ -174,7 +174,7 @@ public override IEnumerable MapContent(long start, long length) if (offsetInSector != 0 || toRead < Sizes.Sector) { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((blockBitmap[sectorInBlock / 8] & mask) != 0) { var extentStart = (_blockAllocationTable[block] + sectorInBlock) * @@ -190,7 +190,7 @@ public override IEnumerable MapContent(long start, long length) // Processing at least one whole sector, read as many as possible var toReadSectors = toRead / Sizes.Sector; - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var readFromParent = (blockBitmap[sectorInBlock / 8] & mask) == 0; var numSectors = 1; @@ -274,7 +274,7 @@ public override int Read(byte[] buffer, int offset, int count) if (offsetInSector != 0 || toRead < Sizes.Sector) { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((blockBitmap[sectorInBlock / 8] & mask) != 0) { @@ -296,7 +296,7 @@ public override int Read(byte[] buffer, int offset, int count) // Processing at least one whole sector, read as many as possible var toReadSectors = toRead / Sizes.Sector; - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var readFromParent = (blockBitmap[sectorInBlock / 8] & mask) == 0; var numSectors = 1; @@ -377,7 +377,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation if (offsetInSector != 0 || toRead < Sizes.Sector) { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((blockBitmap[sectorInBlock / 8] & mask) != 0) { _fileStream.Position = (_blockAllocationTable[block] + sectorInBlock) * @@ -398,7 +398,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation // Processing at least one whole sector, read as many as possible var toReadSectors = toRead / Sizes.Sector; - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var readFromParent = (blockBitmap[sectorInBlock / 8] & mask) == 0; var numSectors = 1; @@ -479,7 +479,7 @@ public override int Read(Span buffer) if (offsetInSector != 0 || toRead < Sizes.Sector) { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((blockBitmap[sectorInBlock / 8] & mask) != 0) { _fileStream.Position = (_blockAllocationTable[block] + sectorInBlock) * @@ -500,7 +500,7 @@ public override int Read(Span buffer) // Processing at least one whole sector, read as many as possible var toReadSectors = toRead / Sizes.Sector; - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var readFromParent = (blockBitmap[sectorInBlock / 8] & mask) == 0; var numSectors = 1; @@ -621,7 +621,7 @@ public override void Write(byte[] buffer, int offset, int count) // Reduce the write to just the end of the current sector toWrite = Math.Min(count - numWritten, Sizes.Sector - offsetInSector); - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var sectorStart = (_blockAllocationTable[block] + sectorInBlock) * Sizes.Sector + _blockBitmapSize; @@ -665,7 +665,7 @@ public override void Write(byte[] buffer, int offset, int count) // Update all of the bits in the block bitmap for (var i = offset; i < offset + toWrite; i += Sizes.Sector) { - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((_blockBitmaps[block][sectorInBlock / 8] & sectorMask) == 0) { _blockBitmaps[block][sectorInBlock / 8] |= sectorMask; @@ -726,7 +726,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, Cancella // Reduce the write to just the end of the current sector toWrite = Math.Min(buffer.Length - numWritten, Sizes.Sector - offsetInSector); - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var sectorStart = (_blockAllocationTable[block] + sectorInBlock) * Sizes.Sector + _blockBitmapSize; @@ -778,7 +778,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, Cancella // Update all of the bits in the block bitmap for (var i = 0; i < toWrite; i += Sizes.Sector) { - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((_blockBitmaps[block][sectorInBlock / 8] & sectorMask) == 0) { _blockBitmaps[block][sectorInBlock / 8] |= sectorMask; @@ -841,7 +841,7 @@ public override void Write(ReadOnlySpan buffer) // Reduce the write to just the end of the current sector toWrite = Math.Min(buffer.Length - numWritten, Sizes.Sector - offsetInSector); - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); var sectorStart = (_blockAllocationTable[block] + sectorInBlock) * Sizes.Sector + _blockBitmapSize; @@ -885,7 +885,7 @@ public override void Write(ReadOnlySpan buffer) // Update all of the bits in the block bitmap for (var i = 0; i < toWrite; i += Sizes.Sector) { - var sectorMask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var sectorMask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((_blockBitmaps[block][sectorInBlock / 8] & sectorMask) == 0) { _blockBitmaps[block][sectorInBlock / 8] |= sectorMask; @@ -977,11 +977,11 @@ private long FindNextPresentSector(long pos, long maxPos) if (_blockBitmaps[block][sectorInBlock / 8] == 0) { - pos += (8 - sectorInBlock & 0x7) * Sizes.Sector; + pos += (8 - (sectorInBlock & 0x7)) * Sizes.Sector; } else { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((_blockBitmaps[block][sectorInBlock / 8] & mask) != 0) { foundStart = true; @@ -1015,11 +1015,11 @@ private long FindNextAbsentSector(long pos, long maxPos) if (_blockBitmaps[block][sectorInBlock / 8] == 0xFF) { - pos += (8 - sectorInBlock & 0x7) * Sizes.Sector; + pos += (8 - (sectorInBlock & 0x7)) * Sizes.Sector; } else { - var mask = (byte)(1 << (7 - sectorInBlock & 0x7)); + var mask = (byte)(1 << (7 - (sectorInBlock & 0x7))); if ((_blockBitmaps[block][sectorInBlock / 8] & mask) == 0) { foundEnd = true; From e1d5c3a7a5b06551666b3a00463598744fc3ca87 Mon Sep 17 00:00:00 2001 From: Devedse Date: Fri, 19 Jun 2026 16:19:06 +0200 Subject: [PATCH 17/19] Fix SynchronizedSparseStream Position never advancing (regressed in 4b5b75df) The synchronized wrapper kept its own Position auto-property and set content.Position = Position before each operation but never wrote the advanced position back afterward, so Position stayed frozen and reads/writes through the wrapper re-processed the same offset (e.g. ExFat ReadLongSparseLimited read the wrong data). Delegate Position to content.Position under the lock - correct for this single-shared- cursor synchronized wrapper and fixes every read/write path (sync and async) at once. Introduced by 4b5b75df ("Synchronized wrappers for disks and streams"). Co-Authored-By: Claude Opus 4.8 (1M context) --- Library/DiscUtils.Streams/SparseStream.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Library/DiscUtils.Streams/SparseStream.cs b/Library/DiscUtils.Streams/SparseStream.cs index ca96bd1a2..1f281f0b8 100644 --- a/Library/DiscUtils.Streams/SparseStream.cs +++ b/Library/DiscUtils.Streams/SparseStream.cs @@ -656,7 +656,11 @@ private sealed class SynchronizedSparseStream(SparseStream content, Ownership ow public override long Length => content.Length; - public override long Position { get; set; } + public override long Position + { + get { lock (sync) { return content.Position; } } + set { lock (sync) { content.Position = value; } } + } public override void Flush() { From ad6dff0b520a4d081a4ed7b6d92031b58fce1041 Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 25 Jun 2026 13:02:20 +0200 Subject: [PATCH 18/19] Bump version to 1.0.106 Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index dedf5c6bc..9c26671b4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,8 +13,8 @@ LTR Data Kenneth Bell;LordMike;Olof Lagerkvist ..\$(Configuration) - 1.0.105 - 1.0.105 + 1.0.106 + 1.0.106 true From 128e0b20801dc21bdad53b9140022edbebebba36 Mon Sep 17 00:00:00 2001 From: Devedse Date: Sat, 4 Jul 2026 14:50:24 +0200 Subject: [PATCH 19/19] Strip trailing whitespace to match upstream in Bin.cs and RegistryKeyTest.cs --- Library/DiscUtils.Registry/Bin.cs | 2 +- Tests/LibraryTests/Registry/RegistryKeyTest.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Library/DiscUtils.Registry/Bin.cs b/Library/DiscUtils.Registry/Bin.cs index 1212e99a2..7804299e8 100644 --- a/Library/DiscUtils.Registry/Bin.cs +++ b/Library/DiscUtils.Registry/Bin.cs @@ -161,7 +161,7 @@ public Span ReadRawCellData(int cellIndex, Span maxBytes, bool ignor { return ReadBigDataCell(data, maxBytes); } - + // Regular cell data var result = maxBytes[..Math.Min(data.Length, maxBytes.Length)]; diff --git a/Tests/LibraryTests/Registry/RegistryKeyTest.cs b/Tests/LibraryTests/Registry/RegistryKeyTest.cs index 86e60bd5b..524efd1c6 100644 --- a/Tests/LibraryTests/Registry/RegistryKeyTest.cs +++ b/Tests/LibraryTests/Registry/RegistryKeyTest.cs @@ -83,14 +83,14 @@ public void SetVeryLargeValue_BigDataCell() // Test big data cells (used for values >~16KB) // This mimics real-world scenarios like Windows registry ProductPolicy values var buffer = new byte[80 * 1024]; // 80KB - larger than big data threshold - + // Set some distinctive bytes at various positions buffer[0] = 0x12; buffer[100] = 0x34; buffer[16384] = 0x56; // Past first 16KB boundary buffer[32768] = 0x78; // Past second 16KB boundary buffer[buffer.Length - 1] = 0x9A; - + hive.Root.SetValue("verybigvalue", buffer); var readVal = (byte[])hive.Root.GetValue("verybigvalue"); @@ -100,7 +100,7 @@ public void SetVeryLargeValue_BigDataCell() Assert.Equal(0x56, readVal[16384]); Assert.Equal(0x78, readVal[32768]); Assert.Equal(0x9A, readVal[buffer.Length - 1]); - + // Verify entire buffer matches Assert.Equal(buffer, readVal); }