Merge branch 'packing'
This commit is contained in:
@@ -29,6 +29,8 @@ build-debug:
|
|||||||
- 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:
|
||||||
@@ -73,6 +75,8 @@ build-release:
|
|||||||
- 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:
|
||||||
@@ -137,6 +141,19 @@ deploy-entityframework:
|
|||||||
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
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
32
AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj
Normal file
32
AMWD.Common.MessagePack/AMWD.Common.MessagePack.csproj
Normal 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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"/>.
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
17
AMWD.Common.MessagePack/Utilities/NetworkHelper.cs
Normal file
17
AMWD.Common.MessagePack/Utilities/NetworkHelper.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
|
|
||||||
|
|
||||||
namespace System
|
namespace System
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
51
AMWD.Common/Packing/Ar/ArFileInfo.cs
Normal file
51
AMWD.Common/Packing/Ar/ArFileInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
176
AMWD.Common/Packing/Ar/ArReader.cs
Normal file
176
AMWD.Common/Packing/Ar/ArReader.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
AMWD.Common/Packing/Ar/ArWriter.cs
Normal file
146
AMWD.Common/Packing/Ar/ArWriter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs
Normal file
26
AMWD.Common/Packing/Tar/Interfaces/IArchiveDataWriter.cs
Normal 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);
|
||||||
|
}
|
||||||
125
AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs
Normal file
125
AMWD.Common/Packing/Tar/Interfaces/ITarHeader.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
205
AMWD.Common/Packing/Tar/TarReader.cs
Normal file
205
AMWD.Common/Packing/Tar/TarReader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
AMWD.Common/Packing/Tar/TarWriter.cs
Normal file
113
AMWD.Common/Packing/Tar/TarWriter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
AMWD.Common/Packing/Tar/Utils/DataWriter.cs
Normal file
45
AMWD.Common/Packing/Tar/Utils/DataWriter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
AMWD.Common/Packing/Tar/Utils/EntryType.cs
Normal file
73
AMWD.Common/Packing/Tar/Utils/EntryType.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
329
AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs
Normal file
329
AMWD.Common/Packing/Tar/Utils/LegacyTarWriter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
AMWD.Common/Packing/Tar/Utils/TarException.cs
Normal file
52
AMWD.Common/Packing/Tar/Utils/TarException.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
179
AMWD.Common/Packing/Tar/Utils/TarHeader.cs
Normal file
179
AMWD.Common/Packing/Tar/Utils/TarHeader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs
Normal file
127
AMWD.Common/Packing/Tar/Utils/UsTarHeader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
355
UnitTests/Common/Packing/Ar/ArReaderTests.cs
Normal file
355
UnitTests/Common/Packing/Ar/ArReaderTests.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
306
UnitTests/Common/Packing/Ar/ArWriterTests.cs
Normal file
306
UnitTests/Common/Packing/Ar/ArWriterTests.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
UnitTests/Common/Packing/Tar/TarReaderTests.cs
Normal file
12
UnitTests/Common/Packing/Tar/TarReaderTests.cs
Normal 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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user