diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8a4a9a..53b649e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,7 +28,9 @@ build-debug: - mv ./AMWD.Common.AspNetCore/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.nupkg ./artifacts/ - - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.snupkg ./artifacts/ + - mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.snupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Debug/*.nupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Debug/*.snupkg ./artifacts/ artifacts: @@ -72,7 +74,9 @@ build-release: - mv ./AMWD.Common.AspNetCore/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.nupkg ./artifacts/ - - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.snupkg ./artifacts/ + - mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.snupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Release/*.nupkg ./artifacts/ + - mv ./AMWD.Common.MessagePack/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Release/*.snupkg ./artifacts/ artifacts: @@ -136,6 +140,19 @@ deploy-entityframework: - if: $CI_COMMIT_TAG =~ /^efc\/v[0-9.]+/ script: - dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/AMWD.Common.EntityFrameworkCore.*.nupkg + +deploy-messagepack: + stage: deploy + dependencies: + - build-release + - test-release + tags: + - docker + - lnx + rules: + - if: $CI_COMMIT_TAG =~ /^msgpack\/v[0-9.]+/ + script: + - dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/AMWD.Common.MessagePack.*.nupkg deploy-test: stage: deploy diff --git a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj index e7d5277..7cece0b 100644 --- a/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj +++ b/AMWD.Common.AspNetCore/AMWD.Common.AspNetCore.csproj @@ -2,7 +2,7 @@ net6.0;net8.0 - 10.0 + 12.0 asp/v[0-9]* AMWD.Common.AspNetCore diff --git a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs index c341fca..b39bb3f 100644 --- a/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/AMWD.Common.AspNetCore/Extensions/HttpContextExtensions.cs @@ -12,12 +12,12 @@ namespace Microsoft.AspNetCore.Http public static class HttpContextExtensions { // Search these additional headers for a remote client ip address. - private static readonly string[] _defaultIpHeaderNames = new[] - { + private static readonly string[] _defaultIpHeaderNames = + [ "Cf-Connecting-Ip", // set by Cloudflare "X-Real-IP", // wide-spread alternative to X-Forwarded-For "X-Forwarded-For", // commonly used on all known proxies - }; + ]; /// /// Retrieves the antiforgery token. diff --git a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs index 515b572..812b39f 100644 --- a/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs +++ b/AMWD.Common.AspNetCore/ModelBinders/InvariantFloatingPointModelBinder.cs @@ -9,26 +9,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// /// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their issue with other cultures than en-US. /// + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + /// The . [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class InvariantFloatingPointModelBinder : IModelBinder + public class InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory) + : IModelBinder { - private readonly NumberStyles _supportedNumberStyles; - private readonly ILogger _logger; - private readonly CultureInfo _cultureInfo; - - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - /// The . - public InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory) - { - _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); - - _supportedNumberStyles = supportedStyles; - _logger = loggerFactory?.CreateLogger(); - } + private readonly NumberStyles _supportedNumberStyles = supportedStyles; + private readonly ILogger _logger = loggerFactory?.CreateLogger(); + private readonly CultureInfo _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); /// public Task BindModelAsync(ModelBindingContext bindingContext) diff --git a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs index f6b90ab..da20985 100644 --- a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationHandler.cs @@ -13,49 +13,39 @@ using Microsoft.Extensions.Options; namespace AMWD.Common.AspNetCore.Security.BasicAuthentication { +#if NET8_0_OR_GREATER /// /// Implements the for Basic Authentication. /// + /// + /// Initializes a new instance of the class. + /// + /// The monitor for the options instance. + /// The . + /// The . + /// An basic autentication validator implementation. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class BasicAuthenticationHandler : AuthenticationHandler + public class BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator) + : AuthenticationHandler(options, logger, encoder) +#else + /// + /// Implements the for Basic Authentication. + /// + /// + /// Initializes a new instance of the class. + /// + /// The monitor for the options instance. + /// The . + /// The . + /// The . + /// An basic autentication validator implementation. + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator) + : AuthenticationHandler(options, logger, encoder, clock) +#endif { - private readonly ILogger _logger; - private readonly IBasicAuthenticationValidator _validator; - -#if NET8_0_OR_GREATER - /// - /// Initializes a new instance of the class. - /// - /// The monitor for the options instance. - /// The . - /// The . - /// An basic autentication validator implementation. - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator) - : base(options, logger, encoder) - { - _logger = logger.CreateLogger(); - _validator = validator; - } -#endif - -#if NET6_0 - - /// - /// Initializes a new instance of the class. - /// - /// The monitor for the options instance. - /// The . - /// The . - /// The . - /// An basic autentication validator implementation. - public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator) - : base(options, logger, encoder, clock) - { - _logger = logger.CreateLogger(); - _validator = validator; - } - -#endif + private readonly ILogger _logger = logger.CreateLogger(); + private readonly IBasicAuthenticationValidator _validator = validator; /// protected override async Task HandleAuthenticateAsync() diff --git a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs index 9bde87a..ca7475d 100644 --- a/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs +++ b/AMWD.Common.AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddleware.cs @@ -11,21 +11,15 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication /// /// Implements a basic authentication. /// - public class BasicAuthenticationMiddleware + /// + /// Initializes a new instance of the class. + /// + /// The following delegate in the process chain. + /// A basic authentication validator. + public class BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator) { - private readonly RequestDelegate _next; - private readonly IBasicAuthenticationValidator _validator; - - /// - /// Initializes a new instance of the class. - /// - /// The following delegate in the process chain. - /// A basic authentication validator. - public BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator) - { - _next = next; - _validator = validator; - } + private readonly RequestDelegate _next = next; + private readonly IBasicAuthenticationValidator _validator = validator; /// /// The delegate invokation. @@ -35,15 +29,27 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication /// An awaitable task. public async Task InvokeAsync(HttpContext httpContext) { +#if NET8_0_OR_GREATER + if (!httpContext.Request.Headers.TryGetValue("Authorization", out var authHeaderValue)) + { + SetAuthenticateRequest(httpContext, _validator.Realm); + return; + } +#else if (!httpContext.Request.Headers.ContainsKey("Authorization")) { SetAuthenticateRequest(httpContext, _validator.Realm); return; } +#endif try { +#if NET8_0_OR_GREATER + var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue); +#else var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); +#endif byte[] decoded = Convert.FromBase64String(authHeader.Parameter); string plain = Encoding.UTF8.GetString(decoded); @@ -70,9 +76,9 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication private static void SetAuthenticateRequest(HttpContext httpContext, string realm) { - httpContext.Response.Headers["WWW-Authenticate"] = "Basic"; + httpContext.Response.Headers.WWWAuthenticate = "Basic"; if (!string.IsNullOrWhiteSpace(realm)) - httpContext.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{realm.Replace("\"", "")}\""; + httpContext.Response.Headers.WWWAuthenticate = $"Basic realm=\"{realm.Replace("\"", "")}\""; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; } diff --git a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs index 8800f3f..41ab28d 100644 --- a/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs +++ b/AMWD.Common.AspNetCore/Security/PathProtection/ProtectedPathMiddleware.cs @@ -8,23 +8,16 @@ namespace AMWD.Common.AspNetCore.Security.PathProtection /// /// Implements a check to provide protected paths. /// - public class ProtectedPathMiddleware + /// + /// Initializes a new instance of the class. + /// + /// The following delegate in the process chain. + /// The options to configure the middleware. + public class ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options) { - private readonly RequestDelegate _next; - private readonly PathString _path; - private readonly string _policyName; - - /// - /// Initializes a new instance of the class. - /// - /// The following delegate in the process chain. - /// The options to configure the middleware. - public ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options) - { - _next = next; - _path = options.Path; - _policyName = options.PolicyName; - } + private readonly RequestDelegate _next = next; + private readonly PathString _path = options.Path; + private readonly string _policyName = options.PolicyName; /// /// The delegate invokation. diff --git a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs index 8492cea..b19d552 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/ConditionClassTagHelper.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers if (items.Any()) { - string classes = string.Join(" ", items.ToArray()); + string classes = string.Join(" ", [.. items]); output.Attributes.Add("class", classes); } } diff --git a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs index c523a8d..d0290f1 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/IntegrityHashTagHelper.cs @@ -13,24 +13,19 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// A tag helper to dynamically create integrity checks for linked sources. /// + /// + /// Initializes a new instance of the class. + /// + /// The web host environment. + /// The application configuration. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("link")] [HtmlTargetElement("script")] - public class IntegrityHashTagHelper : TagHelper + public class IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration) + : TagHelper { - private readonly IWebHostEnvironment _env; - private readonly string _hostUrl; - - /// - /// Initializes a new instance of the class. - /// - /// The web host environment. - /// The application configuration. - public IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration) - { - _env = env; - _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/"); - } + private readonly IWebHostEnvironment _env = env; + private readonly string _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/"); /// /// Gets or sets a value indicating whether the integrity should be calculated. @@ -118,7 +113,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers } string type; - byte[] hashBytes = Array.Empty(); + byte[] hashBytes = []; switch (IntegrityStrength) { case 512: diff --git a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs index 10aad90..41ae538 100644 --- a/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs +++ b/AMWD.Common.AspNetCore/TagHelpers/NumberInputTagHelper.cs @@ -9,17 +9,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers /// /// Adds additional behavior to the modelbinding for numeric properties. /// + /// + /// Initializes a new instance of the class. + /// + /// The HTML generator. [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [HtmlTargetElement("input", Attributes = "asp-for")] - public class NumberInputTagHelper : InputTagHelper + public class NumberInputTagHelper(IHtmlGenerator generator) + : InputTagHelper(generator) { - /// - /// Initializes a new instance of the class. - /// - /// The HTML generator. - public NumberInputTagHelper(IHtmlGenerator generator) - : base(generator) - { } /// public override void Process(TagHelperContext context, TagHelperOutput output) diff --git a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj index 8409a10..4d76a0a 100644 --- a/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj +++ b/AMWD.Common.EntityFrameworkCore/AMWD.Common.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ net6.0;net8.0 - 10.0 + 12.0 efc/v[0-9]* AMWD.Common.EntityFrameworkCore @@ -22,17 +22,17 @@ - - + + - - + + - + diff --git a/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs b/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs index 6475393..18dbd7f 100644 --- a/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs +++ b/AMWD.Common.EntityFrameworkCore/Exceptions/DatabaseProviderException.cs @@ -1,6 +1,4 @@ -using System.Runtime.Serialization; - -namespace System +namespace System { /// /// A DatabaseProvider specific exception. @@ -33,16 +31,16 @@ namespace System : base(message, innerException) { } -#if NET6_0 +#if !NET8_0_OR_GREATER /// /// Initializes a new instance of the class with serialized data. /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. /// The info parameter is null. - /// The class name is null or is zero (0). - protected DatabaseProviderException(SerializationInfo info, StreamingContext context) + /// The class name is null or is zero (0). + protected DatabaseProviderException(Runtime.Serialization.SerializationInfo info, Runtime.Serialization.StreamingContext context) : base(info, context) { } diff --git a/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs b/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs index 1e6f27c..dbcb0b2 100644 --- a/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs +++ b/AMWD.Common.EntityFrameworkCore/Extensions/DatabaseFacadeExtensions.cs @@ -15,7 +15,11 @@ namespace Microsoft.EntityFrameworkCore /// /// Extensions for the . /// +#if NET8_0_OR_GREATER + public static partial class DatabaseFacadeExtensions +#else public static class DatabaseFacadeExtensions +#endif { /// /// Applies migration files to the database. @@ -23,7 +27,7 @@ namespace Microsoft.EntityFrameworkCore /// The database connection. /// An action to set additional options. /// The cancellation token. - /// true on success, otherwise false or an exception is thrown. + /// on success, otherwise false or an exception is thrown. [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208")] public static async Task ApplyMigrationsAsync(this DatabaseFacade database, Action optionsAction, CancellationToken cancellationToken = default) { @@ -211,20 +215,20 @@ END;" if (options.SourceAssembly == null) { availableMigrationFiles = Directory.GetFiles(options.Path) - .Where(f => f.ToLower().StartsWith(options.Path.ToLower())) - .Where(f => f.ToLower().EndsWith(".sql")) + .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase)) + .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)) .ToList(); } else { availableMigrationFiles = options.SourceAssembly .GetManifestResourceNames() - .Where(f => f.ToLower().StartsWith(options.Path.ToLower())) - .Where(f => f.ToLower().EndsWith(".sql")) + .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase)) + .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)) .ToList(); } - if (!availableMigrationFiles.Any()) + if (availableMigrationFiles.Count == 0) return true; using var command = connection.CreateCommand(); @@ -270,7 +274,11 @@ END;" { using var stream = options.SourceAssembly.GetManifestResourceStream(migrationFile); using var sr = new StreamReader(stream); +#if NET8_0_OR_GREATER + sqlScript = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); +#else sqlScript = await sr.ReadToEndAsync().ConfigureAwait(false); +#endif } if (string.IsNullOrWhiteSpace(sqlScript)) @@ -316,7 +324,11 @@ END;" { int affectedRows = 0; // Split script by a single slash in a line +#if NET8_0_OR_GREATER + string[] parts = FindSingleSlashInLine().Split(text); +#else string[] parts = Regex.Split(text, @"\r?\n[ \t]*/[ \t]*\r?\n"); +#endif foreach (string part in parts) { // Make writable copy @@ -325,7 +337,11 @@ END;" // Remove the trailing semicolon from commands where they're not supported // (Oracle doesn't like semicolons. To keep the semicolon, it must be directly // preceeded by "end".) - pt = Regex.Replace(pt.TrimEnd(), @"(?The with applied settings. public static DbContextOptionsBuilder UseDatabaseProvider(this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, Action optionsAction = null) { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(optionsBuilder); + ArgumentNullException.ThrowIfNull(configuration); +#else if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); +#endif var options = new DatabaseProviderOptions(); optionsAction?.Invoke(options); diff --git a/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj b/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj new file mode 100644 index 0000000..986d0c6 --- /dev/null +++ b/AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0;net6.0;net8.0 + 12.0 + + msgpack/v[0-9]* + AMWD.Common.MessagePack + AMWD.Common.MessagePack + + true + AMWD.Common.MessagePack + icon.png + README.md + + AM.WD Common Library for MessagePack + + + + + + + + + + + + + + + + diff --git a/AMWD.Common/Formatters/IPAddressArrayFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs similarity index 92% rename from AMWD.Common/Formatters/IPAddressArrayFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs index 6e9a119..0e40405 100644 --- a/AMWD.Common/Formatters/IPAddressArrayFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPAddressArrayFormatter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; namespace MessagePack.Formatters { @@ -66,7 +66,7 @@ namespace MessagePack.Formatters bytes.AddRange(buffer); } - options.Resolver.GetFormatterWithVerify().Serialize(ref writer, bytes.ToArray(), options); + options.Resolver.GetFormatterWithVerify().Serialize(ref writer, [.. bytes], options); } } } diff --git a/AMWD.Common/Formatters/IPAddressFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressFormatter.cs similarity index 100% rename from AMWD.Common/Formatters/IPAddressFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressFormatter.cs diff --git a/AMWD.Common/Formatters/IPAddressListFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs similarity index 91% rename from AMWD.Common/Formatters/IPAddressListFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs index 0a750bb..1a205b2 100644 --- a/AMWD.Common/Formatters/IPAddressListFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPAddressListFormatter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; namespace MessagePack.Formatters { @@ -61,7 +61,7 @@ namespace MessagePack.Formatters bytes.AddRange(buffer); } - options.Resolver.GetFormatterWithVerify().Serialize(ref writer, bytes.ToArray(), options); + options.Resolver.GetFormatterWithVerify().Serialize(ref writer, [.. bytes], options); } } } diff --git a/AMWD.Common/Formatters/IPNetworkArrayFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs similarity index 86% rename from AMWD.Common/Formatters/IPNetworkArrayFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs index 9866b1f..dfb8550 100644 --- a/AMWD.Common/Formatters/IPNetworkArrayFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkArrayFormatter.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; -using AMWD.Common.Utilities; -using MessagePack; -using MessagePack.Formatters; +using AMWD.Common.MessagePack.Utilities; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif -namespace AMWD.Common.Formatters +namespace MessagePack.Formatters { /// /// Serialization of an array to and from . diff --git a/AMWD.Common/Formatters/IPNetworkFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs similarity index 82% rename from AMWD.Common/Formatters/IPNetworkFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs index 9c408d9..a0aeffb 100644 --- a/AMWD.Common/Formatters/IPNetworkFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkFormatter.cs @@ -1,7 +1,12 @@ using System; using System.Linq; using System.Net; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace MessagePack.Formatters { @@ -15,7 +20,7 @@ namespace MessagePack.Formatters public IPNetwork Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { if (reader.IsNil) - return null; + return default; byte[] bytes = options.Resolver.GetFormatterWithVerify().Deserialize(ref reader, options); return DeserializeInternal(bytes); @@ -24,7 +29,7 @@ namespace MessagePack.Formatters /// public void Serialize(ref MessagePackWriter writer, IPNetwork value, MessagePackSerializerOptions options) { - if (value == null) + if (value == default) { writer.WriteNil(); return; @@ -38,7 +43,11 @@ namespace MessagePack.Formatters { // IP network prefix has a maximum of 128 bit - therefore the length can be covered with a byte. byte prefixLength = (byte)network.PrefixLength; +#if NET8_0_OR_GREATER + byte[] prefixBytes = network.BaseAddress.GetAddressBytes(); +#else byte[] prefixBytes = network.Prefix.GetAddressBytes(); +#endif byte[] bytes = new byte[prefixBytes.Length + 1]; bytes[0] = prefixLength; diff --git a/AMWD.Common/Formatters/IPNetworkListFormatter.cs b/AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs similarity index 87% rename from AMWD.Common/Formatters/IPNetworkListFormatter.cs rename to AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs index 79b3499..7b0f53e 100644 --- a/AMWD.Common/Formatters/IPNetworkListFormatter.cs +++ b/AMWD.Common.MessagePack/Formatters/IPNetworkListFormatter.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using AMWD.Common.Utilities; +using AMWD.Common.MessagePack.Utilities; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace MessagePack.Formatters { diff --git a/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs b/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs new file mode 100644 index 0000000..768adab --- /dev/null +++ b/AMWD.Common.MessagePack/Utilities/NetworkHelper.cs @@ -0,0 +1,17 @@ +using System; + +namespace AMWD.Common.MessagePack.Utilities +{ + /// + /// Provides some network utils. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal static class NetworkHelper + { + public static void SwapBigEndian(byte[] array) + { + if (BitConverter.IsLittleEndian) + Array.Reverse(array); + } + } +} diff --git a/AMWD.Common.Test/AMWD.Common.Test.csproj b/AMWD.Common.Test/AMWD.Common.Test.csproj index f68fc70..fcb3652 100644 --- a/AMWD.Common.Test/AMWD.Common.Test.csproj +++ b/AMWD.Common.Test/AMWD.Common.Test.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 10.0 + 12.0 test/v[0-9]* AMWD.Common.Test @@ -22,7 +22,7 @@ - + diff --git a/AMWD.Common/AMWD.Common.csproj b/AMWD.Common/AMWD.Common.csproj index e2a49d9..d28ffb4 100644 --- a/AMWD.Common/AMWD.Common.csproj +++ b/AMWD.Common/AMWD.Common.csproj @@ -1,8 +1,8 @@  - netstandard2.0 - 10.0 + netstandard2.0;net8.0 + 12.0 AMWD.Common AMWD.Common @@ -21,10 +21,23 @@ - - + + + + + + + + + + + + <_Parameter1>UnitTests + + + diff --git a/AMWD.Common/Cli/CommandLineParser.cs b/AMWD.Common/Cli/CommandLineParser.cs index 76e3537..a7ceecd 100644 --- a/AMWD.Common/Cli/CommandLineParser.cs +++ b/AMWD.Common/Cli/CommandLineParser.cs @@ -14,7 +14,7 @@ namespace AMWD.Common.Cli private string[] _args; private List _parsedArguments; - private readonly List [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public class IPNetworkConverter : JsonConverter + public class IpNetworkConverter : JsonConverter { /// /// List of known types to use this converver. /// - public static readonly Type[] KnownTypes = new[] - { + public static readonly Type[] KnownTypes = + [ typeof(IPNetwork), typeof(IPNetwork[]), typeof(List), typeof(IEnumerable) - }; + ]; /// public override bool CanConvert(Type objectType) - { - return KnownTypes.Contains(objectType); - } + => KnownTypes.Contains(objectType); /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -40,7 +43,7 @@ namespace Newtonsoft.Json if (typeof(IPNetwork) == objectType) return Parse(str); - var networks = str.Split(';').Select(s => Parse(s)); + var networks = str.Split(';').Select(Parse); if (typeof(IPNetwork[]) == objectType) return networks.ToArray(); @@ -65,23 +68,27 @@ namespace Newtonsoft.Json str = ToString(net); if (value is IPNetwork[] netArray) - str = string.Join(";", netArray.Select(n => ToString(n))); + str = string.Join(";", netArray.Select(ToString)); if (value is List netList) - str = string.Join(";", netList.Select(n => ToString(n))); + str = string.Join(";", netList.Select(ToString)); if (value is IEnumerable netEnum) - str = string.Join(";", netEnum.Select(n => ToString(n))); + str = string.Join(";", netEnum.Select(ToString)); writer.WriteValue(str); } - private string ToString(IPNetwork net) + private static string ToString(IPNetwork net) { +#if NET8_0_OR_GREATER + return $"{net.BaseAddress}/{net.PrefixLength}"; +#else return $"{net.Prefix}/{net.PrefixLength}"; +#endif } - private IPNetwork Parse(string str) + private static IPNetwork Parse(string str) { string[] parts = str.Split('/'); var prefix = IPAddress.Parse(parts.First()); diff --git a/AMWD.Common/Extensions/DateTimeExtensions.cs b/AMWD.Common/Extensions/DateTimeExtensions.cs index 2da0626..becc9f1 100644 --- a/AMWD.Common/Extensions/DateTimeExtensions.cs +++ b/AMWD.Common/Extensions/DateTimeExtensions.cs @@ -1,7 +1,5 @@ using System.Text; -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] - namespace System { /// diff --git a/AMWD.Common/Extensions/EnumExtensions.cs b/AMWD.Common/Extensions/EnumExtensions.cs index ee4d431..1a5cece 100644 --- a/AMWD.Common/Extensions/EnumExtensions.cs +++ b/AMWD.Common/Extensions/EnumExtensions.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using System.Linq; namespace System @@ -41,13 +40,5 @@ namespace System /// The description or the string representation of the value. public static string GetDescription(this Enum value) => value.GetAttribute()?.Description ?? value.ToString(); - - /// - /// Returns the name from . - /// - /// The enum value. - /// The display name or the string representation of the value. - public static string GetDisplayName(this Enum value) - => value.GetAttribute()?.Name ?? value.ToString(); } } diff --git a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs index eb644a0..3562252 100644 --- a/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs +++ b/AMWD.Common/Extensions/ReaderWriterLockSlimExtensions.cs @@ -56,16 +56,11 @@ return new DisposableReadWriteLock(rwLock, LockMode.Write); } - private struct DisposableReadWriteLock : IDisposable + private struct DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode) + : IDisposable { - private readonly ReaderWriterLockSlim _rwLock; - private LockMode _lockMode; - - public DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode) - { - _rwLock = rwLock; - _lockMode = lockMode; - } + private readonly ReaderWriterLockSlim _rwLock = rwLock; + private LockMode _lockMode = lockMode; public void Dispose() { diff --git a/AMWD.Common/Extensions/StreamExtensions.cs b/AMWD.Common/Extensions/StreamExtensions.cs index c56409e..481147b 100644 --- a/AMWD.Common/Extensions/StreamExtensions.cs +++ b/AMWD.Common/Extensions/StreamExtensions.cs @@ -40,7 +40,7 @@ namespace System.IO } while (ch != eol); - return encoding.GetString(bytes.ToArray()).Trim(); + return encoding.GetString([.. bytes]).Trim(); } /// @@ -73,7 +73,7 @@ namespace System.IO } while (ch != eol); - return encoding.GetString(bytes.ToArray()).Trim(); + return encoding.GetString([.. bytes]).Trim(); } } } diff --git a/AMWD.Common/Extensions/StringExtensions.cs b/AMWD.Common/Extensions/StringExtensions.cs index cb520b3..7a7e802 100644 --- a/AMWD.Common/Extensions/StringExtensions.cs +++ b/AMWD.Common/Extensions/StringExtensions.cs @@ -15,7 +15,12 @@ namespace System /// /// String extensions. /// +#if NET8_0_OR_GREATER + public static partial class StringExtensions +#else + public static class StringExtensions +#endif { /// /// Converts a hex string into a byte array. @@ -32,8 +37,13 @@ namespace System if (str.Length % 2 == 1) yield break; +#if NET8_0_OR_GREATER + if (InvalidHexCharRegex().IsMatch(str)) + yield break; +#else if (Regex.IsMatch(str, "[^0-9a-fA-F]")) yield break; +#endif for (int i = 0; i < str.Length; i += 2) yield return Convert.ToByte(str.Substring(i, 2), 16); @@ -179,14 +189,14 @@ namespace System { var dnsClientType = Type.GetType("DNS.Client.DnsClient, DNS") ?? throw new DllNotFoundException("The DNS NuGet package is required: https://www.nuget.org/packages/DNS/7.0.0"); var recordTypeType = Type.GetType("DNS.Protocol.RecordType, DNS"); - var resolveMethodInfo = dnsClientType.GetMethod("Resolve", new[] { typeof(string), recordTypeType, typeof(CancellationToken) }); + var resolveMethodInfo = dnsClientType.GetMethod("Resolve", [typeof(string), recordTypeType, typeof(CancellationToken)]); bool exists = false; foreach (var nameserver in nameservers) { - object dnsClient = Activator.CreateInstance(dnsClientType, new object[] { nameserver }); + object dnsClient = Activator.CreateInstance(dnsClientType, [nameserver]); - var waitTask = Task.Run(async () => await resolveMethodInfo.InvokeAsync(dnsClient, new object[] { mailAddress.Host, 15, CancellationToken.None })); // 15 = MX Record + var waitTask = Task.Run(async () => await resolveMethodInfo.InvokeAsync(dnsClient, [mailAddress.Host, 15, CancellationToken.None])); // 15 = MX Record waitTask.Wait(); object response = waitTask.Result; @@ -232,5 +242,10 @@ namespace System /// public static StringBuilder AppendLine(this StringBuilder sb, string value, string newLine) => sb.Append(value).Append(newLine); + +#if NET8_0_OR_GREATER + [GeneratedRegex("[^0-9a-fA-F]")] + private static partial Regex InvalidHexCharRegex(); +#endif } } diff --git a/AMWD.Common/Logging/FileLogger.cs b/AMWD.Common/Logging/FileLogger.cs index 11f3ae1..63c2452 100644 --- a/AMWD.Common/Logging/FileLogger.cs +++ b/AMWD.Common/Logging/FileLogger.cs @@ -6,8 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")] - namespace AMWD.Common.Logging { /// diff --git a/AMWD.Common/Packing/Ar/ArFileInfo.cs b/AMWD.Common/Packing/Ar/ArFileInfo.cs new file mode 100644 index 0000000..f0cbc38 --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArFileInfo.cs @@ -0,0 +1,51 @@ +using System; + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Represents the file information saved in the archive. + /// + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ArFileInfo + { + /// + /// Gets or sets the file name. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the file size in bytes. + /// + public long FileSize { get; set; } + + /// + /// Gets or sets the timestamp of the last modification. + /// + public DateTime ModifyTime { get; set; } + + /// + /// Gets or sets the user id. + /// + public int UserId { get; set; } + + /// + /// Gets or sets the group id. + /// + public int GroupId { get; set; } + + /// + /// Gets or sets the access mode in decimal (not octal!). + /// + /// + /// To see the octal representation use Convert.ToString(Mode, 8). + /// + public int Mode { get; set; } + } + + internal class ArFileInfoExtended : ArFileInfo + { + public long HeaderPosition { get; set; } + + public long DataPosition { get; set; } + } +} diff --git a/AMWD.Common/Packing/Ar/ArReader.cs b/AMWD.Common/Packing/Ar/ArReader.cs new file mode 100644 index 0000000..28b2e41 --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArReader.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Reads UNIX ar (archive) files in the GNU format. + /// + public class ArReader + { + // Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29 + + private readonly Stream _inStream; + private readonly List _files = new(); + private readonly long _streamStartPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read the archive from. + public ArReader(Stream inStream) + { + if (!inStream.CanRead || !inStream.CanSeek) + throw new ArgumentException("Stream not readable or seekable", nameof(inStream)); + + _streamStartPosition = inStream.Position; + _inStream = inStream; + + Initialize(); + } + + /// + /// Returns a list with all filenames of the archive. + /// + public IEnumerable GetFileList() + => _files.Select(fi => fi.FileName).ToList(); + + /// + /// Returns the file info of a specific file in the archive. + /// + /// The name of the specific file. + public ArFileInfo GetFileInfo(string fileName) + { + return _files + .Where(fi => fi.FileName == fileName) + .Select(fi => new ArFileInfo + { + FileName = fi.FileName, + FileSize = fi.FileSize, + GroupId = fi.GroupId, + Mode = fi.Mode, + ModifyTime = fi.ModifyTime, + UserId = fi.UserId + }) + .FirstOrDefault(); + } + + /// + /// Reads a file from the archive into a stream. + /// + /// The file name in the archive. + /// The output stream. + public void ReadFile(string fileName, Stream outStream) + { + if (!outStream.CanWrite) + throw new ArgumentException("Stream not writable", nameof(outStream)); + + var info = _files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + if (info == null) + return; + + long bytesToRead = info.FileSize; + byte[] buffer = new byte[1024 * 1024]; + + _inStream.Seek(info.DataPosition, SeekOrigin.Begin); + while (bytesToRead > 0) + { + int readCount = (int)Math.Min(bytesToRead, buffer.Length); + _inStream.Read(buffer, 0, readCount); + outStream.Write(buffer, 0, readCount); + + bytesToRead -= readCount; + } + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); + } + + /// + /// Reads a fie from the archive and saves it to disk. + /// + /// The file name in the archive. + /// The destination path on disk. + public void ReadFile(string fileName, string destinationPath) + { + var info = _files.Where(fi => fi.FileName == fileName).FirstOrDefault(); + if (info == null) + return; + + using (var fs = File.OpenWrite(destinationPath)) + { + ReadFile(fileName, fs); + } + File.SetLastWriteTimeUtc(destinationPath, info.ModifyTime); + } + + private void Initialize() + { + // Reset stream + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); + + // Read header + string header = ReadAsciiString(8); + if (header != "!\n") + throw new FormatException("The file stream is no archive"); + + // Create file list + while (_inStream.Position < _inStream.Length) + { + var info = ReadFileHeader(); + _files.Add(info); + + // Move stream behind file content + _inStream.Seek(info.FileSize, SeekOrigin.Current); + + // Align to even offsets (padded with LF bytes) + if (_inStream.Position % 2 != 0) + _inStream.Seek(1, SeekOrigin.Current); + } + + // Reset stream + _inStream.Seek(_streamStartPosition, SeekOrigin.Begin); + } + + private string ReadAsciiString(int byteCount) + { + byte[] buffer = new byte[byteCount]; + _inStream.Read(buffer, 0, byteCount); + return Encoding.ASCII.GetString(buffer); + } + + private ArFileInfoExtended ReadFileHeader() + { + long startPosition = _inStream.Position; + + string fileName = ReadAsciiString(16).Trim(); + + int.TryParse(ReadAsciiString(12).Trim(), out int unixTimestamp); + int.TryParse(ReadAsciiString(6).Trim(), out int userId); + int.TryParse(ReadAsciiString(6).Trim(), out int groupId); + int mode = Convert.ToInt32(ReadAsciiString(8).Trim(), 8); + + long.TryParse(ReadAsciiString(10).Trim(), out long fileSize); + + // file magic + byte[] magic = new byte[2]; + _inStream.Read(magic, 0, magic.Length); + + if (magic[0] != 0x60 || magic[1] != 0x0A) // `\n + throw new FormatException("Invalid file magic"); + + return new ArFileInfoExtended + { + HeaderPosition = startPosition, + DataPosition = _inStream.Position, + FileName = fileName, + ModifyTime = DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).DateTime, + UserId = userId, + GroupId = groupId, + Mode = mode, + FileSize = fileSize + }; + } + } +} diff --git a/AMWD.Common/Packing/Ar/ArWriter.cs b/AMWD.Common/Packing/Ar/ArWriter.cs new file mode 100644 index 0000000..fbb948f --- /dev/null +++ b/AMWD.Common/Packing/Ar/ArWriter.cs @@ -0,0 +1,146 @@ +using System; +using System.IO; +using System.Text; + +namespace AMWD.Common.Packing.Ar +{ + /// + /// Writes UNIX ar (archive) files in the GNU format. + /// + /// + /// Copied from: DotnetMakeDeb + /// + public class ArWriter + { + // Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29 + + private readonly Stream _outStream; + + /// + /// Initialises a new instance of the class. + /// + /// The stream to write the archive to. + public ArWriter(Stream outStream) + { + if (!outStream.CanWrite) + throw new ArgumentException("Stream not writable", nameof(outStream)); + + _outStream = outStream; + Initialize(); + } + + /// + /// Writes a file from disk to the archive. + /// + /// The name of the file to copy. + /// The user ID of the file in the archive. + /// The group ID of the file in the archive. + /// The mode of the file in the archive (decimal). + public void WriteFile(string fileName, int userId = 0, int groupId = 0, int mode = 33188 /* 0100644 */) + { + var fi = new FileInfo(fileName); + + using var fs = File.OpenRead(fileName); + WriteFile(fs, fi.Name, fi.LastWriteTimeUtc, userId, groupId, mode); + } + + /// + /// Writes a file from a Stream to the archive. + /// + /// The stream to read the file contents from. + /// The name of the file in the archive. + /// The last modification time of the file in the archive. + /// The user ID of the file in the archive. + /// The group ID of the file in the archive. + /// The mode of the file in the archive (decimal). + public void WriteFile(Stream stream, string fileName, DateTime modifyTime, int userId = 0, int groupId = 0, int mode = 33188 /* 0100644 */) + { + // Write file header + WriteFileHeader(fileName, modifyTime, userId, groupId, mode, stream.Length); + + // Write file contents + stream.CopyTo(_outStream); + + // Align to even offsets, pad with LF bytes + if ((_outStream.Position % 2) != 0) + { + byte[] bytes = new byte[] { 0x0A }; + _outStream.Write(bytes, 0, 1); + } + } + + /// + /// Writes the archive header. + /// + private void Initialize() + { + WriteAsciiString("!\n"); + } + + /// + /// Writes a file header. + /// + private void WriteFileHeader(string fileName, DateTime modifyTime, int userId, int groupId, int mode, long fileSize) + { + // File name + if (fileName.Length > 16) + throw new ArgumentException("Long file names are not supported."); + + WriteAsciiString(fileName.PadRight(16, ' ')); + + // File modification timestamp + long unixTime = ((DateTimeOffset)DateTime.SpecifyKind(modifyTime, DateTimeKind.Utc)).ToUnixTimeSeconds(); + WriteAsciiString(unixTime.ToString().PadRight(12, ' ')); + + // User ID + if (userId >= 0) + { + WriteAsciiString(userId.ToString().PadRight(6, ' ')); + } + else + { + WriteAsciiString(" "); + } + + // Group ID + if (groupId >= 0) + { + WriteAsciiString(groupId.ToString().PadRight(6, ' ')); + } + else + { + WriteAsciiString(" "); + } + + // File mode + if (mode >= 0) + { + WriteAsciiString(Convert.ToString(mode, 8).PadRight(8, ' ')); + } + else + { + WriteAsciiString(" "); + } + + // File size in bytes + if (fileSize < 0 || 10000000000 <= fileSize) + throw new ArgumentOutOfRangeException("Invalid file size."); // above 9.32 GB + + WriteAsciiString(fileSize.ToString().PadRight(10, ' ')); + + // File magic + byte[] bytes = new byte[] { 0x60, 0x0A }; + _outStream.Write(bytes, 0, 2); + } + + /// + /// Writes a string using ASCII encoding. + /// + /// The string to write to the output stream. + private void WriteAsciiString(string str) + { + byte[] bytes = Encoding.ASCII.GetBytes(str); + _outStream.Write(bytes, 0, bytes.Length); + } + } +} diff --git a/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs b/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs new file mode 100644 index 0000000..d621ed3 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs @@ -0,0 +1,26 @@ +namespace AMWD.Common.Packing.Tar.Interfaces +{ + /// + /// Interface of a archive writer. + /// + public interface IArchiveDataWriter + { + /// + /// Write bytes of data from to corresponding archive. + /// + /// The data storage. + /// How many bytes to be written to the corresponding archive. + int Write(byte[] buffer, int count); + + /// + /// Gets a value indicating whether the writer can write. + /// + bool CanWrite { get; } + } + + /// + /// The writer delegate. + /// + /// The writer. + public delegate void WriteDataDelegate(IArchiveDataWriter writer); +} diff --git a/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs b/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs new file mode 100644 index 0000000..f4a474f --- /dev/null +++ b/AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs @@ -0,0 +1,125 @@ +using System; +using AMWD.Common.Packing.Tar.Utils; + +namespace AMWD.Common.Packing.Tar.Interfaces +{ + /// + /// See "struct star_header" in + /// + public interface ITarHeader + { + /// + /// The name field is the file name of the file, with directory names (if any) preceding the file name, + /// separated by slashes. + /// + /// + /// name + ///
+ /// Byte offset: 0 + ///
+ string FileName { get; set; } + + /// + /// The mode field provides nine bits specifying file permissions and three bits to specify + /// the Set UID, Set GID, and Save Text (sticky) modes. + /// When special permissions are required to create a file with a given mode, + /// and the user restoring files from the archive does not hold such permissions, + /// the mode bit(s) specifying those special permissions are ignored. + /// Modes which are not supported by the operating system restoring files from the archive will be ignored. + /// Unsupported modes should be faked up when creating or updating an archive; e.g., + /// the group permission could be copied from the other permission. + /// + /// + /// mode + ///
+ /// Byte offset: 100 + ///
+ int Mode { get; set; } + + /// + /// The uid field is the numeric user ID of the file owners. + /// If the operating system does not support numeric user ID, this field should be ignored. + /// + /// + /// uid + ///
+ /// Byte offset: 108 + ///
+ int UserId { get; set; } + + /// + /// The gid fields is the numeric group ID of the file owners. + /// If the operating system does not support numeric group ID, this field should be ignored. + /// + /// + /// gid + ///
+ /// Byte offset: 116 + ///
+ int GroupId { get; set; } + + /// + /// The size field is the size of the file in bytes; + /// linked files are archived with this field specified as zero. + /// + /// + /// size + ///
+ /// Byte offset: 124 + ///
+ long SizeInBytes { get; set; } + + /// + /// mtime + /// byte offset: 136 + /// The mtime field represents the data modification time of the file at the time it was archived. + /// It represents the integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time. + /// + /// + /// mtime + ///
+ /// Byte offset: 136 + ///
+ DateTime LastModification { get; set; } + + /// + /// The typeflag field specifies the type of file archived. + /// If a particular implementation does not recognize or permit the specified type, + /// the file will be extracted as if it were a regular file. + /// As this action occurs, tar issues a warning to the standard error. + /// + /// + /// typeflag + ///
+ /// Byte offset: 156 + ///
+ EntryType EntryType { get; set; } + + /// + /// The uname field will contain the ASCII representation of the owner of the file. + /// If found, the user ID is used rather than the value in the uid field. + /// + /// + /// uname + ///
+ /// Byte offset: 265 + ///
+ string UserName { get; set; } + + /// + /// The gname field will contain the ASCII representation of the group of the file. + /// If found, the group ID is used rather than the values in the gid field. + /// + /// + /// gname + ///
+ /// Byte offset: 297 + ///
+ string GroupName { get; set; } + + /// + /// The size of this header. + /// + int HeaderSize { get; } + } +} diff --git a/AMWD.Common/Packing/Tar/TarReader.cs b/AMWD.Common/Packing/Tar/TarReader.cs new file mode 100644 index 0000000..51f891f --- /dev/null +++ b/AMWD.Common/Packing/Tar/TarReader.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using AMWD.Common.Packing.Tar.Interfaces; +using AMWD.Common.Packing.Tar.Utils; + +namespace AMWD.Common.Packing.Tar +{ + /// + /// Extract contents of a tar file represented by a stream for the TarReader constructor. + /// + /// + /// Constructs TarReader object to read data from `tarredData` stream. + ///
+ /// Copied from: DotnetMakeDeb + ///
+ /// A stream to read tar archive from + public class TarReader(Stream tarredData) + { + private readonly byte[] _dataBuffer = new byte[512]; + private readonly UsTarHeader _header = new(); + private readonly Stream _inStream = tarredData; + private long _remainingBytesInFile; + + /// + /// Gets the file info (the header). + /// + public ITarHeader FileInfo => _header; + + /// + /// Read all files from an archive to a directory. It creates some child directories to + /// reproduce a file structure from the archive. + /// + /// The out directory. + /// + /// CAUTION! This method is not safe. It's not tar-bomb proof. + /// {see http://en.wikipedia.org/wiki/Tar_(file_format) } + /// If you are not sure about the source of an archive you extracting, + /// then use MoveNext and Read and handle paths like ".." and "../.." according + /// to your business logic. + public void ReadToEnd(string destDirectory) + { + while (MoveNext(skipData: false)) + { + string fileNameFromArchive = FileInfo.FileName; + string totalPath = destDirectory + Path.DirectorySeparatorChar + fileNameFromArchive; + if (UsTarHeader.IsPathSeparator(fileNameFromArchive[fileNameFromArchive.Length - 1]) || FileInfo.EntryType == EntryType.Directory) + { + // Record is a directory + Directory.CreateDirectory(totalPath); + continue; + } + + // If record is a file + string fileName = Path.GetFileName(totalPath); + string directory = totalPath.Remove(totalPath.Length - fileName.Length); + Directory.CreateDirectory(directory); + + using FileStream file = File.Create(totalPath); + Read(file); + } + } + + /// + /// Read data from the current archive to a Stream. + /// + /// A stream to read data to + /// + public void Read(Stream dataDestanation) + { + Debug.WriteLine("tar stream position Read in: " + _inStream.Position); + + int readBytes; + while ((readBytes = Read(out byte[] read)) != -1) + { + Debug.WriteLine("tar stream position Read while(...) : " + _inStream.Position); + dataDestanation.Write(read, 0, readBytes); + } + + Debug.WriteLine("tar stream position Read out: " + _inStream.Position); + } + + /// + /// Reads data from the current archive to a buffer array. + /// + /// The buffer array. + /// The nuber of bytes read. + protected int Read(out byte[] buffer) + { + if (_remainingBytesInFile == 0) + { + buffer = null; + return -1; + } + + int align512 = -1; + long toRead = _remainingBytesInFile - 512; + + if (toRead > 0) + { + toRead = 512; + } + else + { + align512 = 512 - (int)_remainingBytesInFile; + toRead = _remainingBytesInFile; + } + + int bytesRead = _inStream.Read(_dataBuffer, 0, (int)toRead); + _remainingBytesInFile -= bytesRead; + + if (_inStream.CanSeek && align512 > 0) + { + _inStream.Seek(align512, SeekOrigin.Current); + } + else + while (align512 > 0) + { + _inStream.ReadByte(); + --align512; + } + + buffer = _dataBuffer; + return bytesRead; + } + + /// + /// Check if all bytes in buffer are zeroes + /// + /// buffer to check + /// true if all bytes are zeroes, otherwise false + private static bool IsEmpty(IEnumerable buffer) + { + foreach (byte b in buffer) + { + if (b != 0) + return false; + } + return true; + } + + /// + /// Move internal pointer to a next file in archive. + /// + /// Should be true if you want to read a header only, otherwise false + /// false on End Of File otherwise true + /// + /// Example: + /// while(MoveNext()) + /// { + /// Read(dataDestStream); + /// } + /// + public bool MoveNext(bool skipData) + { + Debug.WriteLine("tar stream position MoveNext in: " + _inStream.Position); + if (_remainingBytesInFile > 0) + { + if (!skipData) + { + throw new TarException( + "You are trying to change file while not all the data from the previous one was read. If you do want to skip files use skipData parameter set to true."); + } + + // Skip to the end of file. + if (_inStream.CanSeek) + { + long remainer = _remainingBytesInFile % 512; + _inStream.Seek(_remainingBytesInFile + (512 - (remainer == 0 ? 512 : remainer)), SeekOrigin.Current); + } + else + { + while (Read(out _) != -1) ; + } + } + + byte[] bytes = _header.GetBytes(); + + int headerRead = _inStream.Read(bytes, 0, _header.HeaderSize); + if (headerRead < 512) + throw new TarException("Can not read header"); + + if (IsEmpty(bytes)) + { + headerRead = _inStream.Read(bytes, 0, _header.HeaderSize); + if (headerRead == 512 && IsEmpty(bytes)) + { + Debug.WriteLine("tar stream position MoveNext out(false): " + _inStream.Position); + return false; + } + throw new TarException("Broken archive"); + } + + if (_header.UpdateHeaderFromBytes()) + { + throw new TarException("Checksum check failed"); + } + + _remainingBytesInFile = _header.SizeInBytes; + + Debug.WriteLine("tar stream position MoveNext out(true): " + _inStream.Position); + return true; + } + } +} diff --git a/AMWD.Common/Packing/Tar/TarWriter.cs b/AMWD.Common/Packing/Tar/TarWriter.cs new file mode 100644 index 0000000..12f90f9 --- /dev/null +++ b/AMWD.Common/Packing/Tar/TarWriter.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using AMWD.Common.Packing.Tar.Interfaces; +using AMWD.Common.Packing.Tar.Utils; + +namespace AMWD.Common.Packing.Tar +{ + /// + /// Writes a tar (see GNU tar) archive to a stream. + /// + /// + /// Copied from: DotnetMakeDeb + /// + public class TarWriter : LegacyTarWriter + { + /// + /// Initilizes a new instance of the class. + /// + /// The stream to write the archive to. + public TarWriter(Stream outStream) + : base(outStream) + { } + + /// + /// Writes an entry header (file, dir, ...) to the archive. + /// + /// The name. + /// The last modification time. + /// The number of bytes. + /// The user id. + /// The group id. + /// The access mode. + /// The entry type. + protected override void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) + { + var tarHeader = new UsTarHeader() + { + FileName = name, + Mode = mode, + UserId = userId, + GroupId = groupId, + SizeInBytes = count, + LastModification = lastModificationTime, + EntryType = entryType, + UserName = Convert.ToString(userId, 8), + GroupName = Convert.ToString(groupId, 8) + }; + OutStream.Write(tarHeader.GetHeaderValue(), 0, tarHeader.HeaderSize); + } + + /// + /// Writes an entry header (file, dir, ...) to the archive. + /// Hashes the username and groupname down to a HashCode. + /// + /// The name. + /// The last modification time. + /// The number of bytes. + /// The username. + /// The group name. + /// The access mode. + /// The entry type. + protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, string userName, string groupName, int mode, EntryType entryType) + { + WriteHeader( + name: name, + lastModificationTime: lastModificationTime, + count: count, + userId: userName.GetHashCode(), + groupId: groupName.GetHashCode(), + mode: mode, + entryType: entryType); + } + + /// + /// Writes a file to the archive. + /// + /// The file name. + /// The filesize in bytes. + /// The username. + /// The group name. + /// The access mode. + /// The last modification time. + /// The write handle. + public virtual void Write(string name, long dataSizeInBytes, string userName, string groupName, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate) + { + var writer = new DataWriter(OutStream, dataSizeInBytes); + WriteHeader(name, lastModificationTime, dataSizeInBytes, userName, groupName, mode, EntryType.File); + while (writer.CanWrite) + { + writeDelegate(writer); + } + AlignTo512(dataSizeInBytes, false); + } + + /// + /// Writes a file to the archive. + /// + /// The file stream to add to the archive. + /// The filesize in bytes. + /// The file name. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification time. + public void Write(Stream data, long dataSizeInBytes, string fileName, string userId, string groupId, int mode, + DateTime lastModificationTime) + { + WriteHeader(fileName, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); + WriteContent(dataSizeInBytes, data); + AlignTo512(dataSizeInBytes, false); + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/DataWriter.cs b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs new file mode 100644 index 0000000..2401fcd --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/DataWriter.cs @@ -0,0 +1,45 @@ +using System.IO; +using AMWD.Common.Packing.Tar.Interfaces; + +namespace AMWD.Common.Packing.Tar.Utils +{ + internal class DataWriter : IArchiveDataWriter + { + private readonly long _size; + private long _remainingBytes; + private readonly Stream _stream; + + public DataWriter(Stream data, long dataSizeInBytes) + { + _size = dataSizeInBytes; + _remainingBytes = _size; + _stream = data; + } + + public bool CanWrite { get; private set; } = true; + + public int Write(byte[] buffer, int count) + { + if (_remainingBytes == 0) + { + CanWrite = false; + return -1; + } + + int bytesToWrite; + if (_remainingBytes - count < 0) + { + bytesToWrite = (int)_remainingBytes; + } + else + { + bytesToWrite = count; + } + + _stream.Write(buffer, 0, bytesToWrite); + _remainingBytes -= bytesToWrite; + + return bytesToWrite; + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/EntryType.cs b/AMWD.Common/Packing/Tar/Utils/EntryType.cs new file mode 100644 index 0000000..2eadbec --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/EntryType.cs @@ -0,0 +1,73 @@ +namespace AMWD.Common.Packing.Tar.Utils +{ + ///
+ /// See "Values used in typeflag field." in + /// + public enum EntryType : byte + { + /// + /// AREGTYPE, regular file + /// + File = 0, + + /// + /// REGTYPE, regular file + /// + FileObsolete = 0x30, + + /// + /// LNKTYPE, link + /// + HardLink = 0x31, + + /// + /// SYMTYPE, reserved + /// + SymLink = 0x32, + + /// + /// CHRTYPE, character special + /// + CharDevice = 0x33, + + /// + /// BLKTYPE, block special + /// + BlockDevice = 0x34, + + /// + /// DIRTYPE, directory + /// + Directory = 0x35, + + /// + /// FIFOTYPE, FIFO special + /// + Fifo = 0x36, + + /// + /// CONTTYPE, reserved + /// + Content = 0x37, + + /// + /// XHDTYPE, Extended header referring to the next file in the archive + /// + ExtendedHeader = 0x78, + + /// + /// XGLTYPE, Global extended header + /// + GlobalExtendedHeader = 0x67, + + /// + /// GNUTYPE_LONGLINK, Identifies the *next* file on the tape as having a long linkname. + /// + LongLink = 0x4b, + + /// + /// GNUTYPE_LONGNAME, Identifies the *next* file on the tape as having a long name. + /// + LongName = 0x4c + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs new file mode 100644 index 0000000..1944326 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs @@ -0,0 +1,329 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using AMWD.Common.Packing.Tar.Interfaces; + +namespace AMWD.Common.Packing.Tar.Utils +{ + /// + /// Implements a legacy TAR writer. + /// + /// + /// Writes tar (see GNU tar) archive to a stream + ///
+ /// Copied from: DotnetMakeDeb + ///
+ /// stream to write archive to + public class LegacyTarWriter(Stream outStream) : IDisposable + { + private readonly Stream _outStream = outStream; + private bool _isClosed; + + /// + /// The buffer for writing. + /// + protected byte[] buffer = new byte[1024]; + + /// + /// Gets or sets a value indicating whether to read on zero. + /// + public bool ReadOnZero { get; set; } = true; + + /// + /// Gets the output stream. + /// + protected virtual Stream OutStream + { + get { return _outStream; } + } + + #region IDisposable Members + + /// + public void Dispose() + => Close(); + + #endregion IDisposable Members + + /// + /// Writes a directory entry. + /// + /// The path to the directory. + /// The user id. + /// The group id. + /// The access mode. + /// is not set. + public void WriteDirectoryEntry(string path, int userId, int groupId, int mode) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path), "The path is not set."); + if (path[path.Length - 1] != '/') + path += '/'; + + DateTime lastWriteTime; + if (Directory.Exists(path)) + { + lastWriteTime = Directory.GetLastWriteTime(path); + } + else + { + lastWriteTime = DateTime.Now; + } + + // handle long path names (> 99 characters) + if (path.Length > 99) + { + WriteLongName( + name: path, + userId: userId, + groupId: groupId, + mode: mode, + lastModificationTime: lastWriteTime); + + // shorten the path name so it can be written properly + path = path.Substring(0, 99); + } + + WriteHeader(path, lastWriteTime, 0, userId, groupId, mode, EntryType.Directory); + } + + /// + /// Writes a directory and its contents. + /// + /// The directory. + /// Write also sub-directories. + /// is not set. + public void WriteDirectory(string directory, bool doRecursive) + { + if (string.IsNullOrEmpty(directory)) + throw new ArgumentNullException(nameof(directory), "The directory is not set."); + + WriteDirectoryEntry(directory, 0, 0, 0755); + + string[] files = Directory.GetFiles(directory); + foreach (string fileName in files) + Write(fileName); + + string[] directories = Directory.GetDirectories(directory); + foreach (string dirName in directories) + { + WriteDirectoryEntry(dirName, 0, 0, 0755); + if (doRecursive) + WriteDirectory(dirName, true); + } + } + + /// + /// Writes a file. + /// + /// The file. + /// is not set. + public void Write(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + throw new ArgumentNullException(nameof(fileName), "The file name is not set."); + + using var fileStream = File.OpenRead(fileName); + Write(fileStream, fileStream.Length, fileName, 61, 61, 511, File.GetLastWriteTime(fileStream.Name)); + } + + /// + /// Writes a file stream. + /// + /// The file stream. + public void Write(FileStream file) + { + string path = Path.GetFullPath(file.Name).Replace(Path.GetPathRoot(file.Name), string.Empty); + path = path.Replace(Path.DirectorySeparatorChar, '/'); + Write(file, file.Length, path, 61, 61, 511, File.GetLastWriteTime(file.Name)); + } + + /// + /// Writes a stream. + /// + /// The contents. + /// The file size in bytes. + /// The file name. + public void Write(Stream data, long dataSizeInBytes, string name) + => Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now); + + /// + /// Writes a file to the archive. + /// + /// The file name. + /// The file size in bytes. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification timestamp. + /// The . + public virtual void Write(string name, long dataSizeInBytes, int userId, int groupId, int mode, DateTime lastModificationTime, WriteDataDelegate writeDelegate) + { + var writer = new DataWriter(OutStream, dataSizeInBytes); + + WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); + + while (writer.CanWrite) + writeDelegate(writer); + + AlignTo512(dataSizeInBytes, false); + } + + /// + /// Writes a stream as file to the archive. + /// + /// The content as . + /// The file size in bytes. + /// The file name. + /// The user id. + /// The group id. + /// The access mode. + /// The last modification timestamp. + /// This writer is already closed. + public virtual void Write(Stream data, long dataSizeInBytes, string name, int userId, int groupId, int mode, DateTime lastModificationTime) + { + if (_isClosed) + throw new TarException("Can not write to the closed writer"); + + // handle long file names (> 99 characters) + if (name.Length > 99) + { + WriteLongName( + name: name, + userId: userId, + groupId: groupId, + mode: mode, + lastModificationTime: lastModificationTime); + + // shorten the file name so it can be written properly + name = name.Substring(0, 99); + } + + WriteHeader(name, lastModificationTime, dataSizeInBytes, userId, groupId, mode, EntryType.File); + WriteContent(dataSizeInBytes, data); + AlignTo512(dataSizeInBytes, false); + } + + /// + /// Handle long file or path names (> 99 characters). + /// Write header and content, which its content contain the long (complete) file/path name. + /// This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 + /// + /// File name or path name. + /// User ID. + /// Group ID. + /// Mode. + /// Last modification time. + private void WriteLongName(string name, int userId, int groupId, int mode, DateTime lastModificationTime) + { + // must include a trailing \0 + int nameLength = Encoding.UTF8.GetByteCount(name); + byte[] entryName = new byte[nameLength + 1]; + + Encoding.UTF8.GetBytes(name, 0, name.Length, entryName, 0); + + // add a "././@LongLink" pseudo-entry which contains the full name + using var nameStream = new MemoryStream(entryName); + WriteHeader("././@LongLink", lastModificationTime, entryName.Length, userId, groupId, mode, EntryType.LongName); + WriteContent(entryName.Length, nameStream); + AlignTo512(entryName.Length, false); + } + + /// + /// Writes a stream as file to the archive. + /// + /// The size of the file in bytes. + /// The file content as stream. + /// has not enough to read from. + protected void WriteContent(long count, Stream data) + { + while (count > 0 && count > buffer.Length) + { + int bytesRead = data.Read(buffer, 0, buffer.Length); + if (bytesRead < 0) + throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); + + if (bytesRead == 0) + { + if (ReadOnZero) + Thread.Sleep(100); + else + break; + } + OutStream.Write(buffer, 0, bytesRead); + count -= bytesRead; + } + if (count > 0) + { + int bytesRead = data.Read(buffer, 0, (int)count); + if (bytesRead < 0) + throw new IOException($"{nameof(LegacyTarWriter)} unable to read from provided stream"); + + if (bytesRead == 0) + { + while (count > 0) + { + OutStream.WriteByte(0); + --count; + } + } + else + OutStream.Write(buffer, 0, bytesRead); + } + } + + /// + /// Writes a entry header to the archive. + /// + /// The file name. + /// The last modification time. + /// The number of bytes. + /// The user id. + /// The group id. + /// The file mode. + /// The entry type + protected virtual void WriteHeader(string name, DateTime lastModificationTime, long count, int userId, int groupId, int mode, EntryType entryType) + { + var header = new TarHeader + { + FileName = name, + LastModification = lastModificationTime, + SizeInBytes = count, + UserId = userId, + GroupId = groupId, + Mode = mode, + EntryType = entryType + }; + OutStream.Write(header.GetHeaderValue(), 0, header.HeaderSize); + } + + /// + /// Aligns the entry to 512 bytes. + /// + public void AlignTo512(long size, bool acceptZero) + { + size %= 512; + if (size == 0 && !acceptZero) return; + while (size < 512) + { + OutStream.WriteByte(0); + size++; + } + } + + /// + /// Closes the writer and aligns to 512 bytes. + /// + public virtual void Close() + { + if (_isClosed) + return; + + AlignTo512(0, true); + AlignTo512(0, true); + + _isClosed = true; + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/TarException.cs b/AMWD.Common/Packing/Tar/Utils/TarException.cs new file mode 100644 index 0000000..ed83e55 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/TarException.cs @@ -0,0 +1,52 @@ +using System; + +namespace AMWD.Common.Packing.Tar.Utils +{ + /// + /// Represents errors that occur during tar archive execution. + /// + public class TarException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public TarException() + : base() + { } + + /// + /// Initializes a new instance of the class with a specified + /// error message. + /// + /// The message that describes the error. + public TarException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the System.Exception class with a specified error + /// message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference + /// if no inner exception is specified. + public TarException(string message, Exception innerException) + : base(message, innerException) + { } + +#if !NET8_0_OR_GREATER + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The that holds the serialized + /// object data about the exception being thrown. + /// The that contains contextual information + /// about the source or destination. + protected TarException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { } + +#endif + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/TarHeader.cs b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs new file mode 100644 index 0000000..c1f1107 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/TarHeader.cs @@ -0,0 +1,179 @@ +using System; +using System.Linq; +using System.Net; +using System.Text; +using AMWD.Common.Packing.Tar.Interfaces; + +namespace AMWD.Common.Packing.Tar.Utils +{ + internal class TarHeader : ITarHeader + { + private static readonly byte[] _spaces = Encoding.ASCII.GetBytes(" "); + private readonly byte[] _buffer = new byte[512]; + + private string _fileName; + private long _headerChecksum; + + public TarHeader() + { + // Default values + Mode = 511; // 0777 dec + UserId = 61; // 101 dec + GroupId = 61; // 101 dec + } + + public EntryType EntryType { get; set; } + + public virtual string FileName + { + get => _fileName.Replace("\0", string.Empty); + set + { + if (value.Length > 100) + throw new TarException("A file name can not be more than 100 chars long"); + + _fileName = value; + } + } + + public int Mode { get; set; } + + public string ModeString + => Convert.ToString(Mode, 8).PadLeft(7, '0'); + + public int UserId { get; set; } + + public virtual string UserName + { + get => UserId.ToString(); + set => UserId = int.Parse(value); + } + + public string UserIdString + => Convert.ToString(UserId, 8).PadLeft(7, '0'); + + public int GroupId { get; set; } + + public virtual string GroupName + { + get => GroupId.ToString(); + set => GroupId = int.Parse(value); + } + + public string GroupIdString + => Convert.ToString(GroupId, 8).PadLeft(7, '0'); + + public long SizeInBytes { get; set; } + + public string SizeString + => Convert.ToString(SizeInBytes, 8).PadLeft(11, '0'); + + public DateTime LastModification { get; set; } + + public string LastModificationString + { + get + { + long unixTime = ((DateTimeOffset)DateTime.SpecifyKind(LastModification, DateTimeKind.Utc)).ToUnixTimeSeconds(); + return Convert.ToString(unixTime, 8).PadLeft(11, '0'); + } + } + + public string HeaderChecksumString + => Convert.ToString(_headerChecksum, 8).PadLeft(6, '0'); + + public virtual int HeaderSize => 512; + + public byte[] GetBytes() => _buffer.ToArray(); + + public virtual bool UpdateHeaderFromBytes() + { + FileName = Encoding.ASCII.GetString(_buffer, 0, 100); + + // Thanks to Shasha Alperocivh. Trimming nulls. + Mode = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, 100, 7).Trim(), 8); + UserId = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, 108, 7).Trim(), 8); + GroupId = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, 116, 7).Trim(), 8); + + EntryType = (EntryType)_buffer[156]; + + if ((_buffer[124] & 0x80) == 0x80) // if size in binary + { + long sizeBigEndian = BitConverter.ToInt64(_buffer, 0x80); + SizeInBytes = IPAddress.NetworkToHostOrder(sizeBigEndian); + } + else + { + SizeInBytes = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, 124, 11), 8); + } + long unixTimeStamp = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, 136, 11), 8); + LastModification = DateTimeOffset.FromUnixTimeSeconds(unixTimeStamp).DateTime; + + int storedChecksum = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, 148, 6)); + RecalculateChecksum(_buffer); + if (storedChecksum == _headerChecksum) + return true; + + RecalculateAltChecksum(_buffer); + return storedChecksum == _headerChecksum; + } + + private void RecalculateAltChecksum(byte[] buf) + { + _spaces.CopyTo(buf, 148); + _headerChecksum = 0; + foreach (byte b in buf) + { + if ((b & 0x80) == 0x80) + { + _headerChecksum -= b ^ 0x80; + } + else + { + _headerChecksum += b; + } + } + } + + public virtual byte[] GetHeaderValue() + { + // Clean old values + Array.Clear(_buffer, 0, _buffer.Length); + + if (string.IsNullOrEmpty(FileName)) + throw new TarException("FileName can not be empty."); + + if (FileName.Length >= 100) + throw new TarException("FileName is too long. It must be less than 100 bytes."); + + // Fill header + Encoding.ASCII.GetBytes(FileName.PadRight(100, '\0')).CopyTo(_buffer, 0); + Encoding.ASCII.GetBytes(ModeString).CopyTo(_buffer, 100); + Encoding.ASCII.GetBytes(UserIdString).CopyTo(_buffer, 108); + Encoding.ASCII.GetBytes(GroupIdString).CopyTo(_buffer, 116); + Encoding.ASCII.GetBytes(SizeString).CopyTo(_buffer, 124); + Encoding.ASCII.GetBytes(LastModificationString).CopyTo(_buffer, 136); + + // buffer[156] = 20; + _buffer[156] = ((byte)EntryType); + + RecalculateChecksum(_buffer); + + // Write checksum + Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(_buffer, 148); + + return _buffer; + } + + protected virtual void RecalculateChecksum(byte[] buf) + { + // Set default value for checksum. That is 8 spaces. + _spaces.CopyTo(buf, 148); + + // Calculate checksum + _headerChecksum = 0; + foreach (byte b in buf) + _headerChecksum += b; + } + } +} diff --git a/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs new file mode 100644 index 0000000..4f26bf3 --- /dev/null +++ b/AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs @@ -0,0 +1,127 @@ +using System; +using System.Net; +using System.Text; + +namespace AMWD.Common.Packing.Tar.Utils +{ + /// + /// UsTar header implementation. + /// + internal class UsTarHeader : TarHeader + { + private const string Magic = "ustar"; + private const string Version = " "; + + private string _groupName; + + private string _namePrefix = string.Empty; + private string _userName; + + public override string UserName + { + get => _userName.Replace("\0", string.Empty); + set + { + if (value.Length > 32) + throw new TarException("user name can not be longer than 32 chars"); + + _userName = value; + } + } + + public override string GroupName + { + get => _groupName.Replace("\0", string.Empty); + set + { + if (value.Length > 32) + throw new TarException("group name can not be longer than 32 chars"); + + _groupName = value; + } + } + + public override string FileName + { + get => _namePrefix.Replace("\0", string.Empty) + base.FileName.Replace("\0", string.Empty); + set + { + if (value.Length > 100) + { + if (value.Length > 255) + throw new TarException("UsTar fileName can not be longer thatn 255 chars"); + + int position = value.Length - 100; + + // Find first path separator in the remaining 100 chars of the file name + while (!IsPathSeparator(value[position])) + { + ++position; + if (position == value.Length) + { + break; + } + } + if (position == value.Length) + position = value.Length - 100; + _namePrefix = value.Substring(0, position); + + base.FileName = value.Substring(position, value.Length - position); + } + else + { + base.FileName = value; + } + } + } + + public override bool UpdateHeaderFromBytes() + { + byte[] bytes = GetBytes(); + + UserName = Encoding.ASCII.GetString(bytes, 0x109, 32); + GroupName = Encoding.ASCII.GetString(bytes, 0x129, 32); + _namePrefix = Encoding.ASCII.GetString(bytes, 347, 157); + + return base.UpdateHeaderFromBytes(); + } + + internal static bool IsPathSeparator(char ch) + => ch == '\\' || ch == '/' || ch == '|'; // All the path separators I ever met. + + public override byte[] GetHeaderValue() + { + byte[] header = base.GetHeaderValue(); + + Encoding.ASCII.GetBytes(Magic).CopyTo(header, 0x101); // Mark header as ustar + Encoding.ASCII.GetBytes(Version).CopyTo(header, 0x106); + Encoding.ASCII.GetBytes(UserName).CopyTo(header, 0x109); + Encoding.ASCII.GetBytes(GroupName).CopyTo(header, 0x129); + Encoding.ASCII.GetBytes(_namePrefix).CopyTo(header, 347); + + if (SizeInBytes >= 0x1FFFFFFFF) + { + byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(SizeInBytes)); + SetMarker(AlignTo12(bytes)).CopyTo(header, 124); + } + + RecalculateChecksum(header); + Encoding.ASCII.GetBytes(HeaderChecksumString).CopyTo(header, 148); + + return header; + } + + private static byte[] SetMarker(byte[] bytes) + { + bytes[0] |= 0x80; + return bytes; + } + + private static byte[] AlignTo12(byte[] bytes) + { + byte[] retVal = new byte[12]; + bytes.CopyTo(retVal, 12 - bytes.Length); + return retVal; + } + } +} diff --git a/AMWD.Common/Utilities/AsyncQueue.cs b/AMWD.Common/Utilities/AsyncQueue.cs index a3e2ba9..9ad8c10 100644 --- a/AMWD.Common/Utilities/AsyncQueue.cs +++ b/AMWD.Common/Utilities/AsyncQueue.cs @@ -94,7 +94,7 @@ namespace System.Collections.Generic /// Determines whether an element is in the . ///
/// The object to locate in the . The value can be null for reference types. - /// true if item is found in the ; otherwise, false. + /// if item is found in the , otherwise . [ExcludeFromCodeCoverage] public bool Contains(T item) { @@ -172,7 +172,7 @@ namespace System.Collections.Generic { lock (_queue) { - return _queue.ToArray(); + return [.. _queue]; } } @@ -304,7 +304,7 @@ namespace System.Collections.Generic /// Removes the object at the beginning of the , and copies it to the parameter. /// /// The removed object. - /// true if the object is successfully removed; false if the is empty. + /// if the object is successfully removed, if the is empty. public bool TryDequeue(out T result) { try @@ -325,7 +325,7 @@ namespace System.Collections.Generic /// parameter. The object is not removed from the . /// /// If present, the object at the beginning of the ; otherwise, the default value of . - /// true if there is an object at the beginning of the ; false if the is empty. + /// if there is an object at the beginning of the , if the is empty. public bool TryPeek(out T result) { try @@ -344,7 +344,7 @@ namespace System.Collections.Generic /// Removes the first occurrence of a specific object from the . /// /// The object to remove from the . The value can be null for reference types. - /// true if item is successfully removed; otherwise, false. This method also returns false if item was not found in the . + /// if item is successfully removed, otherwise . This method also returns if item was not found in the . public bool Remove(T item) { lock (_queue) diff --git a/AMWD.Common/Utilities/CryptographyHelper.cs b/AMWD.Common/Utilities/CryptographyHelper.cs index 1842f97..5967256 100644 --- a/AMWD.Common/Utilities/CryptographyHelper.cs +++ b/AMWD.Common/Utilities/CryptographyHelper.cs @@ -171,6 +171,7 @@ namespace System.Security.Cryptography #region Static methods #region Encryption +#pragma warning disable SYSLIB0041 #region AES @@ -185,7 +186,11 @@ namespace System.Security.Cryptography byte[] salt = new byte[_saltLength]; Array.Copy(cipher, salt, _saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; @@ -225,7 +230,11 @@ namespace System.Security.Cryptography { byte[] salt = GetRandomBytes(_saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; @@ -271,7 +280,11 @@ namespace System.Security.Cryptography byte[] salt = new byte[_saltLength]; Array.Copy(cipher, salt, _saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var tdes = TripleDES.Create(); tdes.Mode = CipherMode.CBC; @@ -298,7 +311,11 @@ namespace System.Security.Cryptography { byte[] salt = GetRandomBytes(_saltLength); - using var gen = new Rfc2898DeriveBytes(password, salt); +#if NET8_0_OR_GREATER + using var gen = new Rfc2898DeriveBytes(password, salt, 1000, HashAlgorithmName.SHA1); +#else + using var gen = new Rfc2898DeriveBytes(password, salt, 1000); +#endif using var tdes = TripleDES.Create(); tdes.Mode = CipherMode.CBC; @@ -344,6 +361,7 @@ namespace System.Security.Cryptography #endregion Triple DES +#pragma warning restore SYSLIB0041 #endregion Encryption #region Hashing @@ -379,8 +397,12 @@ namespace System.Security.Cryptography /// The MD5 hash value, in hexadecimal notation. public static string Md5(byte[] bytes) { +#if NET8_0_OR_GREATER + return MD5.HashData(bytes).BytesToHex(); +#else using var md5 = MD5.Create(); return md5.ComputeHash(bytes).BytesToHex(); +#endif } #endregion MD5 @@ -416,8 +438,12 @@ namespace System.Security.Cryptography /// The SHA-1 hash value, in hexadecimal notation. public static string Sha1(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA1.HashData(bytes).BytesToHex(); +#else using var sha1 = SHA1.Create(); return sha1.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-1 @@ -453,8 +479,12 @@ namespace System.Security.Cryptography /// The SHA-256 hash value, in hexadecimal notation. public static string Sha256(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA256.HashData(bytes).BytesToHex(); +#else using var sha256 = SHA256.Create(); return sha256.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-256 @@ -490,8 +520,12 @@ namespace System.Security.Cryptography /// The SHA-512 hash value, in hexadecimal notation. public static string Sha512(byte[] bytes) { +#if NET8_0_OR_GREATER + return SHA512.HashData(bytes).BytesToHex(); +#else using var sha512 = SHA512.Create(); return sha512.ComputeHash(bytes).BytesToHex(); +#endif } #endregion SHA-512 diff --git a/AMWD.Common/Utilities/NetworkHelper.cs b/AMWD.Common/Utilities/NetworkHelper.cs index a71dc76..9faf02e 100644 --- a/AMWD.Common/Utilities/NetworkHelper.cs +++ b/AMWD.Common/Utilities/NetworkHelper.cs @@ -4,7 +4,12 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +#if NET8_0_OR_GREATER +using IPNetwork = System.Net.IPNetwork; +#else using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +#endif namespace AMWD.Common.Utilities { @@ -23,7 +28,7 @@ namespace AMWD.Common.Utilities public static List ResolveHost(string hostname, AddressFamily addressFamily = default) { if (string.IsNullOrWhiteSpace(hostname)) - return new(); + return []; if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) addressFamily = AddressFamily.Unspecified; @@ -31,7 +36,7 @@ namespace AMWD.Common.Utilities var ipAddress = ResolveIpAddress(hostname, addressFamily); // the name was an ip address, should not happen but experience tells other stories if (ipAddress != null) - return new() { ipAddress }; + return [ipAddress]; try { @@ -41,7 +46,7 @@ namespace AMWD.Common.Utilities } catch { - return new(); + return []; } } @@ -54,7 +59,7 @@ namespace AMWD.Common.Utilities public static List ResolveInterface(string interfaceName, AddressFamily addressFamily = default) { if (string.IsNullOrWhiteSpace(interfaceName)) - return new(); + return []; if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) addressFamily = AddressFamily.Unspecified; @@ -62,7 +67,7 @@ namespace AMWD.Common.Utilities var ipAddress = ResolveIpAddress(interfaceName, addressFamily); // the name was an ip address, should not happen but experience tells other stories if (ipAddress != null) - return new() { ipAddress }; + return [ipAddress]; try { @@ -74,45 +79,7 @@ namespace AMWD.Common.Utilities } catch { - return new(); - } - } - - /// - /// Parses a CIDR network definition. - /// - /// The network in CIDR. - /// The or null. - public static IPNetwork ParseNetwork(string network) - { - TryParseNetwork(network, out var ipNetwork); - return ipNetwork; - } - - /// - /// Tries to parse a CIDR network definition. - /// - /// The network in CIDR. - /// The parsed . - /// true on success, otherwise false. - public static bool TryParseNetwork(string network, out IPNetwork ipNetwork) - { - try - { - string[] parts = network.Split('/'); - if (parts.Length != 2) - throw new ArgumentException($"Invalid network type"); - - var prefix = IPAddress.Parse(parts.First()); - int prefixLength = int.Parse(parts.Last()); - - ipNetwork = new IPNetwork(prefix, prefixLength); - return true; - } - catch - { - ipNetwork = null; - return false; + return []; } } @@ -128,7 +95,11 @@ namespace AMWD.Common.Utilities { var list = new List(); +#if NET8_0_OR_GREATER + var ipAddress = network.BaseAddress; +#else var ipAddress = network.Prefix; +#endif while (network.Contains(ipAddress)) { list.Add(ipAddress); diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a2da6..4e8b6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ###### Diffs -- [AMWD.Common](https://git.am-wd.de/AM.WD/common/compare/v2.0.1...main) -- [AMWD.Common.AspNetCore](https://git.am-wd.de/AM.WD/common/compare/asp/v3.0.0...main) -- [AMWD.Common.EntityFrameworkCore](https://git.am-wd.de/AM.WD/common/compare/efc/v3.0.0...main) -- [AMWD.Common.Test](https://git.am-wd.de/AM.WD/common/compare/test/v2.1.1...main) +- [AMWD.Common](https://git.am-wd.de/AM.WD/common/compare/v2.0.1...HEAD) +- [AMWD.Common.AspNetCore](https://git.am-wd.de/AM.WD/common/compare/asp/v3.0.0...HEAD) +- [AMWD.Common.EntityFrameworkCore](https://git.am-wd.de/AM.WD/common/compare/efc/v3.0.0...HEAD) +- [AMWD.Common.Test](https://git.am-wd.de/AM.WD/common/compare/test/v2.1.1...HEAD) -_no changes yet_ +### Added + +- `ArReader` and `ArWriter` for Unix archives +- `TarReader` and `TarWriter` for TAR archives + +### Changed + +- Optimized for C# 12 +- `IPNetwork` is used from (as `Microsoft.AspNetCore.HttpOverrides` is taged as "out of support"): + - .NET Standard 2.0 and .NET 6.0: `Microsoft.AspNetCore.HttpOverrides.IPNetwork` + - .NET 8.0: `System.Net.IPNetwork` +- Moved `MessagePack` extensions formatter extensions to own package `AMWD.Common.MessagePack` + +### Removed + +- `GetDisplayName` for enum values was removed ## asp/v3.0.0, efc/v3.0.0 - 2023-12-28 diff --git a/Common.sln b/Common.sln index f5bfa05..c1d89ff 100644 --- a/Common.sln +++ b/Common.sln @@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{93EC8B16 nuget.config = nuget.config EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common.MessagePack", "AMWD.Common.MessagePack\AMWD.Common.MessagePack.csproj", "{EA014C15-93B6-4F2C-8229-1C13E22BF84A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {9469D87B-126E-4338-92E3-701F762CB54D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.Build.0 = Release|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA014C15-93B6-4F2C-8229-1C13E22BF84A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -80,6 +86,7 @@ Global {86DE1B7C-3ECF-49B1-AB28-A976A3973FF5} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} {7196DA2B-D858-4B25-BC23-865175CFCDEC} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} {93EC8B16-7DEF-4E39-B590-E804DEF7C607} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} + {EA014C15-93B6-4F2C-8229-1C13E22BF84A} = {F2C7556A-99EB-43EB-8954-56A24AFE928F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC} diff --git a/Directory.Build.props b/Directory.Build.props index 06b7292..553ca7c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ true false - false true true @@ -30,11 +29,11 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs index 9377d14..20ac3b5 100644 --- a/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs +++ b/UnitTests/AspNetCore/Security/BasicAuthentication/BasicAuthenticationMiddlewareTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using AMWD.Common.AspNetCore.Security.BasicAuthentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -156,12 +157,16 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication var requestHeaderMock = new Mock(); foreach (var header in _requestHeaders) { + var strVal = new StringValues(header.Value); requestHeaderMock .Setup(h => h.ContainsKey(header.Key)) .Returns(true); requestHeaderMock .Setup(h => h[header.Key]) - .Returns(header.Value); + .Returns(strVal); + requestHeaderMock + .Setup(h => h.TryGetValue(header.Key, out strVal)) + .Returns(true); } var requestMock = new Mock(); @@ -174,6 +179,11 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication responseHeaderMock .SetupSet(h => h[It.IsAny()] = It.IsAny()) .Callback((key, value) => _responseHeadersCallback[key] = value); +#pragma warning disable CS0618 + responseHeaderMock + .SetupSet(h => h.WWWAuthenticate) + .Callback((value) => _responseHeadersCallback[HeaderNames.WWWAuthenticate] = value); +#pragma warning restore CS0618 var responseMock = new Mock(); responseMock diff --git a/UnitTests/Common/Extensions/EnumExtensionsTests.cs b/UnitTests/Common/Extensions/EnumExtensionsTests.cs index 7f0cd93..0099f7a 100644 --- a/UnitTests/Common/Extensions/EnumExtensionsTests.cs +++ b/UnitTests/Common/Extensions/EnumExtensionsTests.cs @@ -108,22 +108,6 @@ namespace UnitTests.Common.Extensions Assert.IsFalse(list.Any()); } - [TestMethod] - public void ShouldReturnDisplayNameOrStringRepresentation() - { - // arrange - var enumWithDisplayName = TestEnum.Two; - var enumWithoutDisplayName = TestEnum.Zero; - - // act - string displayName = enumWithDisplayName.GetDisplayName(); - string noDisplayName = enumWithoutDisplayName.GetDisplayName(); - - // assert - Assert.AreEqual("Zwei", displayName); - Assert.AreEqual(enumWithoutDisplayName.ToString(), noDisplayName); - } - internal enum TestEnum { [CustomMultiple("nix")] @@ -132,7 +116,6 @@ namespace UnitTests.Common.Extensions Zero, [Description("Eins")] One, - [Display(Name = "Zwei")] Two, } } diff --git a/UnitTests/Common/Packing/Ar/ArReaderTests.cs b/UnitTests/Common/Packing/Ar/ArReaderTests.cs new file mode 100644 index 0000000..712a59a --- /dev/null +++ b/UnitTests/Common/Packing/Ar/ArReaderTests.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using AMWD.Common.Packing.Ar; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Common.Packing.Ar +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ArReaderTests + { + private readonly DateTime _fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc); + + private Dictionary _files; + + private Stream inStream; + + [TestInitialize] + public void Initialize() + { + _files = new Dictionary + { + { + "abcd.tmp", + new ArFileInfo + { + FileName = "abcd.tmp", + FileSize = 14, + GroupId = 456, + Mode = 33188, + ModifyTime = _fixedDateTime, + UserId = 123 + } + }, + { + "efgh.tmp", + new ArFileInfo + { + FileName = "efgh.tmp", + FileSize = 14, + GroupId = 456, + Mode = 33188, + ModifyTime = _fixedDateTime, + UserId = 123 + } + }, + { + "ijkl.tmp", + new ArFileInfo + { + FileName = "ijkl.tmp", + FileSize = 13, + GroupId = 456, + Mode = 33188, + ModifyTime = _fixedDateTime, + UserId = 123 + } + } + }; + + inStream = new MemoryStream(); + inStream.Write(Encoding.ASCII.GetBytes("!\n")); + + foreach (var file in _files) + { + int unixSeconds = (int)file.Value.ModifyTime.Subtract(DateTime.UnixEpoch).TotalSeconds; + + inStream.Write(Encoding.ASCII.GetBytes($"{file.Key,-16}{unixSeconds,-12}123 456 100644 {file.Value.FileSize,-10}`\n")); + inStream.Write(Encoding.UTF8.GetBytes(new string('a', (int)file.Value.FileSize))); + if (file.Value.FileSize % 2 != 0) + inStream.Write(Encoding.ASCII.GetBytes("\n")); + } + + inStream.Seek(0, SeekOrigin.Begin); + } + + [TestCleanup] + public void Cleanup() + { + inStream.Dispose(); + inStream = null; + } + + [TestMethod] + public void ShouldInitializeArchive() + { + // Arrange + inStream.Dispose(); + inStream = new MemoryStream(); + inStream.Write(Encoding.ASCII.GetBytes("!\n")); + inStream.Seek(0, SeekOrigin.Begin); + + // Act + var reader = new ArReader(inStream); + + // Assert + Assert.IsNotNull(reader); + Assert.IsFalse(reader.GetFileList().Any()); + } + + [TestMethod] + public void ShouldInitializeWithFiles() + { + // Arrange + + // Act + var reader = new ArReader(inStream); + + // Assert + Assert.IsNotNull(reader); + Assert.IsTrue(reader.GetFileList().Any()); + } + + [TestMethod] + public void ShouldListFileNames() + { + // Arrange + var reader = new ArReader(inStream); + + // Act + var fileList = reader.GetFileList().ToList(); + + // Assert + Assert.IsNotNull(reader); + Assert.AreEqual(_files.Count, fileList.Count); + + foreach (string name in _files.Keys) + Assert.IsTrue(fileList.Contains(name)); + } + + [TestMethod] + public void ShouldReturnValidFileInfo() + { + // Arrange + var infos = new List(); + var reader = new ArReader(inStream); + + // Act + foreach (string name in _files.Keys) + infos.Add(reader.GetFileInfo(name)); + + // Assert + Assert.IsNotNull(reader); + Assert.AreEqual(_files.Count, infos.Count); + + foreach (var expected in _files.Values) + { + var actual = infos.Single(fi => fi.FileName == expected.FileName); + + Assert.AreEqual(expected.FileName, actual.FileName); + Assert.AreEqual(expected.FileSize, actual.FileSize); + Assert.AreEqual(expected.GroupId, actual.GroupId); + Assert.AreEqual(expected.Mode, actual.Mode); + Assert.AreEqual(expected.ModifyTime, actual.ModifyTime); + Assert.AreEqual(expected.UserId, actual.UserId); + } + } + + [TestMethod] + public void ShouldReturnValidFileContent() + { + // Arrange + var contents = new Dictionary(); + var reader = new ArReader(inStream); + + // Act + foreach (string name in _files.Keys) + { + using var ms = new MemoryStream(); + reader.ReadFile(name, ms); + ms.Seek(0, SeekOrigin.Begin); + + contents.Add(name, Encoding.UTF8.GetString(ms.ToArray())); + } + + // Assert + Assert.IsNotNull(reader); + Assert.AreEqual(_files.Count, contents.Count); + + foreach (var expected in _files.Values) + { + string content = contents[expected.FileName]; + + if (expected.FileSize % 2 != 0) + Assert.AreEqual(13, content.Length); + else + Assert.AreEqual(14, content.Length); + + Assert.AreEqual(new string('a', (int)expected.FileSize), content); + } + } + + [TestMethod] + public void ShouldWriteFileToDisk() + { + // Arrange + string tmpFile = Path.GetTempFileName(); + var reader = new ArReader(inStream); + + try + { + // Act + using var ms = new MemoryStream(); + + reader.ReadFile("abcd.tmp", ms); + reader.ReadFile("abcd.tmp", tmpFile); + + // Assert + CollectionAssert.AreEqual(ms.ToArray(), File.ReadAllBytes(tmpFile)); + } + finally + { + File.Delete(tmpFile); + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldThrowExceptionOnMissingRead() + { + // Arrange + using var stream = new OverrideStream(); + stream.CanReadOR = false; + stream.CanSeekOR = true; + stream.CanWriteOR = true; + + // Act + var reader = new ArReader(stream); + + // Assert - ArgumentException + Assert.Fail(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldThrowExceptionOnMissingSeek() + { + // Arrange + using var stream = new OverrideStream(); + stream.CanReadOR = true; + stream.CanSeekOR = false; + stream.CanWriteOR = true; + + // Act + var reader = new ArReader(stream); + + // Assert - ArgumentException + Assert.Fail(); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldThrowExceptionOnMissingWrite() + { + // Arrange + using var stream = new OverrideStream(); + stream.CanReadOR = true; + stream.CanSeekOR = true; + stream.CanWriteOR = false; + + var reader = new ArReader(inStream); + + // Act + reader.ReadFile("abcd.tmp", stream); + + // Assert - ArgumentException + Assert.Fail(); + } + + [TestMethod] + [ExpectedException(typeof(FormatException))] + public void ShouldThrowExceptionOnInvalidArchive() + { + // Arrange + inStream.Seek(8, SeekOrigin.Begin); + + // Act + _ = new ArReader(inStream); + + // Assert - FormatException + Assert.Fail(); + } + + [TestMethod] + [ExpectedException(typeof(FormatException))] + public void ShouldThrowExceptionOnInvalidMagic() + { + // Arrange + inStream.Seek(0, SeekOrigin.End); + inStream.Write(Encoding.ASCII.GetBytes($"{"foo.bar",-16}{"123456789",-12}123 456 100644 {"0",-10}´\n")); + inStream.Seek(0, SeekOrigin.Begin); + + // Act + _ = new ArReader(inStream); + + // Assert - FormatException + Assert.Fail(); + } + + [TestMethod] + public void ShouldWriteNothingToStreamForMissingFile() + { + // Arrange + var reader = new ArReader(inStream); + + // Act + using var ms = new MemoryStream(); + + reader.ReadFile("foo.bar", ms); + ms.Seek(0, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(0, ms.Length); + } + + [TestMethod] + public void ShouldWriteNothingToDiskForMissingFile() + { + // Arrange + string tmpFile = Path.GetTempFileName(); + var reader = new ArReader(inStream); + + try + { + // Act + reader.ReadFile("foo.bar", tmpFile); + + // Assert + Assert.AreEqual(0, new FileInfo(tmpFile).Length); + } + finally + { + File.Delete(tmpFile); + } + } + + private class OverrideStream : MemoryStream + { + public override bool CanWrite => CanWriteOR; + + public bool CanWriteOR { get; set; } + + public override bool CanSeek => CanSeekOR; + + public bool CanSeekOR { get; set; } + + public override bool CanRead => CanReadOR; + + public bool CanReadOR { get; set; } + } + } +} diff --git a/UnitTests/Common/Packing/Ar/ArWriterTests.cs b/UnitTests/Common/Packing/Ar/ArWriterTests.cs new file mode 100644 index 0000000..f872687 --- /dev/null +++ b/UnitTests/Common/Packing/Ar/ArWriterTests.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using AMWD.Common.Packing.Ar; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Common.Packing.Ar +{ + [TestClass] + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public class ArWriterTests + { + private readonly DateTime fixedDateTime = new(2023, 03, 01, 10, 20, 30, 0, DateTimeKind.Utc); + + private readonly Dictionary files = new(); + + private Stream outStream; + + [TestInitialize] + public void Initialize() + { + files.Clear(); + for (int i = 0; i < 3; i++) + { + var (filePath, content) = GenerateTestFile(); + files.Add(filePath, content); + } + + outStream = new MemoryStream(); + } + + [TestCleanup] + public void Cleanup() + { + foreach (var kvp in files) + File.Delete(kvp.Key); + + files.Clear(); + + outStream.Dispose(); + outStream = null; + } + + [TestMethod] + public void ShouldInitializeArchive() + { + // Arrange + byte[] initBytes = new byte[8]; + + // Act + _ = new ArWriter(outStream); + + outStream.Seek(0, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(initBytes.Length, outStream.Length); + + outStream.Read(initBytes, 0, initBytes.Length); + CollectionAssert.AreEqual(Encoding.ASCII.GetBytes("!\n"), initBytes); + } + + [TestMethod] + public void ShouldWriteOneFile() + { + // Arrange + var firstFileKvp = files.First(); + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(firstFileKvp.Key, 123, 456); + + outStream.Seek(8, SeekOrigin.Begin); // set behind init bytes + + // Assert + Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length); + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 456 100644 14 `\n", header); + Assert.AreEqual(firstFileKvp.Value, content); + } + + [TestMethod] + public void ShouldWriteMultipleFiles() + { + // Arrange + var writer = new ArWriter(outStream); + + // Act + foreach (var kvp in files) + writer.WriteFile(kvp.Key, 123, 456); + + outStream.Seek(8, SeekOrigin.Begin); + + // Assert + Assert.AreEqual((8 + 3 * 60 + 3 * 14), outStream.Length); + + foreach (var kvp in files) + { + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(kvp.Key),-16}1677666030 123 456 100644 14 `\n", header); + Assert.AreEqual(kvp.Value, content); + } + } + + [TestMethod] + public void ShouldPadToEven() + { + // Arrange + var (filePath, fileContent) = GenerateTestFile(13); + + try + { + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(filePath, 123, 456); + + outStream.Seek(8, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length); + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(filePath),-16}1677666030 123 456 100644 13 `\n", header); + Assert.AreEqual(fileContent + "\n", content); + } + finally + { + File.Delete(filePath); + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldFailOnFileNameTooLong() + { + // Arrange + var (filePath, _) = GenerateTestFile(); + try + { + string path = Path.GetDirectoryName(filePath); + string fileName = Path.GetFileName(filePath); + fileName = fileName.PadLeft(20, 'a'); + + File.Move(filePath, Path.Combine(path, fileName)); + filePath = Path.Combine(path, fileName); + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(filePath, 123, 456); + + // Assert - Exception + Assert.Fail(); + } + finally + { + File.Delete(filePath); + } + } + + [TestMethod] + public void ShouldWriteEmptyOnNegativeUserId() + { + // Arrange + var firstFileKvp = files.First(); + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(firstFileKvp.Key, -123, 456); + + outStream.Seek(8, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length); + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 456 100644 14 `\n", header); + Assert.AreEqual(firstFileKvp.Value, content); + } + + [TestMethod] + public void ShouldWriteEmptyOnNegativeGroupId() + { + // Arrange + var firstFileKvp = files.First(); + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(firstFileKvp.Key, 123, -456); + + outStream.Seek(8, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length); + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 100644 14 `\n", header); + Assert.AreEqual(firstFileKvp.Value, content); + } + + [TestMethod] + public void ShouldWriteEmptyOnNegativeMode() + { + // Arrange + var firstFileKvp = files.First(); + byte[] headerBytes = new byte[60]; + byte[] contentBytes = new byte[14]; + + var writer = new ArWriter(outStream); + + // Act + writer.WriteFile(firstFileKvp.Key, 123, 456, -1); + + outStream.Seek(8, SeekOrigin.Begin); + + // Assert + Assert.AreEqual(8 + headerBytes.Length + contentBytes.Length, outStream.Length); + + outStream.Read(headerBytes, 0, headerBytes.Length); + outStream.Read(contentBytes, 0, contentBytes.Length); + + string header = Encoding.ASCII.GetString(headerBytes); + string content = Encoding.UTF8.GetString(contentBytes); + + Assert.AreEqual($"{Path.GetFileName(firstFileKvp.Key),-16}1677666030 123 456 14 `\n", header); + Assert.AreEqual(firstFileKvp.Value, content); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ShouldFailOnNonWritableStream() + { + // Arrange + using var testStream = new NonWriteStream(); + + // Act + _ = new ArWriter(testStream); + + // Assert - ArgumentException + Assert.Fail(); + } + + private (string filePath, string content) GenerateTestFile(int length = 14) + { + string filePath = Path.GetTempFileName(); + string text = CryptographyHelper.GetRandomString(length); + + File.WriteAllText(filePath, text); + File.SetLastWriteTimeUtc(filePath, fixedDateTime); + return (filePath, text); + } + + private class NonWriteStream : MemoryStream + { + public NonWriteStream() + : base() + { } + + public override bool CanWrite => false; + } + } +} diff --git a/UnitTests/Common/Packing/Tar/TarReaderTests.cs b/UnitTests/Common/Packing/Tar/TarReaderTests.cs new file mode 100644 index 0000000..3e5abc7 --- /dev/null +++ b/UnitTests/Common/Packing/Tar/TarReaderTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UnitTests.Common.Packing.Tar +{ + internal class TarReaderTests + { + } +} diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 6c1489a..aed5779 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -2,6 +2,7 @@ net8.0 + 12.0 false true false @@ -13,8 +14,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + +