1
0

Merge branch 'packing'

This commit is contained in:
2024-01-14 13:10:45 +01:00
61 changed files with 2741 additions and 309 deletions

View File

@@ -28,7 +28,9 @@ build-debug:
- mv ./AMWD.Common.AspNetCore/bin/Debug/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Debug/*.nupkg ./artifacts/
- mv ./AMWD.Common.AspNetCore/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Debug/*.snupkg ./artifacts/
- mv ./AMWD.Common.EntityFrameworkCore/bin/Debug/*.nupkg ./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/*.nupkg ./artifacts/
- mv ./AMWD.Common.Test/bin/Debug/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Debug/*.snupkg ./artifacts/
artifacts: artifacts:
@@ -72,7 +74,9 @@ build-release:
- mv ./AMWD.Common.AspNetCore/bin/Release/*.nupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Release/*.nupkg ./artifacts/
- mv ./AMWD.Common.AspNetCore/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.AspNetCore/bin/Release/*.snupkg ./artifacts/
- mv ./AMWD.Common.EntityFrameworkCore/bin/Release/*.nupkg ./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/*.nupkg ./artifacts/
- mv ./AMWD.Common.Test/bin/Release/*.snupkg ./artifacts/ - mv ./AMWD.Common.Test/bin/Release/*.snupkg ./artifacts/
artifacts: artifacts:
@@ -136,6 +140,19 @@ deploy-entityframework:
- if: $CI_COMMIT_TAG =~ /^efc\/v[0-9.]+/ - if: $CI_COMMIT_TAG =~ /^efc\/v[0-9.]+/
script: script:
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.am-wd.de/v3/index.json --skip-duplicate artifacts/AMWD.Common.EntityFrameworkCore.*.nupkg - 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: deploy-test:
stage: deploy stage: deploy

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<NrtTagMatch>asp/v[0-9]*</NrtTagMatch> <NrtTagMatch>asp/v[0-9]*</NrtTagMatch>
<AssemblyName>AMWD.Common.AspNetCore</AssemblyName> <AssemblyName>AMWD.Common.AspNetCore</AssemblyName>

View File

@@ -12,12 +12,12 @@ namespace Microsoft.AspNetCore.Http
public static class HttpContextExtensions public static class HttpContextExtensions
{ {
// Search these additional headers for a remote client ip address. // 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 "Cf-Connecting-Ip", // set by Cloudflare
"X-Real-IP", // wide-spread alternative to X-Forwarded-For "X-Real-IP", // wide-spread alternative to X-Forwarded-For
"X-Forwarded-For", // commonly used on all known proxies "X-Forwarded-For", // commonly used on all known proxies
}; ];
/// <summary> /// <summary>
/// Retrieves the antiforgery token. /// Retrieves the antiforgery token.

View File

@@ -9,26 +9,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary> /// <summary>
/// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their <see href="https://github.com/dotnet/aspnetcore/issues/6566">issue</see> with other cultures than en-US. /// Custom floating point ModelBinder as the team of Microsoft is not capable of fixing their <see href="https://github.com/dotnet/aspnetcore/issues/6566">issue</see> with other cultures than en-US.
/// </summary> /// </summary>
/// <remarks>
/// Initializes a new instance of <see cref="InvariantFloatingPointModelBinder"/>.
/// </remarks>
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class InvariantFloatingPointModelBinder : IModelBinder public class InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
: IModelBinder
{ {
private readonly NumberStyles _supportedNumberStyles; private readonly NumberStyles _supportedNumberStyles = supportedStyles;
private readonly ILogger _logger; private readonly ILogger _logger = loggerFactory?.CreateLogger<InvariantFloatingPointModelBinder>();
private readonly CultureInfo _cultureInfo; private readonly CultureInfo _cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
/// <summary>
/// Initializes a new instance of <see cref="InvariantFloatingPointModelBinder"/>.
/// </summary>
/// <param name="supportedStyles">The <see cref="NumberStyles"/>.</param>
/// <param name="cultureInfo">The <see cref="CultureInfo"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public InvariantFloatingPointModelBinder(NumberStyles supportedStyles, CultureInfo cultureInfo, ILoggerFactory loggerFactory)
{
_cultureInfo = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo));
_supportedNumberStyles = supportedStyles;
_logger = loggerFactory?.CreateLogger<InvariantFloatingPointModelBinder>();
}
/// <inheritdoc /> /// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)

View File

@@ -13,49 +13,39 @@ using Microsoft.Extensions.Options;
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
{ {
#if NET8_0_OR_GREATER
/// <summary> /// <summary>
/// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication. /// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication.
/// </summary> /// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
/// </remarks>
/// <param name="options" > The monitor for the options instance.</param>
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="validator">An basic autentication validator implementation.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> public class BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
#else
/// <summary>
/// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
/// </remarks>
/// <param name="options" > The monitor for the options instance.</param>
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="clock">The <see cref="ISystemClock"/>.</param>
/// <param name="validator">An basic autentication validator implementation.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)
#endif
{ {
private readonly ILogger _logger; private readonly ILogger _logger = logger.CreateLogger<BasicAuthenticationHandler>();
private readonly IBasicAuthenticationValidator _validator; private readonly IBasicAuthenticationValidator _validator = validator;
#if NET8_0_OR_GREATER
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
/// </summary>
/// <param name="options">The monitor for the options instance.</param>
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="validator">An basic autentication validator implementation.</param>
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IBasicAuthenticationValidator validator)
: base(options, logger, encoder)
{
_logger = logger.CreateLogger<BasicAuthenticationHandler>();
_validator = validator;
}
#endif
#if NET6_0
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthenticationHandler"/> class.
/// </summary>
/// <param name="options" > The monitor for the options instance.</param>
/// <param name="logger">The <see cref="ILoggerFactory"/>.</param>
/// <param name="encoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="clock">The <see cref="ISystemClock"/>.</param>
/// <param name="validator">An basic autentication validator implementation.</param>
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBasicAuthenticationValidator validator)
: base(options, logger, encoder, clock)
{
_logger = logger.CreateLogger<BasicAuthenticationHandler>();
_validator = validator;
}
#endif
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() protected override async Task<AuthenticateResult> HandleAuthenticateAsync()

View File

@@ -11,21 +11,15 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
/// <summary> /// <summary>
/// Implements a basic authentication. /// Implements a basic authentication.
/// </summary> /// </summary>
public class BasicAuthenticationMiddleware /// <remarks>
/// Initializes a new instance of the <see cref="BasicAuthenticationMiddleware"/> class.
/// </remarks>
/// <param name="next">The following delegate in the process chain.</param>
/// <param name="validator">A basic authentication validator.</param>
public class BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator)
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next = next;
private readonly IBasicAuthenticationValidator _validator; private readonly IBasicAuthenticationValidator _validator = validator;
/// <summary>
/// Initializes a new instance of the <see cref="BasicAuthenticationMiddleware"/> class.
/// </summary>
/// <param name="next">The following delegate in the process chain.</param>
/// <param name="validator">A basic authentication validator.</param>
public BasicAuthenticationMiddleware(RequestDelegate next, IBasicAuthenticationValidator validator)
{
_next = next;
_validator = validator;
}
/// <summary> /// <summary>
/// The delegate invokation. /// The delegate invokation.
@@ -35,15 +29,27 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
/// <returns>An awaitable task.</returns> /// <returns>An awaitable task.</returns>
public async Task InvokeAsync(HttpContext httpContext) 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")) if (!httpContext.Request.Headers.ContainsKey("Authorization"))
{ {
SetAuthenticateRequest(httpContext, _validator.Realm); SetAuthenticateRequest(httpContext, _validator.Realm);
return; return;
} }
#endif
try try
{ {
#if NET8_0_OR_GREATER
var authHeader = AuthenticationHeaderValue.Parse(authHeaderValue);
#else
var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]); var authHeader = AuthenticationHeaderValue.Parse(httpContext.Request.Headers["Authorization"]);
#endif
byte[] decoded = Convert.FromBase64String(authHeader.Parameter); byte[] decoded = Convert.FromBase64String(authHeader.Parameter);
string plain = Encoding.UTF8.GetString(decoded); string plain = Encoding.UTF8.GetString(decoded);
@@ -70,9 +76,9 @@ namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
private static void SetAuthenticateRequest(HttpContext httpContext, string realm) private static void SetAuthenticateRequest(HttpContext httpContext, string realm)
{ {
httpContext.Response.Headers["WWW-Authenticate"] = "Basic"; httpContext.Response.Headers.WWWAuthenticate = "Basic";
if (!string.IsNullOrWhiteSpace(realm)) 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; httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
} }

View File

@@ -8,23 +8,16 @@ namespace AMWD.Common.AspNetCore.Security.PathProtection
/// <summary> /// <summary>
/// Implements a check to provide protected paths. /// Implements a check to provide protected paths.
/// </summary> /// </summary>
public class ProtectedPathMiddleware /// <remarks>
/// Initializes a new instance of the <see cref="ProtectedPathExtensions"/> class.
/// </remarks>
/// <param name="next">The following delegate in the process chain.</param>
/// <param name="options">The options to configure the middleware.</param>
public class ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options)
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next = next;
private readonly PathString _path; private readonly PathString _path = options.Path;
private readonly string _policyName; private readonly string _policyName = options.PolicyName;
/// <summary>
/// Initializes a new instance of the <see cref="ProtectedPathExtensions"/> class.
/// </summary>
/// <param name="next">The following delegate in the process chain.</param>
/// <param name="options">The options to configure the middleware.</param>
public ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options)
{
_next = next;
_path = options.Path;
_policyName = options.PolicyName;
}
/// <summary> /// <summary>
/// The delegate invokation. /// The delegate invokation.

View File

@@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
if (items.Any()) if (items.Any())
{ {
string classes = string.Join(" ", items.ToArray()); string classes = string.Join(" ", [.. items]);
output.Attributes.Add("class", classes); output.Attributes.Add("class", classes);
} }
} }

View File

@@ -13,24 +13,19 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <summary> /// <summary>
/// A tag helper to dynamically create integrity checks for linked sources. /// A tag helper to dynamically create integrity checks for linked sources.
/// </summary> /// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="IntegrityHashTagHelper"/> class.
/// </remarks>
/// <param name="env">The web host environment.</param>
/// <param name="configuration">The application configuration.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[HtmlTargetElement("link")] [HtmlTargetElement("link")]
[HtmlTargetElement("script")] [HtmlTargetElement("script")]
public class IntegrityHashTagHelper : TagHelper public class IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration)
: TagHelper
{ {
private readonly IWebHostEnvironment _env; private readonly IWebHostEnvironment _env = env;
private readonly string _hostUrl; private readonly string _hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/");
/// <summary>
/// Initializes a new instance of the <see cref="IntegrityHashTagHelper"/> class.
/// </summary>
/// <param name="env">The web host environment.</param>
/// <param name="configuration">The application configuration.</param>
public IntegrityHashTagHelper(IWebHostEnvironment env, IConfiguration configuration)
{
_env = env;
_hostUrl = configuration.GetValue("ASPNETCORE_APPL_URL", "http://localhost/");
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the integrity should be calculated. /// Gets or sets a value indicating whether the integrity should be calculated.
@@ -118,7 +113,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
} }
string type; string type;
byte[] hashBytes = Array.Empty<byte>(); byte[] hashBytes = [];
switch (IntegrityStrength) switch (IntegrityStrength)
{ {
case 512: case 512:

View File

@@ -9,17 +9,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
/// <summary> /// <summary>
/// Adds additional behavior to the modelbinding for numeric properties. /// Adds additional behavior to the modelbinding for numeric properties.
/// </summary> /// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
/// </remarks>
/// <param name="generator">The HTML generator.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[HtmlTargetElement("input", Attributes = "asp-for")] [HtmlTargetElement("input", Attributes = "asp-for")]
public class NumberInputTagHelper : InputTagHelper public class NumberInputTagHelper(IHtmlGenerator generator)
: InputTagHelper(generator)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="NumberInputTagHelper"/> class.
/// </summary>
/// <param name="generator">The HTML generator.</param>
public NumberInputTagHelper(IHtmlGenerator generator)
: base(generator)
{ }
/// <inheritdoc /> /// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output) public override void Process(TagHelperContext context, TagHelperOutput output)

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<NrtTagMatch>efc/v[0-9]*</NrtTagMatch> <NrtTagMatch>efc/v[0-9]*</NrtTagMatch>
<AssemblyName>AMWD.Common.EntityFrameworkCore</AssemblyName> <AssemblyName>AMWD.Common.EntityFrameworkCore</AssemblyName>
@@ -22,17 +22,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.25" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.26" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.25" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.26" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,4 @@
using System.Runtime.Serialization; namespace System
namespace System
{ {
/// <summary> /// <summary>
/// A DatabaseProvider specific exception. /// A DatabaseProvider specific exception.
@@ -33,16 +31,16 @@ namespace System
: base(message, innerException) : base(message, innerException)
{ } { }
#if NET6_0 #if !NET8_0_OR_GREATER
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DatabaseProviderException"/> class with serialized data. /// Initializes a new instance of the <see cref="DatabaseProviderException"/> class with serialized data.
/// </summary> /// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param> /// <param name="info">The <see cref="Runtime.Serialization.SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param> /// <param name="context">The <see cref="Runtime.Serialization.StreamingContext"/> that contains contextual information about the source or destination.</param>
/// <exception cref="ArgumentNullException">The info parameter is null.</exception> /// <exception cref="ArgumentNullException">The info parameter is null.</exception>
/// <exception cref="SerializationException">The class name is null or <see cref="Exception.HResult"/> is zero (0).</exception> /// <exception cref="Runtime.Serialization.SerializationException">The class name is null or <see cref="Exception.HResult"/> is zero (0).</exception>
protected DatabaseProviderException(SerializationInfo info, StreamingContext context) protected DatabaseProviderException(Runtime.Serialization.SerializationInfo info, Runtime.Serialization.StreamingContext context)
: base(info, context) : base(info, context)
{ } { }

View File

@@ -15,7 +15,11 @@ namespace Microsoft.EntityFrameworkCore
/// <summary> /// <summary>
/// Extensions for the <see cref="DatabaseFacade"/>. /// Extensions for the <see cref="DatabaseFacade"/>.
/// </summary> /// </summary>
#if NET8_0_OR_GREATER
public static partial class DatabaseFacadeExtensions
#else
public static class DatabaseFacadeExtensions public static class DatabaseFacadeExtensions
#endif
{ {
/// <summary> /// <summary>
/// Applies migration files to the database. /// Applies migration files to the database.
@@ -23,7 +27,7 @@ namespace Microsoft.EntityFrameworkCore
/// <param name="database">The database connection.</param> /// <param name="database">The database connection.</param>
/// <param name="optionsAction">An action to set additional options.</param> /// <param name="optionsAction">An action to set additional options.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>true on success, otherwise false or an exception is thrown.</returns> /// <returns><see langword="true"/> on success, otherwise false or an exception is thrown.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208")]
public static async Task<bool> ApplyMigrationsAsync(this DatabaseFacade database, Action<DatabaseMigrationOptions> optionsAction, CancellationToken cancellationToken = default) public static async Task<bool> ApplyMigrationsAsync(this DatabaseFacade database, Action<DatabaseMigrationOptions> optionsAction, CancellationToken cancellationToken = default)
{ {
@@ -211,20 +215,20 @@ END;"
if (options.SourceAssembly == null) if (options.SourceAssembly == null)
{ {
availableMigrationFiles = Directory.GetFiles(options.Path) availableMigrationFiles = Directory.GetFiles(options.Path)
.Where(f => f.ToLower().StartsWith(options.Path.ToLower())) .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase))
.Where(f => f.ToLower().EndsWith(".sql")) .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
} }
else else
{ {
availableMigrationFiles = options.SourceAssembly availableMigrationFiles = options.SourceAssembly
.GetManifestResourceNames() .GetManifestResourceNames()
.Where(f => f.ToLower().StartsWith(options.Path.ToLower())) .Where(f => f.StartsWith(options.Path, StringComparison.OrdinalIgnoreCase))
.Where(f => f.ToLower().EndsWith(".sql")) .Where(f => f.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
} }
if (!availableMigrationFiles.Any()) if (availableMigrationFiles.Count == 0)
return true; return true;
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
@@ -270,7 +274,11 @@ END;"
{ {
using var stream = options.SourceAssembly.GetManifestResourceStream(migrationFile); using var stream = options.SourceAssembly.GetManifestResourceStream(migrationFile);
using var sr = new StreamReader(stream); 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); sqlScript = await sr.ReadToEndAsync().ConfigureAwait(false);
#endif
} }
if (string.IsNullOrWhiteSpace(sqlScript)) if (string.IsNullOrWhiteSpace(sqlScript))
@@ -316,7 +324,11 @@ END;"
{ {
int affectedRows = 0; int affectedRows = 0;
// Split script by a single slash in a line // 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"); string[] parts = Regex.Split(text, @"\r?\n[ \t]*/[ \t]*\r?\n");
#endif
foreach (string part in parts) foreach (string part in parts)
{ {
// Make writable copy // Make writable copy
@@ -325,7 +337,11 @@ END;"
// Remove the trailing semicolon from commands where they're not supported // Remove the trailing semicolon from commands where they're not supported
// (Oracle doesn't like semicolons. To keep the semicolon, it must be directly // (Oracle doesn't like semicolons. To keep the semicolon, it must be directly
// preceeded by "end".) // preceeded by "end".)
pt = Regex.Replace(pt.TrimEnd(), @"(?<!end);$", "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); #if NET8_0_OR_GREATER
pt = FindEndCommand().Replace(pt.TrimEnd(), "");
#else
pt = Regex.Replace(pt, @"(?<!end);$", "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
#endif
// Execute all non-empty parts as individual commands // Execute all non-empty parts as individual commands
if (!string.IsNullOrWhiteSpace(pt)) if (!string.IsNullOrWhiteSpace(pt))
@@ -352,5 +368,13 @@ END;"
SQLServer = 5, SQLServer = 5,
InMemory = 6, InMemory = 6,
} }
#if NET8_0_OR_GREATER
[GeneratedRegex(@"\r?\n[ \t]*/[ \t]*\r?\n")]
private static partial Regex FindSingleSlashInLine();
[GeneratedRegex(@"(?<!end);$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex FindEndCommand();
#endif
} }
} }

View File

@@ -33,11 +33,15 @@ namespace Microsoft.EntityFrameworkCore
/// <returns>The <see cref="DbContextOptionsBuilder"/> with applied settings.</returns> /// <returns>The <see cref="DbContextOptionsBuilder"/> with applied settings.</returns>
public static DbContextOptionsBuilder UseDatabaseProvider(this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, Action<DatabaseProviderOptions> optionsAction = null) public static DbContextOptionsBuilder UseDatabaseProvider(this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, Action<DatabaseProviderOptions> optionsAction = null)
{ {
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(optionsBuilder);
ArgumentNullException.ThrowIfNull(configuration);
#else
if (optionsBuilder == null) if (optionsBuilder == null)
throw new ArgumentNullException(nameof(optionsBuilder)); throw new ArgumentNullException(nameof(optionsBuilder));
if (configuration == null) if (configuration == null)
throw new ArgumentNullException(nameof(configuration)); throw new ArgumentNullException(nameof(configuration));
#endif
var options = new DatabaseProviderOptions(); var options = new DatabaseProviderOptions();
optionsAction?.Invoke(options); optionsAction?.Invoke(options);

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<NrtTagMatch>msgpack/v[0-9]*</NrtTagMatch>
<AssemblyName>AMWD.Common.MessagePack</AssemblyName>
<RootNamespace>AMWD.Common.MessagePack</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>AMWD.Common.MessagePack</PackageId>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Product>AM.WD Common Library for MessagePack</Product>
</PropertyGroup>
<ItemGroup>
<None Include="../icon.png" Pack="true" PackagePath="/" />
<None Include="../README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.140" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using AMWD.Common.Utilities; using AMWD.Common.MessagePack.Utilities;
namespace MessagePack.Formatters namespace MessagePack.Formatters
{ {
@@ -66,7 +66,7 @@ namespace MessagePack.Formatters
bytes.AddRange(buffer); bytes.AddRange(buffer);
} }
options.Resolver.GetFormatterWithVerify<byte[]>().Serialize(ref writer, bytes.ToArray(), options); options.Resolver.GetFormatterWithVerify<byte[]>().Serialize(ref writer, [.. bytes], options);
} }
} }
} }

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using AMWD.Common.Utilities; using AMWD.Common.MessagePack.Utilities;
namespace MessagePack.Formatters namespace MessagePack.Formatters
{ {
@@ -61,7 +61,7 @@ namespace MessagePack.Formatters
bytes.AddRange(buffer); bytes.AddRange(buffer);
} }
options.Resolver.GetFormatterWithVerify<byte[]>().Serialize(ref writer, bytes.ToArray(), options); options.Resolver.GetFormatterWithVerify<byte[]>().Serialize(ref writer, [.. bytes], options);
} }
} }
} }

View File

@@ -1,12 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AMWD.Common.Utilities; using AMWD.Common.MessagePack.Utilities;
using MessagePack; #if NET8_0_OR_GREATER
using MessagePack.Formatters; using IPNetwork = System.Net.IPNetwork;
#else
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
#endif
namespace AMWD.Common.Formatters namespace MessagePack.Formatters
{ {
/// <summary> /// <summary>
/// Serialization of an <see cref="IPNetwork"/> array to and from <see cref="MessagePack"/>. /// Serialization of an <see cref="IPNetwork"/> array to and from <see cref="MessagePack"/>.

View File

@@ -1,7 +1,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
#if NET8_0_OR_GREATER
using IPNetwork = System.Net.IPNetwork;
#else
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
#endif
namespace MessagePack.Formatters namespace MessagePack.Formatters
{ {
@@ -15,7 +20,7 @@ namespace MessagePack.Formatters
public IPNetwork Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) public IPNetwork Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{ {
if (reader.IsNil) if (reader.IsNil)
return null; return default;
byte[] bytes = options.Resolver.GetFormatterWithVerify<byte[]>().Deserialize(ref reader, options); byte[] bytes = options.Resolver.GetFormatterWithVerify<byte[]>().Deserialize(ref reader, options);
return DeserializeInternal(bytes); return DeserializeInternal(bytes);
@@ -24,7 +29,7 @@ namespace MessagePack.Formatters
/// <inheritdoc/> /// <inheritdoc/>
public void Serialize(ref MessagePackWriter writer, IPNetwork value, MessagePackSerializerOptions options) public void Serialize(ref MessagePackWriter writer, IPNetwork value, MessagePackSerializerOptions options)
{ {
if (value == null) if (value == default)
{ {
writer.WriteNil(); writer.WriteNil();
return; 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. // IP network prefix has a maximum of 128 bit - therefore the length can be covered with a byte.
byte prefixLength = (byte)network.PrefixLength; byte prefixLength = (byte)network.PrefixLength;
#if NET8_0_OR_GREATER
byte[] prefixBytes = network.BaseAddress.GetAddressBytes();
#else
byte[] prefixBytes = network.Prefix.GetAddressBytes(); byte[] prefixBytes = network.Prefix.GetAddressBytes();
#endif
byte[] bytes = new byte[prefixBytes.Length + 1]; byte[] bytes = new byte[prefixBytes.Length + 1];
bytes[0] = prefixLength; bytes[0] = prefixLength;

View File

@@ -1,8 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; 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 Microsoft.AspNetCore.HttpOverrides;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
#endif
namespace MessagePack.Formatters namespace MessagePack.Formatters
{ {

View File

@@ -0,0 +1,17 @@
using System;
namespace AMWD.Common.MessagePack.Utilities
{
/// <summary>
/// Provides some network utils.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static class NetworkHelper
{
public static void SwapBigEndian(byte[] array)
{
if (BitConverter.IsLittleEndian)
Array.Reverse(array);
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<NrtTagMatch>test/v[0-9]*</NrtTagMatch> <NrtTagMatch>test/v[0-9]*</NrtTagMatch>
<AssemblyName>AMWD.Common.Test</AssemblyName> <AssemblyName>AMWD.Common.Test</AssemblyName>
@@ -22,7 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Moq" Version="4.20.69" /> <PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<AssemblyName>AMWD.Common</AssemblyName> <AssemblyName>AMWD.Common</AssemblyName>
<RootNamespace>AMWD.Common</RootNamespace> <RootNamespace>AMWD.Common</RootNamespace>
@@ -21,10 +21,23 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.129" />
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Unclassified.DeepConvert" Version="1.4.0" /> <PackageReference Include="Unclassified.DeepConvert" Version="1.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

@@ -14,7 +14,7 @@ namespace AMWD.Common.Cli
private string[] _args; private string[] _args;
private List<Argument> _parsedArguments; private List<Argument> _parsedArguments;
private readonly List<Option> _options = new(); private readonly List<Option> _options = [];
#endregion Private data #endregion Private data
@@ -129,7 +129,7 @@ namespace AMWD.Common.Cli
{ {
args.Add(currentArg.ToString()); args.Add(currentArg.ToString());
} }
return args.ToArray(); return [.. args];
} }
/// <summary> /// <summary>
@@ -199,7 +199,7 @@ namespace AMWD.Common.Cli
} }
// Clear/reset data // Clear/reset data
_parsedArguments = new(); _parsedArguments = [];
foreach (var option in _options) foreach (var option in _options)
{ {
option.IsSet = false; option.IsSet = false;
@@ -223,7 +223,7 @@ namespace AMWD.Common.Cli
string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1); string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1);
// Split option value if separated with : or = instead of whitespace // Split option value if separated with : or = instead of whitespace
int separatorIndex = optName.IndexOfAny(new[] { ':', '=' }); int separatorIndex = optName.IndexOfAny([':', '=']);
string optValue = null; string optValue = null;
if (separatorIndex != -1) if (separatorIndex != -1)
{ {
@@ -288,7 +288,7 @@ namespace AMWD.Common.Cli
} }
else else
{ {
_parsedArguments.Add(new Argument(null, new[] { arg })); _parsedArguments.Add(new Argument(null, [arg]));
} }
} }
@@ -315,7 +315,7 @@ namespace AMWD.Common.Cli
if (_parsedArguments == null) if (_parsedArguments == null)
Parse(); Parse();
return _parsedArguments.ToArray(); return [.. _parsedArguments];
} }
} }

View File

@@ -8,21 +8,16 @@ namespace AMWD.Common.Cli
/// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items. /// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
internal class EnumerableWalker<T> : IEnumerable<T> /// <remarks>
where T : class /// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
/// </remarks>
/// <param name="array">The array to walk though.</param>
internal class EnumerableWalker<T>(IEnumerable<T> array)
: IEnumerable<T> where T : class
{ {
private readonly IEnumerable<T> _array; private readonly IEnumerable<T> _array = array ?? throw new ArgumentNullException(nameof(array));
private IEnumerator<T> _enumerator; private IEnumerator<T> _enumerator;
/// <summary>
/// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
/// </summary>
/// <param name="array">The array to walk though.</param>
public EnumerableWalker(IEnumerable<T> array)
{
_array = array ?? throw new ArgumentNullException(nameof(array));
}
IEnumerator<T> IEnumerable<T>.GetEnumerator() IEnumerator<T> IEnumerable<T>.GetEnumerator()
{ {
_enumerator = _array.GetEnumerator(); _enumerator = _array.GetEnumerator();

View File

@@ -16,7 +16,7 @@ namespace AMWD.Common.Cli
/// <param name="parameterCount">The number of additional parameters for this option.</param> /// <param name="parameterCount">The number of additional parameters for this option.</param>
internal Option(string name, int parameterCount) internal Option(string name, int parameterCount)
{ {
Names = new List<string>() { name }; Names = [name];
ParameterCount = parameterCount; ParameterCount = parameterCount;
} }

View File

@@ -13,18 +13,16 @@ namespace Newtonsoft.Json
/// <summary> /// <summary>
/// List of known types to use this converver. /// List of known types to use this converver.
/// </summary> /// </summary>
public static readonly Type[] KnownTypes = new[] public static readonly Type[] KnownTypes =
{ [
typeof(byte[]), typeof(byte[]),
typeof(List<byte>), typeof(List<byte>),
typeof(IEnumerable<byte>) typeof(IEnumerable<byte>)
}; ];
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ => KnownTypes.Contains(objectType);
return KnownTypes.Contains(objectType);
}
/// <inheritdoc/> /// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

View File

@@ -14,19 +14,17 @@ namespace Newtonsoft.Json
/// <summary> /// <summary>
/// List of known types to use this converver. /// List of known types to use this converver.
/// </summary> /// </summary>
public static readonly Type[] KnownTypes = new[] public static readonly Type[] KnownTypes =
{ [
typeof(IPAddress), typeof(IPAddress),
typeof(IPAddress[]), typeof(IPAddress[]),
typeof(List<IPAddress>), typeof(List<IPAddress>),
typeof(IEnumerable<IPAddress>) typeof(IEnumerable<IPAddress>)
}; ];
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ => KnownTypes.Contains(objectType);
return KnownTypes.Contains(objectType);
}
/// <inheritdoc/> /// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

View File

@@ -2,7 +2,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
#if NET8_0_OR_GREATER
using IPNetwork = System.Net.IPNetwork;
#else
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
#endif
namespace Newtonsoft.Json namespace Newtonsoft.Json
{ {
@@ -10,24 +15,22 @@ namespace Newtonsoft.Json
/// Converts an <see cref="IPNetwork"/> from and to JSON. /// Converts an <see cref="IPNetwork"/> from and to JSON.
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class IPNetworkConverter : JsonConverter public class IpNetworkConverter : JsonConverter
{ {
/// <summary> /// <summary>
/// List of known types to use this converver. /// List of known types to use this converver.
/// </summary> /// </summary>
public static readonly Type[] KnownTypes = new[] public static readonly Type[] KnownTypes =
{ [
typeof(IPNetwork), typeof(IPNetwork),
typeof(IPNetwork[]), typeof(IPNetwork[]),
typeof(List<IPNetwork>), typeof(List<IPNetwork>),
typeof(IEnumerable<IPNetwork>) typeof(IEnumerable<IPNetwork>)
}; ];
/// <inheritdoc/> /// <inheritdoc/>
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ => KnownTypes.Contains(objectType);
return KnownTypes.Contains(objectType);
}
/// <inheritdoc/> /// <inheritdoc/>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
@@ -40,7 +43,7 @@ namespace Newtonsoft.Json
if (typeof(IPNetwork) == objectType) if (typeof(IPNetwork) == objectType)
return Parse(str); return Parse(str);
var networks = str.Split(';').Select(s => Parse(s)); var networks = str.Split(';').Select(Parse);
if (typeof(IPNetwork[]) == objectType) if (typeof(IPNetwork[]) == objectType)
return networks.ToArray(); return networks.ToArray();
@@ -65,23 +68,27 @@ namespace Newtonsoft.Json
str = ToString(net); str = ToString(net);
if (value is IPNetwork[] netArray) if (value is IPNetwork[] netArray)
str = string.Join(";", netArray.Select(n => ToString(n))); str = string.Join(";", netArray.Select(ToString));
if (value is List<IPNetwork> netList) if (value is List<IPNetwork> netList)
str = string.Join(";", netList.Select(n => ToString(n))); str = string.Join(";", netList.Select(ToString));
if (value is IEnumerable<IPNetwork> netEnum) if (value is IEnumerable<IPNetwork> netEnum)
str = string.Join(";", netEnum.Select(n => ToString(n))); str = string.Join(";", netEnum.Select(ToString));
writer.WriteValue(str); 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}"; return $"{net.Prefix}/{net.PrefixLength}";
#endif
} }
private IPNetwork Parse(string str) private static IPNetwork Parse(string str)
{ {
string[] parts = str.Split('/'); string[] parts = str.Split('/');
var prefix = IPAddress.Parse(parts.First()); var prefix = IPAddress.Parse(parts.First());

View File

@@ -1,7 +1,5 @@
using System.Text; using System.Text;
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
namespace System namespace System
{ {
/// <summary> /// <summary>

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
namespace System namespace System
@@ -41,13 +40,5 @@ namespace System
/// <returns>The description or the string representation of the value.</returns> /// <returns>The description or the string representation of the value.</returns>
public static string GetDescription(this Enum value) public static string GetDescription(this Enum value)
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString(); => value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
/// <summary>
/// Returns the name from <see cref="DisplayAttribute"/>.
/// </summary>
/// <param name="value">The enum value.</param>
/// <returns>The display name or the string representation of the value.</returns>
public static string GetDisplayName(this Enum value)
=> value.GetAttribute<DisplayAttribute>()?.Name ?? value.ToString();
} }
} }

View File

@@ -56,16 +56,11 @@
return new DisposableReadWriteLock(rwLock, LockMode.Write); return new DisposableReadWriteLock(rwLock, LockMode.Write);
} }
private struct DisposableReadWriteLock : IDisposable private struct DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode)
: IDisposable
{ {
private readonly ReaderWriterLockSlim _rwLock; private readonly ReaderWriterLockSlim _rwLock = rwLock;
private LockMode _lockMode; private LockMode _lockMode = lockMode;
public DisposableReadWriteLock(ReaderWriterLockSlim rwLock, LockMode lockMode)
{
_rwLock = rwLock;
_lockMode = lockMode;
}
public void Dispose() public void Dispose()
{ {

View File

@@ -40,7 +40,7 @@ namespace System.IO
} }
while (ch != eol); while (ch != eol);
return encoding.GetString(bytes.ToArray()).Trim(); return encoding.GetString([.. bytes]).Trim();
} }
/// <summary> /// <summary>
@@ -73,7 +73,7 @@ namespace System.IO
} }
while (ch != eol); while (ch != eol);
return encoding.GetString(bytes.ToArray()).Trim(); return encoding.GetString([.. bytes]).Trim();
} }
} }
} }

View File

@@ -15,7 +15,12 @@ namespace System
/// <summary> /// <summary>
/// String extensions. /// String extensions.
/// </summary> /// </summary>
#if NET8_0_OR_GREATER
public static partial class StringExtensions
#else
public static class StringExtensions public static class StringExtensions
#endif
{ {
/// <summary> /// <summary>
/// Converts a hex string into a byte array. /// Converts a hex string into a byte array.
@@ -32,8 +37,13 @@ namespace System
if (str.Length % 2 == 1) if (str.Length % 2 == 1)
yield break; yield break;
#if NET8_0_OR_GREATER
if (InvalidHexCharRegex().IsMatch(str))
yield break;
#else
if (Regex.IsMatch(str, "[^0-9a-fA-F]")) if (Regex.IsMatch(str, "[^0-9a-fA-F]"))
yield break; yield break;
#endif
for (int i = 0; i < str.Length; i += 2) for (int i = 0; i < str.Length; i += 2)
yield return Convert.ToByte(str.Substring(i, 2), 16); 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 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 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; bool exists = false;
foreach (var nameserver in nameservers) 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<object>(dnsClient, new object[] { mailAddress.Host, 15, CancellationToken.None })); // 15 = MX Record var waitTask = Task.Run(async () => await resolveMethodInfo.InvokeAsync<object>(dnsClient, [mailAddress.Host, 15, CancellationToken.None])); // 15 = MX Record
waitTask.Wait(); waitTask.Wait();
object response = waitTask.Result; object response = waitTask.Result;
@@ -232,5 +242,10 @@ namespace System
/// <returns></returns> /// <returns></returns>
public static StringBuilder AppendLine(this StringBuilder sb, string value, string newLine) public static StringBuilder AppendLine(this StringBuilder sb, string value, string newLine)
=> sb.Append(value).Append(newLine); => sb.Append(value).Append(newLine);
#if NET8_0_OR_GREATER
[GeneratedRegex("[^0-9a-fA-F]")]
private static partial Regex InvalidHexCharRegex();
#endif
} }
} }

View File

@@ -6,8 +6,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
namespace AMWD.Common.Logging namespace AMWD.Common.Logging
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,51 @@
using System;
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Represents the file information saved in the archive.
/// </summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public class ArFileInfo
{
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the file size in bytes.
/// </summary>
public long FileSize { get; set; }
/// <summary>
/// Gets or sets the timestamp of the last modification.
/// </summary>
public DateTime ModifyTime { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Gets or sets the group id.
/// </summary>
public int GroupId { get; set; }
/// <summary>
/// Gets or sets the access mode in decimal (not octal!).
/// </summary>
/// <remarks>
/// To see the octal representation use <c>Convert.ToString(Mode, 8)</c>.
/// </remarks>
public int Mode { get; set; }
}
internal class ArFileInfoExtended : ArFileInfo
{
public long HeaderPosition { get; set; }
public long DataPosition { get; set; }
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Reads UNIX ar (archive) files in the GNU format.
/// </summary>
public class ArReader
{
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
private readonly Stream _inStream;
private readonly List<ArFileInfoExtended> _files = new();
private readonly long _streamStartPosition;
/// <summary>
/// Initializes a new instance of the <see cref="ArReader"/> class.
/// </summary>
/// <param name="inStream">The stream to read the archive from.</param>
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();
}
/// <summary>
/// Returns a list with all filenames of the archive.
/// </summary>
public IEnumerable<string> GetFileList()
=> _files.Select(fi => fi.FileName).ToList();
/// <summary>
/// Returns the file info of a specific file in the archive.
/// </summary>
/// <param name="fileName">The name of the specific file.</param>
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();
}
/// <summary>
/// Reads a file from the archive into a stream.
/// </summary>
/// <param name="fileName">The file name in the archive.</param>
/// <param name="outStream">The output stream.</param>
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);
}
/// <summary>
/// Reads a fie from the archive and saves it to disk.
/// </summary>
/// <param name="fileName">The file name in the archive.</param>
/// <param name="destinationPath">The destination path on disk.</param>
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 != "!<arch>\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
};
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.IO;
using System.Text;
namespace AMWD.Common.Packing.Ar
{
/// <summary>
/// Writes UNIX ar (archive) files in the GNU format.
/// </summary>
/// <remarks>
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Ar/ArWriter.cs">DotnetMakeDeb</see>
/// </remarks>
public class ArWriter
{
// Source: http://en.wikipedia.org/wiki/Ar_%28Unix%29
private readonly Stream _outStream;
/// <summary>
/// Initialises a new instance of the <see cref="ArWriter"/> class.
/// </summary>
/// <param name="outStream">The stream to write the archive to.</param>
public ArWriter(Stream outStream)
{
if (!outStream.CanWrite)
throw new ArgumentException("Stream not writable", nameof(outStream));
_outStream = outStream;
Initialize();
}
/// <summary>
/// Writes a file from disk to the archive.
/// </summary>
/// <param name="fileName">The name of the file to copy.</param>
/// <param name="userId">The user ID of the file in the archive.</param>
/// <param name="groupId">The group ID of the file in the archive.</param>
/// <param name="mode">The mode of the file in the archive (decimal).</param>
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);
}
/// <summary>
/// Writes a file from a Stream to the archive.
/// </summary>
/// <param name="stream">The stream to read the file contents from.</param>
/// <param name="fileName">The name of the file in the archive.</param>
/// <param name="modifyTime">The last modification time of the file in the archive.</param>
/// <param name="userId">The user ID of the file in the archive.</param>
/// <param name="groupId">The group ID of the file in the archive.</param>
/// <param name="mode">The mode of the file in the archive (decimal).</param>
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);
}
}
/// <summary>
/// Writes the archive header.
/// </summary>
private void Initialize()
{
WriteAsciiString("!<arch>\n");
}
/// <summary>
/// Writes a file header.
/// </summary>
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);
}
/// <summary>
/// Writes a string using ASCII encoding.
/// </summary>
/// <param name="str">The string to write to the output stream.</param>
private void WriteAsciiString(string str)
{
byte[] bytes = Encoding.ASCII.GetBytes(str);
_outStream.Write(bytes, 0, bytes.Length);
}
}
}

View File

@@ -0,0 +1,26 @@
namespace AMWD.Common.Packing.Tar.Interfaces
{
/// <summary>
/// Interface of a archive writer.
/// </summary>
public interface IArchiveDataWriter
{
/// <summary>
/// Write <paramref name="count"/> bytes of data from <paramref name="buffer"/> to corresponding archive.
/// </summary>
/// <param name="buffer">The data storage.</param>
/// <param name="count">How many bytes to be written to the corresponding archive.</param>
int Write(byte[] buffer, int count);
/// <summary>
/// Gets a value indicating whether the writer can write.
/// </summary>
bool CanWrite { get; }
}
/// <summary>
/// The writer delegate.
/// </summary>
/// <param name="writer">The writer.</param>
public delegate void WriteDataDelegate(IArchiveDataWriter writer);
}

View File

@@ -0,0 +1,125 @@
using System;
using AMWD.Common.Packing.Tar.Utils;
namespace AMWD.Common.Packing.Tar.Interfaces
{
/// <summary>
/// See "struct star_header" in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
/// </summary>
public interface ITarHeader
{
/// <summary>
/// The name field is the file name of the file, with directory names (if any) preceding the file name,
/// separated by slashes.
/// </summary>
/// <remarks>
/// <c>name</c>
/// <br/>
/// Byte offset: <c>0</c>
/// </remarks>
string FileName { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>mode</c>
/// <br/>
/// Byte offset: <c>100</c>
/// </remarks>
int Mode { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>uid</c>
/// <br/>
/// Byte offset: <c>108</c>
/// </remarks>
int UserId { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>gid</c>
/// <br/>
/// Byte offset: <c>116</c>
/// </remarks>
int GroupId { get; set; }
/// <summary>
/// The size field is the size of the file in bytes;
/// linked files are archived with this field specified as zero.
/// </summary>
/// <remarks>
/// <c>size</c>
/// <br/>
/// Byte offset: <c>124</c>
/// </remarks>
long SizeInBytes { get; set; }
/// <summary>
/// <para>mtime</para>
/// <para>byte offset: 136</para>
/// 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.
/// </summary>
/// <remarks>
/// <c>mtime</c>
/// <br/>
/// Byte offset: <c>136</c>
/// </remarks>
DateTime LastModification { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>typeflag</c>
/// <br/>
/// Byte offset: <c>156</c>
/// </remarks>
EntryType EntryType { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>uname</c>
/// <br/>
/// Byte offset: <c>265</c>
/// </remarks>
string UserName { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <c>gname</c>
/// <br/>
/// Byte offset: <c>297</c>
/// </remarks>
string GroupName { get; set; }
/// <summary>
/// The size of this header.
/// </summary>
int HeaderSize { get; }
}
}

View File

@@ -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
{
/// <summary>
/// Extract contents of a tar file represented by a stream for the TarReader constructor.
/// </summary>
/// <remarks>
/// Constructs TarReader object to read data from `tarredData` stream.
/// <br />
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarReader.cs">DotnetMakeDeb</see>
/// </remarks>
/// <param name="tarredData">A stream to read tar archive from</param>
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;
/// <summary>
/// Gets the file info (the header).
/// </summary>
public ITarHeader FileInfo => _header;
/// <summary>
/// Read all files from an archive to a directory. It creates some child directories to
/// reproduce a file structure from the archive.
/// </summary>
/// <param name="destDirectory">The out directory.</param>
///
/// 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);
}
}
/// <summary>
/// Read data from the current archive to a Stream.
/// </summary>
/// <param name="dataDestanation">A stream to read data to</param>
/// <seealso cref="MoveNext"/>
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);
}
/// <summary>
/// Reads data from the current archive to a buffer array.
/// </summary>
/// <param name="buffer">The buffer array.</param>
/// <returns>The nuber of bytes read.</returns>
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;
}
/// <summary>
/// Check if all bytes in buffer are zeroes
/// </summary>
/// <param name="buffer">buffer to check</param>
/// <returns>true if all bytes are zeroes, otherwise false</returns>
private static bool IsEmpty(IEnumerable<byte> buffer)
{
foreach (byte b in buffer)
{
if (b != 0)
return false;
}
return true;
}
/// <summary>
/// Move internal pointer to a next file in archive.
/// </summary>
/// <param name="skipData">Should be true if you want to read a header only, otherwise false</param>
/// <returns>false on End Of File otherwise true</returns>
///
/// Example:
/// while(MoveNext())
/// {
/// Read(dataDestStream);
/// }
/// <seealso cref="Read(Stream)"/>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Writes a tar (see GNU tar) archive to a stream.
/// </summary>
/// <remarks>
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/TarWriter.cs">DotnetMakeDeb</see>
/// </remarks>
public class TarWriter : LegacyTarWriter
{
/// <summary>
/// Initilizes a new instance of the <see cref="TarWriter"/> class.
/// </summary>
/// <param name="outStream">The stream to write the archive to.</param>
public TarWriter(Stream outStream)
: base(outStream)
{ }
/// <summary>
/// Writes an entry header (file, dir, ...) to the archive.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="lastModificationTime">The last modification time.</param>
/// <param name="count">The number of bytes.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The access mode.</param>
/// <param name="entryType">The entry type.</param>
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);
}
/// <summary>
/// Writes an entry header (file, dir, ...) to the archive.
/// Hashes the username and groupname down to a HashCode.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="lastModificationTime">The last modification time.</param>
/// <param name="count">The number of bytes.</param>
/// <param name="userName">The username.</param>
/// <param name="groupName">The group name.</param>
/// <param name="mode">The access mode.</param>
/// <param name="entryType">The entry type.</param>
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);
}
/// <summary>
/// Writes a file to the archive.
/// </summary>
/// <param name="name">The file name.</param>
/// <param name="dataSizeInBytes">The filesize in bytes.</param>
/// <param name="userName">The username.</param>
/// <param name="groupName">The group name.</param>
/// <param name="mode">The access mode.</param>
/// <param name="lastModificationTime">The last modification time.</param>
/// <param name="writeDelegate">The write handle.</param>
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);
}
/// <summary>
/// Writes a file to the archive.
/// </summary>
/// <param name="data">The file stream to add to the archive.</param>
/// <param name="dataSizeInBytes">The filesize in bytes.</param>
/// <param name="fileName">The file name.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The access mode.</param>
/// <param name="lastModificationTime">The last modification time.</param>
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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,73 @@
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// See "Values used in typeflag field." in <a href="https://www.gnu.org/software/tar/manual/html_node/Standard.html" />
/// </summary>
public enum EntryType : byte
{
/// <summary>
/// AREGTYPE, regular file
/// </summary>
File = 0,
/// <summary>
/// REGTYPE, regular file
/// </summary>
FileObsolete = 0x30,
/// <summary>
/// LNKTYPE, link
/// </summary>
HardLink = 0x31,
/// <summary>
/// SYMTYPE, reserved
/// </summary>
SymLink = 0x32,
/// <summary>
/// CHRTYPE, character special
/// </summary>
CharDevice = 0x33,
/// <summary>
/// BLKTYPE, block special
/// </summary>
BlockDevice = 0x34,
/// <summary>
/// DIRTYPE, directory
/// </summary>
Directory = 0x35,
/// <summary>
/// FIFOTYPE, FIFO special
/// </summary>
Fifo = 0x36,
/// <summary>
/// CONTTYPE, reserved
/// </summary>
Content = 0x37,
/// <summary>
/// XHDTYPE, Extended header referring to the next file in the archive
/// </summary>
ExtendedHeader = 0x78,
/// <summary>
/// XGLTYPE, Global extended header
/// </summary>
GlobalExtendedHeader = 0x67,
/// <summary>
/// GNUTYPE_LONGLINK, Identifies the *next* file on the tape as having a long linkname.
/// </summary>
LongLink = 0x4b,
/// <summary>
/// GNUTYPE_LONGNAME, Identifies the *next* file on the tape as having a long name.
/// </summary>
LongName = 0x4c
}
}

View File

@@ -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
{
/// <summary>
/// Implements a legacy TAR writer.
/// </summary>
/// <remarks>
/// Writes tar (see GNU tar) archive to a stream
/// <br/>
/// Copied from: <see href="https://github.com/ygoe/DotnetMakeDeb/blob/v1.1.0/DotnetMakeDeb/Tar/LegacyTarWriter.cs">DotnetMakeDeb</see>
/// </remarks>
/// <param name="outStream">stream to write archive to</param>
public class LegacyTarWriter(Stream outStream) : IDisposable
{
private readonly Stream _outStream = outStream;
private bool _isClosed;
/// <summary>
/// The buffer for writing.
/// </summary>
protected byte[] buffer = new byte[1024];
/// <summary>
/// Gets or sets a value indicating whether to read on zero.
/// </summary>
public bool ReadOnZero { get; set; } = true;
/// <summary>
/// Gets the output stream.
/// </summary>
protected virtual Stream OutStream
{
get { return _outStream; }
}
#region IDisposable Members
/// <inheritdoc/>
public void Dispose()
=> Close();
#endregion IDisposable Members
/// <summary>
/// Writes a directory entry.
/// </summary>
/// <param name="path">The path to the directory.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The access mode.</param>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is not set.</exception>
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);
}
/// <summary>
/// Writes a directory and its contents.
/// </summary>
/// <param name="directory">The directory.</param>
/// <param name="doRecursive">Write also sub-directories.</param>
/// <exception cref="ArgumentNullException"><paramref name="directory"/> is not set.</exception>
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);
}
}
/// <summary>
/// Writes a file.
/// </summary>
/// <param name="fileName">The file.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is not set.</exception>
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));
}
/// <summary>
/// Writes a file stream.
/// </summary>
/// <param name="file">The file stream.</param>
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));
}
/// <summary>
/// Writes a stream.
/// </summary>
/// <param name="data">The contents.</param>
/// <param name="dataSizeInBytes">The file size in bytes.</param>
/// <param name="name">The file name.</param>
public void Write(Stream data, long dataSizeInBytes, string name)
=> Write(data, dataSizeInBytes, name, 61, 61, 511, DateTime.Now);
/// <summary>
/// Writes a file to the archive.
/// </summary>
/// <param name="name">The file name.</param>
/// <param name="dataSizeInBytes">The file size in bytes.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The access mode.</param>
/// <param name="lastModificationTime">The last modification timestamp.</param>
/// <param name="writeDelegate">The <see cref="WriteDataDelegate"/>.</param>
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);
}
/// <summary>
/// Writes a stream as file to the archive.
/// </summary>
/// <param name="data">The content as <see cref="Stream"/>.</param>
/// <param name="dataSizeInBytes">The file size in bytes.</param>
/// <param name="name">The file name.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The access mode.</param>
/// <param name="lastModificationTime">The last modification timestamp.</param>
/// <exception cref="TarException">This writer is already closed.</exception>
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);
}
/// <summary>
/// Handle long file or path names (> 99 characters).
/// Write header and content, which its content contain the long (complete) file/path name.
/// <para>This handling method is adapted from https://github.com/qmfrederik/dotnet-packaging/pull/50/files#diff-f64c58cc18e8e445cee6ffed7a0d765cdb442c0ef21a3ed80bd20514057967b1 </para>
/// </summary>
/// <param name="name">File name or path name.</param>
/// <param name="userId">User ID.</param>
/// <param name="groupId">Group ID.</param>
/// <param name="mode">Mode.</param>
/// <param name="lastModificationTime">Last modification time.</param>
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);
}
/// <summary>
/// Writes a stream as file to the archive.
/// </summary>
/// <param name="count">The size of the file in bytes.</param>
/// <param name="data">The file content as stream.</param>
/// <exception cref="IOException"><paramref name="data"/> has not enough to read from.</exception>
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);
}
}
/// <summary>
/// Writes a entry header to the archive.
/// </summary>
/// <param name="name">The file name.</param>
/// <param name="lastModificationTime">The last modification time.</param>
/// <param name="count">The number of bytes.</param>
/// <param name="userId">The user id.</param>
/// <param name="groupId">The group id.</param>
/// <param name="mode">The file mode.</param>
/// <param name="entryType">The entry type</param>
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);
}
/// <summary>
/// Aligns the entry to 512 bytes.
/// </summary>
public void AlignTo512(long size, bool acceptZero)
{
size %= 512;
if (size == 0 && !acceptZero) return;
while (size < 512)
{
OutStream.WriteByte(0);
size++;
}
}
/// <summary>
/// Closes the writer and aligns to 512 bytes.
/// </summary>
public virtual void Close()
{
if (_isClosed)
return;
AlignTo512(0, true);
AlignTo512(0, true);
_isClosed = true;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// Represents errors that occur during tar archive execution.
/// </summary>
public class TarException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="TarException"/> class.
/// </summary>
public TarException()
: base()
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TarException"/> class with a specified
/// error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public TarException(string message)
: base(message)
{ }
/// <summary>
/// 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.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference
/// if no inner exception is specified.</param>
public TarException(string message, Exception innerException)
: base(message, innerException)
{ }
#if !NET8_0_OR_GREATER
/// <summary>
/// Initializes a new instance of the <see cref="TarException"/> class with serialized data.
/// </summary>
/// <param name="info">The <see cref="System.Runtime.Serialization.SerializationInfo"/> that holds the serialized
/// object data about the exception being thrown.</param>
/// <param name="context">The <see cref="System.Runtime.Serialization.StreamingContext"/> that contains contextual information
/// about the source or destination.</param>
protected TarException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{ }
#endif
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Net;
using System.Text;
namespace AMWD.Common.Packing.Tar.Utils
{
/// <summary>
/// UsTar header implementation.
/// </summary>
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;
}
}
}

View File

@@ -94,7 +94,7 @@ namespace System.Collections.Generic
/// Determines whether an element is in the <see cref="AsyncQueue{T}"/>. /// Determines whether an element is in the <see cref="AsyncQueue{T}"/>.
/// </summary> /// </summary>
/// <param name="item">The object to locate in the <see cref="AsyncQueue{T}"/>. The value can be null for reference types.</param> /// <param name="item">The object to locate in the <see cref="AsyncQueue{T}"/>. The value can be null for reference types.</param>
/// <returns>true if item is found in the <see cref="AsyncQueue{T}"/>; otherwise, false.</returns> /// <returns><see langword="true"/> if item is found in the <see cref="AsyncQueue{T}"/>, otherwise <see langword="false"/>.</returns>
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public bool Contains(T item) public bool Contains(T item)
{ {
@@ -172,7 +172,7 @@ namespace System.Collections.Generic
{ {
lock (_queue) lock (_queue)
{ {
return _queue.ToArray(); return [.. _queue];
} }
} }
@@ -304,7 +304,7 @@ namespace System.Collections.Generic
/// Removes the object at the beginning of the <see cref="AsyncQueue{T}"/>, and copies it to the <paramref name="result"/> parameter. /// Removes the object at the beginning of the <see cref="AsyncQueue{T}"/>, and copies it to the <paramref name="result"/> parameter.
/// </summary> /// </summary>
/// <param name="result">The removed object.</param> /// <param name="result">The removed object.</param>
/// <returns>true if the object is successfully removed; false if the <see cref="AsyncQueue{T}"/> is empty.</returns> /// <returns><see langword="true"/> if the object is successfully removed, <see langword="false"/> if the <see cref="AsyncQueue{T}"/> is empty.</returns>
public bool TryDequeue(out T result) public bool TryDequeue(out T result)
{ {
try try
@@ -325,7 +325,7 @@ namespace System.Collections.Generic
/// <paramref name="result"/> parameter. The object is not removed from the <see cref="AsyncQueue{T}"/>. /// <paramref name="result"/> parameter. The object is not removed from the <see cref="AsyncQueue{T}"/>.
/// </summary> /// </summary>
/// <param name="result">If present, the object at the beginning of the <see cref="AsyncQueue{T}"/>; otherwise, the default value of <typeparamref name="T"/>.</param> /// <param name="result">If present, the object at the beginning of the <see cref="AsyncQueue{T}"/>; otherwise, the default value of <typeparamref name="T"/>.</param>
/// <returns>true if there is an object at the beginning of the <see cref="AsyncQueue{T}"/>; false if the <see cref="AsyncQueue{T}"/> is empty.</returns> /// <returns><see langword="true"/> if there is an object at the beginning of the <see cref="AsyncQueue{T}"/>, <see langword="false"/> if the <see cref="AsyncQueue{T}"/> is empty.</returns>
public bool TryPeek(out T result) public bool TryPeek(out T result)
{ {
try try
@@ -344,7 +344,7 @@ namespace System.Collections.Generic
/// Removes the first occurrence of a specific object from the <see cref="AsyncQueue{T}"/>. /// Removes the first occurrence of a specific object from the <see cref="AsyncQueue{T}"/>.
/// </summary> /// </summary>
/// <param name="item">The object to remove from the <see cref="AsyncQueue{T}"/>. The value can be null for reference types.</param> /// <param name="item">The object to remove from the <see cref="AsyncQueue{T}"/>. The value can be null for reference types.</param>
/// <returns>true if item is successfully removed; otherwise, false. This method also returns false if item was not found in the <see cref="AsyncQueue{T}"/>.</returns> /// <returns><see langword="true"/> if item is successfully removed, otherwise <see langword="false"/>. This method also returns <see langword="false"/> if item was not found in the <see cref="AsyncQueue{T}"/>.</returns>
public bool Remove(T item) public bool Remove(T item)
{ {
lock (_queue) lock (_queue)

View File

@@ -171,6 +171,7 @@ namespace System.Security.Cryptography
#region Static methods #region Static methods
#region Encryption #region Encryption
#pragma warning disable SYSLIB0041
#region AES #region AES
@@ -185,7 +186,11 @@ namespace System.Security.Cryptography
byte[] salt = new byte[_saltLength]; byte[] salt = new byte[_saltLength];
Array.Copy(cipher, salt, _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(); using var aes = Aes.Create();
aes.Mode = CipherMode.CBC; aes.Mode = CipherMode.CBC;
@@ -225,7 +230,11 @@ namespace System.Security.Cryptography
{ {
byte[] salt = GetRandomBytes(_saltLength); 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(); using var aes = Aes.Create();
aes.Mode = CipherMode.CBC; aes.Mode = CipherMode.CBC;
@@ -271,7 +280,11 @@ namespace System.Security.Cryptography
byte[] salt = new byte[_saltLength]; byte[] salt = new byte[_saltLength];
Array.Copy(cipher, salt, _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(); using var tdes = TripleDES.Create();
tdes.Mode = CipherMode.CBC; tdes.Mode = CipherMode.CBC;
@@ -298,7 +311,11 @@ namespace System.Security.Cryptography
{ {
byte[] salt = GetRandomBytes(_saltLength); 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(); using var tdes = TripleDES.Create();
tdes.Mode = CipherMode.CBC; tdes.Mode = CipherMode.CBC;
@@ -344,6 +361,7 @@ namespace System.Security.Cryptography
#endregion Triple DES #endregion Triple DES
#pragma warning restore SYSLIB0041
#endregion Encryption #endregion Encryption
#region Hashing #region Hashing
@@ -379,8 +397,12 @@ namespace System.Security.Cryptography
/// <returns>The MD5 hash value, in hexadecimal notation.</returns> /// <returns>The MD5 hash value, in hexadecimal notation.</returns>
public static string Md5(byte[] bytes) public static string Md5(byte[] bytes)
{ {
#if NET8_0_OR_GREATER
return MD5.HashData(bytes).BytesToHex();
#else
using var md5 = MD5.Create(); using var md5 = MD5.Create();
return md5.ComputeHash(bytes).BytesToHex(); return md5.ComputeHash(bytes).BytesToHex();
#endif
} }
#endregion MD5 #endregion MD5
@@ -416,8 +438,12 @@ namespace System.Security.Cryptography
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns> /// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
public static string Sha1(byte[] bytes) public static string Sha1(byte[] bytes)
{ {
#if NET8_0_OR_GREATER
return SHA1.HashData(bytes).BytesToHex();
#else
using var sha1 = SHA1.Create(); using var sha1 = SHA1.Create();
return sha1.ComputeHash(bytes).BytesToHex(); return sha1.ComputeHash(bytes).BytesToHex();
#endif
} }
#endregion SHA-1 #endregion SHA-1
@@ -453,8 +479,12 @@ namespace System.Security.Cryptography
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns> /// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
public static string Sha256(byte[] bytes) public static string Sha256(byte[] bytes)
{ {
#if NET8_0_OR_GREATER
return SHA256.HashData(bytes).BytesToHex();
#else
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
return sha256.ComputeHash(bytes).BytesToHex(); return sha256.ComputeHash(bytes).BytesToHex();
#endif
} }
#endregion SHA-256 #endregion SHA-256
@@ -490,8 +520,12 @@ namespace System.Security.Cryptography
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns> /// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
public static string Sha512(byte[] bytes) public static string Sha512(byte[] bytes)
{ {
#if NET8_0_OR_GREATER
return SHA512.HashData(bytes).BytesToHex();
#else
using var sha512 = SHA512.Create(); using var sha512 = SHA512.Create();
return sha512.ComputeHash(bytes).BytesToHex(); return sha512.ComputeHash(bytes).BytesToHex();
#endif
} }
#endregion SHA-512 #endregion SHA-512

View File

@@ -4,7 +4,12 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
#if NET8_0_OR_GREATER
using IPNetwork = System.Net.IPNetwork;
#else
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
#endif
namespace AMWD.Common.Utilities namespace AMWD.Common.Utilities
{ {
@@ -23,7 +28,7 @@ namespace AMWD.Common.Utilities
public static List<IPAddress> ResolveHost(string hostname, AddressFamily addressFamily = default) public static List<IPAddress> ResolveHost(string hostname, AddressFamily addressFamily = default)
{ {
if (string.IsNullOrWhiteSpace(hostname)) if (string.IsNullOrWhiteSpace(hostname))
return new(); return [];
if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6)
addressFamily = AddressFamily.Unspecified; addressFamily = AddressFamily.Unspecified;
@@ -31,7 +36,7 @@ namespace AMWD.Common.Utilities
var ipAddress = ResolveIpAddress(hostname, addressFamily); var ipAddress = ResolveIpAddress(hostname, addressFamily);
// the name was an ip address, should not happen but experience tells other stories // the name was an ip address, should not happen but experience tells other stories
if (ipAddress != null) if (ipAddress != null)
return new() { ipAddress }; return [ipAddress];
try try
{ {
@@ -41,7 +46,7 @@ namespace AMWD.Common.Utilities
} }
catch catch
{ {
return new(); return [];
} }
} }
@@ -54,7 +59,7 @@ namespace AMWD.Common.Utilities
public static List<IPAddress> ResolveInterface(string interfaceName, AddressFamily addressFamily = default) public static List<IPAddress> ResolveInterface(string interfaceName, AddressFamily addressFamily = default)
{ {
if (string.IsNullOrWhiteSpace(interfaceName)) if (string.IsNullOrWhiteSpace(interfaceName))
return new(); return [];
if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6)
addressFamily = AddressFamily.Unspecified; addressFamily = AddressFamily.Unspecified;
@@ -62,7 +67,7 @@ namespace AMWD.Common.Utilities
var ipAddress = ResolveIpAddress(interfaceName, addressFamily); var ipAddress = ResolveIpAddress(interfaceName, addressFamily);
// the name was an ip address, should not happen but experience tells other stories // the name was an ip address, should not happen but experience tells other stories
if (ipAddress != null) if (ipAddress != null)
return new() { ipAddress }; return [ipAddress];
try try
{ {
@@ -74,45 +79,7 @@ namespace AMWD.Common.Utilities
} }
catch catch
{ {
return new(); return [];
}
}
/// <summary>
/// Parses a CIDR network definition.
/// </summary>
/// <param name="network">The network in CIDR.</param>
/// <returns>The <see cref="IPNetwork"/> or <c>null</c>.</returns>
public static IPNetwork ParseNetwork(string network)
{
TryParseNetwork(network, out var ipNetwork);
return ipNetwork;
}
/// <summary>
/// Tries to parse a CIDR network definition.
/// </summary>
/// <param name="network">The network in CIDR.</param>
/// <param name="ipNetwork">The parsed <see cref="IPNetwork"/>.</param>
/// <returns><c>true</c> on success, otherwise <c>false</c>.</returns>
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;
} }
} }
@@ -128,7 +95,11 @@ namespace AMWD.Common.Utilities
{ {
var list = new List<IPAddress>(); var list = new List<IPAddress>();
#if NET8_0_OR_GREATER
var ipAddress = network.BaseAddress;
#else
var ipAddress = network.Prefix; var ipAddress = network.Prefix;
#endif
while (network.Contains(ipAddress)) while (network.Contains(ipAddress))
{ {
list.Add(ipAddress); list.Add(ipAddress);

View File

@@ -9,12 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
###### Diffs ###### Diffs
- [AMWD.Common](https://git.am-wd.de/AM.WD/common/compare/v2.0.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...main) - [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...main) - [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...main) - [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 ## asp/v3.0.0, efc/v3.0.0 - 2023-12-28

View File

@@ -41,6 +41,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{93EC8B16
nuget.config = nuget.config nuget.config = nuget.config
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMWD.Common.MessagePack", "AMWD.Common.MessagePack\AMWD.Common.MessagePack.csproj", "{EA014C15-93B6-4F2C-8229-1C13E22BF84A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{9469D87B-126E-4338-92E3-701F762CB54D}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -80,6 +86,7 @@ Global
{86DE1B7C-3ECF-49B1-AB28-A976A3973FF5} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB} {86DE1B7C-3ECF-49B1-AB28-A976A3973FF5} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB}
{7196DA2B-D858-4B25-BC23-865175CFCDEC} = {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} {93EC8B16-7DEF-4E39-B590-E804DEF7C607} = {AFBF83AE-FE7D-48C1-B7E7-31BF3E17C6FB}
{EA014C15-93B6-4F2C-8229-1C13E22BF84A} = {F2C7556A-99EB-43EB-8954-56A24AFE928F}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC} SolutionGuid = {961E8DF8-DDF5-4D10-A510-CE409E9962AC}

View File

@@ -4,7 +4,6 @@
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory> <CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
@@ -30,11 +29,11 @@
<ItemGroup Condition="'$(CI)' == 'true'"> <ItemGroup Condition="'$(CI)' == 'true'">
<SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" /> <SourceLinkGitLabHost Include="$(CI_SERVER_HOST)" Version="$(CI_SERVER_VERSION)" />
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="1.1.1" PrivateAssets="all" /> <PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AMWD.NetRevisionTask" Version="1.1.0"> <PackageReference Include="AMWD.NetRevisionTask" Version="1.2.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using AMWD.Common.AspNetCore.Security.BasicAuthentication; using AMWD.Common.AspNetCore.Security.BasicAuthentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq; using Moq;
@@ -156,12 +157,16 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication
var requestHeaderMock = new Mock<IHeaderDictionary>(); var requestHeaderMock = new Mock<IHeaderDictionary>();
foreach (var header in _requestHeaders) foreach (var header in _requestHeaders)
{ {
var strVal = new StringValues(header.Value);
requestHeaderMock requestHeaderMock
.Setup(h => h.ContainsKey(header.Key)) .Setup(h => h.ContainsKey(header.Key))
.Returns(true); .Returns(true);
requestHeaderMock requestHeaderMock
.Setup(h => h[header.Key]) .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<HttpRequest>(); var requestMock = new Mock<HttpRequest>();
@@ -174,6 +179,11 @@ namespace UnitTests.AspNetCore.Security.BasicAuthentication
responseHeaderMock responseHeaderMock
.SetupSet(h => h[It.IsAny<string>()] = It.IsAny<StringValues>()) .SetupSet(h => h[It.IsAny<string>()] = It.IsAny<StringValues>())
.Callback<string, StringValues>((key, value) => _responseHeadersCallback[key] = value); .Callback<string, StringValues>((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<HttpResponse>(); var responseMock = new Mock<HttpResponse>();
responseMock responseMock

View File

@@ -108,22 +108,6 @@ namespace UnitTests.Common.Extensions
Assert.IsFalse(list.Any()); 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 internal enum TestEnum
{ {
[CustomMultiple("nix")] [CustomMultiple("nix")]
@@ -132,7 +116,6 @@ namespace UnitTests.Common.Extensions
Zero, Zero,
[Description("Eins")] [Description("Eins")]
One, One,
[Display(Name = "Zwei")]
Two, Two,
} }
} }

View File

@@ -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<string, ArFileInfo> _files;
private Stream inStream;
[TestInitialize]
public void Initialize()
{
_files = new Dictionary<string, ArFileInfo>
{
{
"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("!<arch>\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("!<arch>\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<ArFileInfo>();
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<string, string>();
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; }
}
}
}

View File

@@ -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<string, string> 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("!<arch>\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;
}
}
}

View File

@@ -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
{
}
}

View File

@@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
@@ -13,8 +14,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="DNS" Version="7.0.0" /> <PackageReference Include="DNS" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.69" /> <PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" /> <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="ReflectionMagic" Version="5.0.0" /> <PackageReference Include="ReflectionMagic" Version="5.0.0" />