2025-09-05 14:06:55 +02:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-09-05 14:07:35 +02:00
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
2025-09-05 14:06:55 +02:00
// Software is furnished to do so, subject to the following conditions:
2025-09-05 14:07:35 +02:00
//
// The above copyright notice and this permission notice shall be included in
2025-09-05 14:06:55 +02:00
// all copies or substantial portions of the Software.
2025-09-05 14:07:35 +02:00
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2025-09-05 14:06:55 +02:00
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System ;
using System.Collections.Generic ;
using System.Diagnostics.CodeAnalysis ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Net ;
using System.Net.NetworkInformation ;
using System.Net.Sockets ;
using System.Runtime.CompilerServices ;
using System.Runtime.Versioning ;
using System.Security.Cryptography ;
using System.Security.Cryptography.X509Certificates ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
using Duplicati.Library.Common.IO ;
using Duplicati.StreamUtil ;
namespace Duplicati.Library.Utility
{
public static class Utility
{
/// <summary>
/// Size of buffers for copying stream
/// </summary>
public static long DEFAULT_BUFFER_SIZE => SystemContextSettings . Buffersize ;
/// <summary>
/// A cache of the FileSystemCaseSensitive property, which is computed upon the first access.
/// </summary>
private static bool? CachedIsFSCaseSensitive ;
/// <summary>
/// The EPOCH offset (unix style)
/// </summary>
public static readonly DateTime EPOCH = new DateTime ( 1970 , 1 , 1 , 0 , 0 , 0 , DateTimeKind . Utc );
/// <summary>
/// The attribute value used to indicate error
/// </summary>
public const FileAttributes ATTRIBUTE_ERROR = ( FileAttributes )( 1 << 30 );
2026-02-06 10:37:27 +01:00
/// <summary>
/// The multiplier used to determine if there is enough free space on the temporary volume
/// </summary>
public const long VOLUME_SIZE_FREE_SPACE_MULTIPLIER = 10 ;
2025-09-05 14:06:55 +02:00
/// <summary>
/// The callback delegate type used to collecting file information
/// </summary>
/// <param name="rootpath">The path that the file enumeration started at</param>
/// <param name="path">The current element</param>
/// <param name="attributes">The attributes of the element</param>
/// <returns>A value indicating if the folder should be recursed, ignored for other types</returns>
public delegate bool EnumerationFilterDelegate ( string rootpath , string path , FileAttributes attributes );
/// <summary>
/// Copies the content of one stream into another
/// </summary>
/// <param name="source">The stream to read from</param>
/// <param name="target">The stream to write to</param>
public static long CopyStream ( Stream source , Stream target )
{
return CopyStream ( source , target , true );
}
/// <summary>
/// Copies the content of one stream into another
/// </summary>
/// <param name="source">The stream to read from</param>
/// <param name="target">The stream to write to</param>
/// <param name="tryRewindSource">True if an attempt should be made to rewind the source stream, false otherwise</param>
/// <param name="buf">Temporary buffer to use (optional)</param>
public static long CopyStream ( Stream source , Stream target , bool tryRewindSource , byte []? buf = null )
{
if ( tryRewindSource && source . CanSeek )
try { source . Position = 0 ; }
catch
{
// ignored
}
buf = buf ?? new byte [ DEFAULT_BUFFER_SIZE ];
int read ;
long total = 0 ;
while (( read = source . Read ( buf , 0 , buf . Length )) != 0 )
{
target . Write ( buf , 0 , read );
total += read ;
}
return total ;
}
/// <summary>
/// Copies the content of one stream into another
/// </summary>
/// <param name="source">The stream to read from</param>
/// <param name="target">The stream to write to</param>
/// <param name="cancelToken">Token to cancel the operation.</param>
public static async Task < long > CopyStreamAsync ( Stream source , Stream target , CancellationToken cancelToken )
{
return await CopyStreamAsync ( source , target , tryRewindSource : true , cancelToken : cancelToken ). ConfigureAwait ( false );
}
/// <summary>
/// Copies the content of one stream into another
/// </summary>
/// <param name="source">The stream to read from</param>
/// <param name="target">The stream to write to</param>
/// <param name="tryRewindSource">True if an attempt should be made to rewind the source stream, false otherwise</param>
/// <param name="cancelToken">Token to cancel the operation.</param>
/// <param name="buf">Temporary buffer to use (optional)</param>
public static async Task < long > CopyStreamAsync ( Stream source , Stream target , bool tryRewindSource , CancellationToken cancelToken , byte []? buf = null )
{
if ( tryRewindSource && source . CanSeek )
try { source . Position = 0 ; }
catch { }
buf = buf ?? new byte [ DEFAULT_BUFFER_SIZE ];
int read ;
long total = 0 ;
while ( true )
{
read = await source . ReadAsync ( buf , 0 , buf . Length , cancelToken ). ConfigureAwait ( false );
if ( read == 0 ) break ;
await target . WriteAsync ( buf , 0 , read , cancelToken ). ConfigureAwait ( false );
total += read ;
}
return total ;
}
/// <summary>
/// Get the length of a stream.
/// Attempt to use the stream's Position property if allowPositionFallback is <c>true</c> (only valid if stream is at the end).
/// </summary>
/// <param name="stream">Stream to get the length of.</param>
/// <param name="allowPositionFallback">Attempt to use the Position property if <c>true</c> and the Length property is not available (only valid if stream is at the end).</param>
/// <returns>Returns the stream's length, if available, or null if not supported by the stream.</returns>
public static long? GetStreamLength ( Stream stream , bool allowPositionFallback = true )
{
return GetStreamLength ( stream , out bool _ , allowPositionFallback );
}
/// <summary>
/// Get the length of a stream.
/// Attempt to use the stream's Position property if allowPositionFallback is <c>true</c> (only valid if stream is at the end).
/// </summary>
/// <param name="stream">Stream to get the length of.</param>
/// <param name="isStreamPosition">Indicates if the Position value was used instead of Length.</param>
/// <param name="allowPositionFallback">Attempt to use the Position property if <c>true</c> and the Length property is not available (only valid if stream is at the end).</param>
/// <returns>Returns the stream's length, if available, or null if not supported by the stream.</returns>
public static long? GetStreamLength ( Stream stream , out bool isStreamPosition , bool allowPositionFallback = true )
{
isStreamPosition = false ;
long? streamLength = null ;
try { streamLength = stream . Length ; } catch { }
if (! streamLength . HasValue && allowPositionFallback )
{
try
{
// Hack: This is a fall-back method to detect the source stream size, assuming the current position is the end of the stream.
streamLength = stream . Position ;
isStreamPosition = true ;
}
2025-09-05 14:07:35 +02:00
catch { } //
2025-09-05 14:06:55 +02:00
}
return streamLength ;
}
2026-01-14 09:58:19 +01:00
/// <summary>
/// Reads a byte array and returns a string, removing any BOM if present
/// </summary>
/// <param name="s">The byte array to read from.</param>
/// <returns>The resulting string.</returns>
public static string GetStringWithoutBOM ( byte [] s )
{
using var ms = new MemoryStream ( s );
return GetStringWithoutBOM ( ms , leaveOpen : false );
}
/// <summary>
/// Reads a stream and returns a string, removing any BOM if present
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="leaveOpen">If true, the stream is not closed after reading.</param>
/// <returns>The resulting string.</returns>
public static string GetStringWithoutBOM ( Stream stream , bool leaveOpen )
{
if ( stream . CanSeek )
try { stream . Position = 0 ; }
catch { }
using var sr = new StreamReader ( stream , Encoding . UTF8 , true , leaveOpen : leaveOpen );
return sr . ReadToEnd ();
}
2025-09-05 14:06:55 +02:00
/// <summary>
/// These are characters that must be escaped when using a globbing expression
/// </summary>
private static readonly string BADCHARS = @"\\|\+|\||\{|\[|\(|\)|\]|\}|\^|\$|\#|\." ;
/// <summary>
/// Most people will probably want to use fileglobbing, but RegExp's are more flexible.
/// By converting from the weak globbing to the stronger regexp, we support both.
/// </summary>
/// <param name="globexp"></param>
/// <returns></returns>
public static string ConvertGlobbingToRegExp ( string globexp )
{
//First escape all special characters
globexp = Regex . Replace ( globexp , BADCHARS , @"\$&" );
//Replace the globbing expressions with the corresponding regular expressions
globexp = globexp . Replace ( '?' , '.' ). Replace ( "*" , ".*" );
return "^" + globexp + "$" ;
}
/// <summary>
/// Convert literal path to the equivalent regular expression.
/// </summary>
public static string ConvertLiteralToRegExp ( string literalPath )
{
// Escape all special characters
return Regex . Escape ( literalPath );
}
/// <summary>
/// Returns a list of all files found in the given folder.
/// The search is recursive.
/// </summary>
/// <param name="basepath">The folder to look in</param>
/// <returns>A list of the full filenames</returns>
public static IEnumerable < string > EnumerateFiles ( string basepath )
{
return EnumerateFileSystemEntries ( basepath ). Where ( x => ! x . EndsWith ( Util . DirectorySeparatorString , StringComparison . Ordinal ));
}
/// <summary>
/// Returns a list of folder names found in the given folder.
/// The search is recursive.
/// </summary>
/// <param name="basepath">The folder to look in</param>
/// <returns>A list of the full paths</returns>
public static IEnumerable < string > EnumerateFolders ( string basepath )
{
return EnumerateFileSystemEntries ( basepath ). Where ( x => x . EndsWith ( Util . DirectorySeparatorString , StringComparison . Ordinal ));
}
/// <summary>
/// Returns a list of all files and subfolders found in the given folder.
/// The search is recursive.
/// </summary>
/// <param name="basepath">The folder to look in.</param>
/// <returns>A list of the full filenames and foldernames. Foldernames ends with the directoryseparator char</returns>
public static IEnumerable < string > EnumerateFileSystemEntries ( string basepath )
{
return EnumerateFileSystemEntries ( basepath , SystemIO . IO_OS . GetDirectories , Directory . GetFiles , null );
}
/// <summary>
/// A callback delegate used for applying alternate enumeration of filesystems
/// </summary>
/// <param name="path">The path to return data from</param>
/// <returns>A list of paths</returns>
public delegate string [] FileSystemInteraction ( string path );
/// <summary>
/// A callback delegate used for extracting attributes from a file or folder
/// </summary>
/// <param name="path">The path to return data from</param>
/// <returns>Attributes for the file or folder</returns>
public delegate FileAttributes ExtractFileAttributes ( string path );
/// <summary>
/// A callback delegate used for extracting attributes from a file or folder
/// </summary>
/// <param name="rootpath">The root folder where the path was found</param>
/// <param name="path">The path that produced the error</param>
/// <param name="ex">The exception for the error</param>
public delegate void ReportAccessError ( string rootpath , string path , Exception ex );
/// <summary>
/// Returns a list of all files found in the given folder.
/// The search is recursive.
/// </summary>
/// <param name="rootpath">The folder to look in</param>
/// <param name="folderList">A function to call that lists all folders in the supplied folder</param>
/// <param name="fileList">A function to call that lists all files in the supplied folder</param>
/// <param name="attributeReader">A function to call that obtains the attributes for an element, set to null to avoid reading attributes</param>
/// <param name="errorCallback">An optional function to call with error messages.</param>
/// <returns>A list of the full filenames</returns>
public static IEnumerable < string > EnumerateFileSystemEntries ( string rootpath , FileSystemInteraction folderList , FileSystemInteraction fileList , ExtractFileAttributes ? attributeReader , ReportAccessError ? errorCallback = null )
{
var lst = new Stack < string >();
if ( IsFolder ( rootpath , attributeReader ))
{
rootpath = Util . AppendDirSeparator ( rootpath );
try
{
var attr = attributeReader ?. Invoke ( rootpath ) ?? FileAttributes . Directory ;
lst . Push ( rootpath );
}
catch ( Exception ex ) when (! ex . IsAbortException ())
{
errorCallback ?. Invoke ( rootpath , rootpath , ex );
}
while ( lst . Count > 0 )
{
var f = lst . Pop ();
yield return f ;
try
{
foreach ( var s in folderList ( f ))
{
var sf = Util . AppendDirSeparator ( s );
try
{
var attr = attributeReader ?. Invoke ( sf ) ?? FileAttributes . Directory ;
lst . Push ( sf );
}
catch ( Exception ex ) when (! ex . IsAbortException ())
{
errorCallback ?. Invoke ( rootpath , sf , ex );
}
}
}
catch ( Exception ex ) when (! ex . IsAbortException ())
{
errorCallback ?. Invoke ( rootpath , f , ex );
}
string []? files = null ;
if ( fileList != null )
{
try
{
files = fileList ( f );
}
catch ( Exception ex ) when (! ex . IsAbortException ())
{
errorCallback ?. Invoke ( rootpath , f , ex );
}
}
if ( files != null )
{
foreach ( var s in files )
yield return s ;
}
}
}
else
{
yield return rootpath ;
}
}
/// <summary>
/// Test if specified path is a folder
/// </summary>
/// <param name="path">Path to test</param>
/// <param name="attributeReader">Function to use for testing path</param>
/// <returns>True if path is refers to a folder</returns>
public static bool IsFolder ( string path , ExtractFileAttributes ? attributeReader )
{
if ( attributeReader == null )
return true ;
try
{
return attributeReader ( path ). HasFlag ( FileAttributes . Directory );
}
catch
{
return false ;
}
}
/// <summary>
/// Tests if path refers to a file, or folder, <b>below</b> the parent folder
/// </summary>
/// <param name="fileOrFolderPath">File or folder to test</param>
/// <param name="parentFolder">Candidate parent folder</param>
/// <returns>True if below parent folder, false otherwise
/// (note that this returns false if the two argument paths are identical!)</returns>
public static bool IsPathBelowFolder ( string fileOrFolderPath , string parentFolder )
{
var sanitizedParentFolder = Util . AppendDirSeparator ( parentFolder );
return fileOrFolderPath . StartsWith ( sanitizedParentFolder , ClientFilenameStringComparison ) &&
! fileOrFolderPath . Equals ( sanitizedParentFolder , ClientFilenameStringComparison );
}
/// <summary>
/// Returns parent folder of path
/// </summary>
/// <param name="path">Full file or folder path</param>
/// <param name="forceTrailingDirectorySeparator">If true, return value always has trailing separator</param>
/// <returns>Parent folder of path (containing folder for file paths, parent folder for folder paths)</returns>
public static string? GetParent ( string path , bool forceTrailingDirectorySeparator )
{
var len = path . Length - 1 ;
if ( len > 1 && path [ len ] == Path . DirectorySeparatorChar )
{
len --;
}
var last = path . LastIndexOf ( Path . DirectorySeparatorChar , len );
if ( last == - 1 || last == 0 && len == 0 )
return null ;
if ( last == 0 && ! OperatingSystem . IsWindows ())
return Util . DirectorySeparatorString ;
var parent = path . Substring ( 0 , last );
if ( forceTrailingDirectorySeparator ||
OperatingSystem . IsWindows () && parent . Length == 2 && parent [ 1 ] == ':' && char . IsLetter ( parent [ 0 ]))
{
parent += Path . DirectorySeparatorChar ;
}
return parent ;
}
/// <summary>
/// Generates a new GUID and returns it as a hex string (without hyphens).
/// </summary>
/// <remarks>The string is preformed using invariant culture.</remarks>
/// <returns>A hex string representation of a new GUID</returns>
public static string GetHexGuid ()
{
2025-09-25 11:14:54 +02:00
return ByteArrayAsHexString (
2025-09-05 14:06:55 +02:00
Guid . NewGuid (). ToByteArray ()
2025-09-25 11:14:54 +02:00
);
2025-09-05 14:06:55 +02:00
}
/// <summary>
/// Given a collection of unique folders, returns only parent-most folders
/// </summary>
/// <param name="folders">Collection of unique folders</param>
/// <returns>Parent-most folders of input collection</returns>
public static IEnumerable < string > SimplifyFolderList ( ICollection < string > folders )
{
if (! folders . Any ())
return folders ;
var result = new LinkedList < string >();
result . AddFirst ( folders . First ());
foreach ( var folder1 in folders )
{
bool addFolder = true ;
LinkedListNode < string >? next ;
for ( var node = result . First ; node != null ; node = next )
{
next = node . Next ;
var folder2 = node . Value ;
if ( IsPathBelowFolder ( folder1 , folder2 ))
{
// higher-level folder already present
addFolder = false ;
break ;
}
if ( IsPathBelowFolder ( folder2 , folder1 ))
{
// retain folder1
result . Remove ( node );
}
}
if ( addFolder )
{
result . AddFirst ( folder1 );
}
}
return result . Distinct ();
}
/// <summary>
/// Given a collection of file paths, return those NOT contained within specified collection of folders
/// </summary>
/// <param name="files">Collection of files to filter</param>
/// <param name="folders">Collection of folders to use as filter</param>
/// <returns>Files not in any of specified <c>folders</c></returns>
public static IEnumerable < string > GetFilesNotInFolders ( IEnumerable < string > files , IEnumerable < string > folders )
{
return files . Where ( x => folders . All ( folder => ! IsPathBelowFolder ( x , folder )));
}
/// <summary>
/// Calculates the size of files in a given folder
/// </summary>
/// <param name="folder">The folder to examine</param>
/// <returns>The combined size of all files that match the filter</returns>
public static long GetDirectorySize ( string folder )
{
return EnumerateFolders ( folder ). Sum (( path ) => new FileInfo ( path ). Length );
}
/// <summary>
/// Some streams can return a number that is less than the requested number of bytes.
/// This is usually due to fragmentation, and is solved by issuing a new read.
/// This function wraps that functionality.
/// </summary>
/// <param name="stream">The stream to read</param>
/// <param name="buf">The buffer to read into</param>
/// <param name="count">The amount of bytes to read</param>
/// <returns>The actual number of bytes read</returns>
public static int ForceStreamRead ( Stream stream , byte [] buf , int count )
{
int a ;
int index = 0 ;
do
{
a = stream . Read ( buf , index , count );
index += a ;
count -= a ;
} while ( a != 0 && count > 0 );
return index ;
}
/// <summary>
/// Some streams can return a number that is less than the requested number of bytes.
/// This is usually due to fragmentation, and is solved by issuing a new read.
/// This function wraps that functionality.
/// </summary>
/// <param name="stream">The stream to read.</param>
/// <param name="buf">The buffer to read into.</param>
/// <param name="count">The amount of bytes to read.</param>
/// <returns>The number of bytes read</returns>
2026-01-14 10:11:24 +01:00
public static async Task < int > ForceStreamReadAsync ( this System . IO . Stream stream , byte [] buf , int count , CancellationToken cancellationToken )
2025-09-05 14:06:55 +02:00
{
int a ;
int index = 0 ;
do
{
2026-01-14 10:11:24 +01:00
a = await stream . ReadAsync ( buf , index , count , cancellationToken ). ConfigureAwait ( false );
2025-09-05 14:06:55 +02:00
index += a ;
count -= a ;
} while ( a != 0 && count > 0 );
return index ;
}
/// <summary>
/// Compares two streams to see if they are binary equals
/// </summary>
/// <param name="stream1">One stream</param>
/// <param name="stream2">Another stream</param>
/// <param name="checkLength">True if the length of the two streams should be compared</param>
/// <returns>True if they are equal, false otherwise</returns>
public static bool CompareStreams ( Stream stream1 , Stream stream2 , bool checkLength )
{
if ( checkLength )
{
try
{
if ( stream1 . Length != stream2 . Length )
return false ;
}
catch
{
//We must read along, trying to determine if they are equals
}
}
int longSize = BitConverter . GetBytes (( long ) 0 ). Length ;
byte [] buf1 = new byte [ longSize * 512 ];
byte [] buf2 = new byte [ buf1 . Length ];
int a1 , a2 ;
while (( a1 = ForceStreamRead ( stream1 , buf1 , buf1 . Length )) == ( a2 = ForceStreamRead ( stream2 , buf2 , buf2 . Length )))
{
int ix = 0 ;
for ( int i = 0 ; i < a1 / longSize ; i ++)
if ( BitConverter . ToUInt64 ( buf1 , ix ) != BitConverter . ToUInt64 ( buf2 , ix ))
return false ;
else
ix += longSize ;
for ( int i = 0 ; i < a1 % longSize ; i ++)
if ( buf1 [ ix ] != buf2 [ ix ])
return false ;
else
ix ++;
if ( a1 == 0 )
break ;
}
return a1 == a2 ;
}
/// <summary>
/// Reads a file, attempts to detect encoding
/// </summary>
/// <param name="filename">The path to the file to read</param>
/// <returns>The file contents</returns>
public static string ReadFileWithDefaultEncoding ( string filename )
{
// Since StreamReader defaults to UTF8 and most text files will NOT be UTF8 without BOM,
// we need to detect the encoding (at least that it's not UTF8).
2025-09-05 14:07:35 +02:00
// So we read the first 4096 bytes and try to decode them as UTF8.
2025-09-05 14:06:55 +02:00
var buffer = new byte [ 4096 ];
using ( var file = new FileStream ( filename , FileMode . Open , FileAccess . Read , FileShare . Read ))
{
Utility . ForceStreamRead ( file , buffer , 4096 );
}
var enc = Encoding . UTF8 ;
try
{
// this will throw an error if not really UTF8
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
new UTF8Encoding ( false , true ). GetString ( buffer );
}
catch ( Exception )
{
enc = Encoding . Default ;
}
// This will load the text using the BOM, or the detected encoding if no BOM.
using ( var reader = new StreamReader ( filename , enc , true ))
{
// Remove all \r from the file and split on \n, then pass directly to ExtractOptions
return reader . ReadToEnd ();
}
}
/// <summary>
/// Formats a size into a human readable format, e.g. 2048 becomes "2 KB" or -2283 becomes "-2.23 KB%quot.
/// </summary>
/// <param name="size">The size to format</param>
/// <returns>A human readable string representing the size</returns>
public static string FormatSizeString ( double size )
{
double sizeAbs = Math . Abs ( size ); // Allow formatting of negative sizes
if ( sizeAbs >= 1024 * 1024 * 1024 * 1024L )
return Strings . Utility . FormatStringTB ( size / ( 1024 * 1024 * 1024 * 1024L ));
else if ( sizeAbs >= 1024 * 1024 * 1024 )
return Strings . Utility . FormatStringGB ( size / ( 1024 * 1024 * 1024 ));
else if ( sizeAbs >= 1024 * 1024 )
return Strings . Utility . FormatStringMB ( size / ( 1024 * 1024 ));
else if ( sizeAbs >= 1024 )
return Strings . Utility . FormatStringKB ( size / 1024 );
else
return Strings . Utility . FormatStringB (( long ) size ); // safe to cast because lower than 1024 and thus well within range of long
}
/// <summary>
/// Parses a string into a boolean value.
/// </summary>
/// <param name="value">The value to parse.</param>
/// <param name="defaultFunc">A delegate that returns the default value if <paramref name="value"/> is not a valid boolean value.</param>
/// <returns>The parsed value, or the value returned by <paramref name="defaultFunc"/>.</returns>
public static bool ParseBool ( string? value , Func < bool > defaultFunc )
{
if ( String . IsNullOrWhiteSpace ( value ))
{
return defaultFunc ();
}
switch ( value . Trim (). ToLower ( CultureInfo . InvariantCulture ))
{
case "1" :
case "on" :
case "true" :
case "yes" :
return true ;
case "0" :
case "off" :
case "false" :
case "no" :
return false ;
default :
return defaultFunc ();
}
}
/// <summary>
/// Parses a string into a boolean value.
/// </summary>
/// <param name="value">The value to parse.</param>
/// <param name="default">The default value, in case <paramref name="value"/> is not a valid boolean value.</param>
/// <returns>The parsed value, or the default value.</returns>
public static bool ParseBool ( string? value , bool @default )
{
return ParseBool ( value , () => @default );
}
/// <summary>
/// Parses an option from the option set, using the convention that if the option is set, it is true unless it parses to false, and false otherwise
/// </summary>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <returns>The parsed value, or the default value (<c>false</c>).</returns>
public static bool ParseBoolOption ( IReadOnlyDictionary < string , string? > options , string value )
{
if ( options . TryGetValue ( value , out var opt ))
return ParseBool ( opt , true );
else
return false ;
}
/// <summary>
/// Parses an integer option from the option set, returning the default value if the option is not found or cannot be parsed
/// </summary>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <returns>The parsed or default integer value.</returns>
public static TimeSpan ParseTimespanOption ( IReadOnlyDictionary < string , string? > options , string value , string @default )
{
var opt = options . GetValueOrDefault ( value );
if ( string . IsNullOrWhiteSpace ( opt ))
opt = @default ;
return Timeparser . ParseTimeSpan ( opt );
}
/// <summary>
/// Parses an enum found in the options dictionary
/// </summary>
/// <returns>The parsed or default enum value.</returns>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <typeparam name="T">The enum type parameter.</typeparam>
public static T ParseEnumOption < T >( IReadOnlyDictionary < string , string? > options , string value , T @default ) where T : struct , Enum
{
return options . TryGetValue ( value , out var opt ) ? ParseEnum ( opt , @default ) : @default ;
}
/// <summary>
/// Parses a flags-type enum found in the options dictionary
/// </summary>
/// <returns>The parsed or default enum value.</returns>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <typeparam name="T">The enum type parameter.</typeparam>
public static T ParseFlagsOption < T >( IReadOnlyDictionary < string , string? > options , string value , T @default ) where T : struct , Enum
{
2025-10-31 11:38:06 +01:00
return options . TryGetValue ( value , out var opt ) ? ParseFlags ( opt , @default ) : @default ;
2025-09-05 14:06:55 +02:00
}
/// <summary>
/// Attempts to parse an enum with case-insensitive lookup, returning the default value if there was no match
/// </summary>
/// <returns>The parsed or default enum value.</returns>
/// <param name="value">The string to parse.</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <typeparam name="T">The enum type parameter.</typeparam>
public static T ParseEnum < T >( string? value , T @default ) where T : struct , Enum
{
if ( string . IsNullOrWhiteSpace ( value ))
return @default ;
foreach ( var s in Enum . GetNames ( typeof ( T )))
if ( s . Equals ( value , StringComparison . OrdinalIgnoreCase ))
return ( T ) Enum . Parse ( typeof ( T ), s );
return @default ;
}
/// <summary>
/// Parses a string into a flags enum value.
/// </summary>
/// <typeparam name="T">The enum type to parse.</typeparam>
/// <param name="value">The value to parse.</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <returns></returns>
public static T ParseFlags < T >( string? value , T @default ) where T : struct , Enum
{
if ( string . IsNullOrWhiteSpace ( value ))
return @default ;
var flags = 0 ;
foreach ( var s in value . Split ( new [] { ',' }, StringSplitOptions . RemoveEmptyEntries ))
{
var trimmed = s . Trim ();
if ( Enum . TryParse ( trimmed , true , out T flag ))
flags = flags | ( int )( object ) flag ;
}
return ( T )( object ) flags ;
}
2025-09-05 14:07:35 +02:00
/// <summary>
/// Parses an option with int value, returning the default value if the option is not found or cannot be parsed
/// </summary>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="default">default value</param>
/// <returns></returns>
2025-09-05 14:06:55 +02:00
public static int ParseIntOption ( IReadOnlyDictionary < string , string? > options , string value , int @default )
=> options . TryGetValue ( value , out var opt ) && int . TryParse ( opt ?? string . Empty , out var result ) ? result : @default ;
2025-09-05 14:07:35 +02:00
/// <summary>
/// Parses an option with long value, returning the default value if the option is not found or cannot be parsed
/// </summary>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="default">default value</param>
/// <returns></returns>
2025-09-05 14:06:55 +02:00
public static long ParseLongOption ( IReadOnlyDictionary < string , string? > options , string value , long @default )
=> options . TryGetValue ( value , out var opt ) && long . TryParse ( opt ?? string . Empty , out var result ) ? result : @default ;
/// <summary>
/// Parses a size option from the option set, returning the default value if the option is not found or cannot be parsed
/// </summary>
/// <param name="options">The set of options to look for the setting in</param>
/// <param name="value">The value to look for in the settings</param>
/// <param name="defaultMultiplier">Multiplier to use if the value does not have a multiplier</param>
/// <param name="default">The default value to return if there are no matches.</param>
/// <returns>The parsed or default size value.</returns>
public static long ParseSizeOption ( IReadOnlyDictionary < string , string? > options , string value , string defaultMultiplier , string @default )
{
var opt = options . GetValueOrDefault ( value );
if ( string . IsNullOrWhiteSpace ( opt ))
opt = @default ;
return Sizeparser . ParseSize ( opt , defaultMultiplier );
}
/// <summary>
/// Converts a sequence of bytes to a hex string
/// </summary>
/// <returns>The array as hex string.</returns>
/// <param name="data">The data to convert</param>
public static string ByteArrayAsHexString ( byte [] data )
{
return BitConverter . ToString ( data ). Replace ( "-" , string . Empty );
}
/// <summary>
/// Converts a hex string to a byte array
/// </summary>
/// <returns>The string as byte array.</returns>
/// <param name="hex">The hex string</param>
/// <param name="data">The parsed data</param>
public static void HexStringAsByteArray ( string hex , byte [] data )
{
for ( var i = 0 ; i < hex . Length ; i += 2 )
data [ i / 2 ] = Convert . ToByte ( hex . Substring ( i , 2 ), 16 );
}
/// <summary>
/// Converts a hex string to a byte array, as a function so no variable declaration on caller's side is needed
/// </summary>
/// <returns>The string as byte array.</returns>
/// <param name="hex">The hex string</param>
public static byte [] HexStringAsByteArray ( string hex )
{
var data = new byte [ hex . Length / 2 ];
HexStringAsByteArray ( hex , data );
return data ;
}
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
/// <summary>
/// Invokes the "which" command to determine if a given application is available in the path
/// </summary>
/// <param name="appname">The name of the application to look for</param>
public static bool Which ( string appname )
{
if (!( OperatingSystem . IsLinux () || OperatingSystem . IsMacOS ()))
return false ;
try
{
var psi = new System . Diagnostics . ProcessStartInfo ( "which" , appname )
{
RedirectStandardOutput = true ,
RedirectStandardError = false ,
RedirectStandardInput = false ,
UseShellExecute = false
};
var pi = System . Diagnostics . Process . Start ( psi )
?? throw new Exception ( "Unexpected failure to start process" );
pi . WaitForExit ( 5000 );
if ( pi . HasExited )
return pi . ExitCode == 0 ;
else
return false ;
}
catch
{
}
return false ;
}
/// <value>
2025-09-05 14:07:35 +02:00
/// Returns a value indicating if the filesystem, is case sensitive
2025-09-05 14:06:55 +02:00
/// </value>
public static bool IsFSCaseSensitive
{
get
{
if (! CachedIsFSCaseSensitive . HasValue )
{
var str = Environment . GetEnvironmentVariable ( "FILESYSTEM_CASE_SENSITIVE" );
// TODO: This should probably be determined by filesystem rather than OS,
// OSX can actually have the disks formatted as Case Sensitive, but insensitive is default
CachedIsFSCaseSensitive = ParseBool ( str , () => OperatingSystem . IsLinux ());
}
return CachedIsFSCaseSensitive . Value ;
}
}
/// <summary>
/// Gets a string comparer that matches the client filesystems case sensitivity
/// </summary>
public static StringComparer ClientFilenameStringComparer => IsFSCaseSensitive ? StringComparer . Ordinal : StringComparer . OrdinalIgnoreCase ;
/// <summary>
/// Gets the string comparision that matches the client filesystems case sensitivity
/// </summary>
public static StringComparison ClientFilenameStringComparison => IsFSCaseSensitive ? StringComparison . Ordinal : StringComparison . OrdinalIgnoreCase ;
/// <summary>
/// The path to the users home directory
/// </summary>
public static readonly string HOME_PATH = Environment . GetFolderPath (! OperatingSystem . IsWindows () ? Environment . SpecialFolder . Personal : Environment . SpecialFolder . UserProfile );
/// <summary>
/// Regexp for matching environment variables on Windows (%VAR%)
/// </summary>
private static readonly Regex ENVIRONMENT_VARIABLE_MATCHER_WINDOWS = new Regex ( @"\%(?<name>\w+)\%" );
/// <summary>
/// Expands environment variables in a RegExp safe format
/// </summary>
/// <returns>The expanded string.</returns>
/// <param name="str">The string to expand.</param>
/// <param name="lookup">A lookup method that converts an environment key to an expanded string</param>
public static string? ExpandEnvironmentVariablesRegexp ( string? str , Func < string? , string? >? lookup = null )
{
if ( string . IsNullOrWhiteSpace ( str ))
return str ;
if ( lookup == null )
lookup = x => Environment . GetEnvironmentVariable ( x ?? string . Empty );
return
// TODO: Should we switch to using the native format ($VAR or ${VAR}), instead of following the Windows scheme?
// IsClientLinux ? new Regex(@"\$(?<name>\w+)|(\{(?<name>[^\}]+)\})") : ENVIRONMENT_VARIABLE_MATCHER_WINDOWS
ENVIRONMENT_VARIABLE_MATCHER_WINDOWS . Replace ( str , m => Regex . Escape ( lookup ( m . Groups [ "name" ]. Value ) ?? string . Empty ));
}
/// <summary>
/// Normalizes a DateTime instance by converting to UTC and flooring to seconds.
/// </summary>
/// <returns>The normalized date time</returns>
/// <param name="input">The input time</param>
public static DateTime NormalizeDateTime ( DateTime input )
{
var ticks = input . ToUniversalTime (). Ticks ;
ticks -= ticks % TimeSpan . TicksPerSecond ;
return new DateTime ( ticks , DateTimeKind . Utc );
}
/// <summary>
/// Given a DateTime instance, return the number of elapsed seconds since the Unix epoch
/// </summary>
/// <returns>The number of elapsed seconds since the Unix epoch</returns>
/// <param name="input">The input time</param>
public static long NormalizeDateTimeToEpochSeconds ( DateTime input )
{
// Note that we cannot return (new DateTimeOffset(input)).ToUnixTimeSeconds() here.
// The DateTimeOffset constructor will convert the provided DateTime to the UTC
// equivalent. However, if DateTime.MinValue is provided (for example, when creating
// a new backup), this can result in values that fall outside the DateTimeOffset.MinValue
// and DateTimeOffset.MaxValue bounds.
return ( long ) Math . Floor (( NormalizeDateTime ( input ) - EPOCH ). TotalSeconds );
}
/// <summary>
/// The format string for a DateTime
/// </summary>
//Note: Actually the K should be Z which is more correct as it is forced to be Z, but Z as a format specifier is fairly undocumented
public static string SERIALIZED_DATE_TIME_FORMAT = "yyyyMMdd'T'HHmmssK" ;
/// <summary>
/// Returns a string representation of a <see cref="System.DateTime"/> in UTC format
/// </summary>
/// <param name="dt">The <see cref="System.DateTime"/> instance</param>
/// <returns>A string representing the time</returns>
public static string SerializeDateTime ( DateTime dt )
{
return dt . ToUniversalTime (). ToString ( SERIALIZED_DATE_TIME_FORMAT , System . Globalization . CultureInfo . InvariantCulture );
}
/// <summary>
/// Parses a serialized <see cref="System.DateTime"/> instance
/// </summary>
/// <param name="str">The string to parse</param>
/// <returns>The parsed <see cref="System.DateTime"/> instance</returns>
public static bool TryDeserializeDateTime ( string str , out DateTime dt )
{
return DateTime . TryParseExact ( str , SERIALIZED_DATE_TIME_FORMAT , System . Globalization . CultureInfo . InvariantCulture , System . Globalization . DateTimeStyles . AssumeUniversal , out dt );
}
/// <summary>
/// Parses a serialized <see cref="System.DateTime"/> instance
/// </summary>
/// <param name="str">The string to parse</param>
/// <returns>The parsed <see cref="System.DateTime"/> instance</returns>
public static DateTime DeserializeDateTime ( string str )
{
if (! TryDeserializeDateTime ( str , out var dt ))
throw new Exception ( Strings . Utility . InvalidDateError ( str ));
return dt ;
}
/// <summary>
/// Gets the unique items from a collection.
/// </summary>
/// <typeparam name="T">The type of the elements in <paramref name="collection"/>.</typeparam>
/// <param name="collection">The collection to remove duplicate items from.</param>
/// <param name="duplicateItems">The duplicate items in <paramref name="collection"/>.</param>
/// <returns>The unique items from <paramref name="collection"/>.</returns>
public static IList < T > GetUniqueItems < T >( IEnumerable < T > collection , out ISet < T > duplicateItems )
{
return GetUniqueItems ( collection , EqualityComparer < T >. Default , out duplicateItems );
}
/// <summary>
/// Gets the unique items from a collection.
/// </summary>
/// <typeparam name="T">The type of the elements in <paramref name="collection"/>.</typeparam>
/// <param name="collection">The collection to remove duplicate items from.</param>
/// <param name="comparer">The <see cref="System.Collections.Generic.IEqualityComparer{T}"/> implementation to use when comparing values in the collection.</param>
/// <param name="duplicateItems">The duplicate items in <paramref name="collection"/>.</param>
/// <returns>The unique items from <paramref name="collection"/>.</returns>
public static IList < T > GetUniqueItems < T >( IEnumerable < T > collection , IEqualityComparer < T > comparer , out ISet < T > duplicateItems )
{
var uniqueItems = new HashSet < T >( comparer );
var results = new List < T >();
duplicateItems = new HashSet < T >( comparer );
foreach ( var item in collection )
{
if ( uniqueItems . Add ( item ))
results . Add ( item );
else
duplicateItems . Add ( item );
}
return results ;
}
// <summary>
// Returns the entry assembly or reasonable approximation if no entry assembly is available.
// This is the case in NUnit tests. The following approach does not work w/ Mono due to unimplemented members:
// http://social.msdn.microsoft.com/Forums/nb-NO/clr/thread/db44fe1a-3bb4-41d4-a0e0-f3021f30e56f
// so this layer of indirection is necessary
// </summary>
// <returns>entry assembly or reasonable approximation</returns>
public static System . Reflection . Assembly getEntryAssembly ()
{
return System . Reflection . Assembly . GetEntryAssembly () ?? System . Reflection . Assembly . GetExecutingAssembly ();
}
/// <summary>
/// Converts a Base64 encoded string to "base64 for url"
/// See https://en.wikipedia.org/wiki/Base64#URL_applications
/// </summary>
/// <param name="data">The base64 encoded string</param>
/// <returns>The base64 for url encoded string</returns>
public static string Base64PlainToBase64Url ( string data )
{
return data . Replace ( '+' , '-' ). Replace ( '/' , '_' );
}
/// <summary>
/// Converts a "base64 for url" encoded string to a Base64 encoded string.
/// See https://en.wikipedia.org/wiki/Base64#URL_applications
/// </summary>
/// <param name="data">The base64 for url encoded string</param>
/// <returns>The base64 encoded string</returns>
public static string Base64UrlToBase64Plain ( string data )
{
return data . Replace ( '-' , '+' ). Replace ( '_' , '/' );
}
/// <summary>
/// Encodes a byte array into a "base64 for url" encoded string.
/// See https://en.wikipedia.org/wiki/Base64#URL_applications
/// </summary>
/// <param name="data">The data to encode</param>
/// <returns>The base64 for url encoded string</returns>
public static string Base64UrlEncode ( byte [] data )
{
return Base64PlainToBase64Url ( Convert . ToBase64String ( data ));
}
/// <summary>
/// Converts a DateTime instance to a Unix timestamp
/// </summary>
/// <returns>The Unix timestamp.</returns>
/// <param name="input">The DateTime instance to convert.</param>
public static long ToUnixTimestamp ( DateTime input )
{
var ticks = input . ToUniversalTime (). Ticks ;
ticks -= ticks % TimeSpan . TicksPerSecond ;
input = new DateTime ( ticks , DateTimeKind . Utc );
return ( long ) Math . Floor (( input - EPOCH ). TotalSeconds );
}
/// <summary>
/// Returns a value indicating if the given type should be treated as a primitive
/// </summary>
/// <returns><c>true</c>, if type is primitive for serialization, <c>false</c> otherwise.</returns>
/// <param name="t">The type to check.</param>
private static bool IsPrimitiveTypeForSerialization ( Type t )
{
return t . IsPrimitive || t . IsEnum || t == typeof ( string ) || t == typeof ( DateTime ) || t == typeof ( TimeSpan );
}
/// <summary>
/// Writes a primitive to the output, or returns false if the input is not primitive
/// </summary>
/// <returns><c>true</c>, the item was printed, <c>false</c> otherwise.</returns>
/// <param name="item">The item to write.</param>
/// <param name="writer">The target writer.</param>
private static bool PrintSerializeIfPrimitive ( object? item , TextWriter writer )
{
if ( item == null )
{
writer . Write ( "null" );
return true ;
}
if ( IsPrimitiveTypeForSerialization ( item . GetType ()))
{
if ( item is DateTime time )
{
writer . Write ( time . ToLocalTime ());
writer . Write ( " (" );
writer . Write ( ToUnixTimestamp ( time ));
writer . Write ( ")" );
}
else
writer . Write ( item );
return true ;
}
return false ;
}
/// <summary>
/// Prints the object to a stream, which can be used for display or logging
/// </summary>
/// <returns>The serialized object</returns>
/// <param name="item">The object to serialize</param>
/// <param name="writer">The writer to write the results to</param>
/// <param name="filter">A filter applied to properties to decide if they are omitted or not</param>
/// <param name="recurseobjects">A value indicating if non-primitive values are recursed</param>
/// <param name="indentation">The string indentation</param>
/// <param name="visited">A lookup table with visited objects, used to avoid infinite recursion</param>
/// <param name="collectionlimit">The maximum number of items to report from an IEnumerable instance</param>
public static void PrintSerializeObject ( object? item , TextWriter writer , Func < System . Reflection . PropertyInfo , object , bool >? filter = null , bool recurseobjects = false , int indentation = 0 , int collectionlimit = 0 , Dictionary < object , object? >? visited = null )
{
visited = visited ?? new Dictionary < object , object? >();
var indentstring = new string ( ' ' , indentation );
var first = true ;
if ( item == null || IsPrimitiveTypeForSerialization ( item . GetType ()))
{
writer . Write ( indentstring );
if ( PrintSerializeIfPrimitive ( item , writer ))
return ;
}
if ( item == null )
return ;
foreach ( var p in item . GetType (). GetProperties ())
{
if ( filter != null && ! filter ( p , item ))
continue ;
if ( IsPrimitiveTypeForSerialization ( p . PropertyType ))
{
if ( first )
first = false ;
else
writer . WriteLine ();
writer . Write ( "{0}{1}: " , indentstring , p . Name );
PrintSerializeIfPrimitive ( p . GetValue ( item , null ), writer );
}
else if ( typeof ( Task ). IsAssignableFrom ( p . PropertyType ) || p . Name == "TaskReader" )
{
// Ignore Task items
continue ;
}
else if ( typeof ( System . Collections . IEnumerable ). IsAssignableFrom ( p . PropertyType ))
{
var enumerable = p . GetValue ( item , null ) as System . Collections . IEnumerable ;
var any = false ;
if ( enumerable != null )
{
var enumerator = enumerable . GetEnumerator ();
if ( enumerator != null )
{
var remain = collectionlimit ;
if ( first )
first = false ;
else
writer . WriteLine ();
writer . Write ( "{0}{1}: [" , indentstring , p . Name );
if ( enumerator . MoveNext ())
{
any = true ;
writer . WriteLine ();
PrintSerializeObject ( enumerator . Current , writer , filter , recurseobjects , indentation + 4 , collectionlimit , visited );
remain --;
while ( enumerator . MoveNext ())
{
writer . WriteLine ( "," );
if ( remain == 0 )
{
writer . Write ( "..." );
break ;
}
PrintSerializeObject ( enumerator . Current , writer , filter , recurseobjects , indentation + 4 , collectionlimit , visited );
remain --;
}
}
if ( any )
{
writer . WriteLine ();
writer . Write ( indentstring );
}
writer . Write ( "]" );
}
}
}
else if ( recurseobjects )
{
var value = p . GetValue ( item , null );
if ( value == null )
{
if ( first )
first = false ;
else
writer . WriteLine ();
writer . Write ( "{0}{1}: null" , indentstring , p . Name );
}
else if (! visited . ContainsKey ( value ))
{
if ( first )
first = false ;
else
writer . WriteLine ();
writer . WriteLine ( "{0}{1}:" , indentstring , p . Name );
visited [ value ] = null ;
PrintSerializeObject ( value , writer , filter , recurseobjects , indentation + 4 , collectionlimit , visited );
}
}
}
writer . Flush ();
}
/// <summary>
/// Returns a string representing the object, which can be used for display or logging
/// </summary>
/// <returns>The serialized object</returns>
/// <param name="item">The object to serialize</param>
/// <param name="filter">A filter applied to properties to decide if they are omitted or not</param>
/// <param name="recurseobjects">A value indicating if non-primitive values are recursed</param>
/// <param name="indentation">The string indentation</param>
/// <param name="collectionlimit">The maximum number of items to report from an IEnumerable instance, set to zero or less for reporting all</param>
public static StringBuilder PrintSerializeObject ( object? item , StringBuilder ? sb = null , Func < System . Reflection . PropertyInfo , object , bool >? filter = null , bool recurseobjects = false , int indentation = 0 , int collectionlimit = 10 )
{
sb = sb ?? new StringBuilder ();
using ( var sw = new StringWriter ( sb ))
PrintSerializeObject ( item , sw , filter , recurseobjects , indentation , collectionlimit );
return sb ;
}
/// <summary>
/// Repeatedly hash a value with a salt.
/// This effectively masks the original value,
/// and destroys lookup methods, like rainbow tables
/// </summary>
/// <param name="data">The data to hash</param>
/// <param name="salt">The salt to apply</param>
/// <param name="repeats">The number of times to repeat the hashing</param>
/// <returns>The salted hash</returns>
public static byte [] RepeatedHashWithSalt ( string data , string salt , int repeats = 1200 )
{
return RepeatedHashWithSalt (
Encoding . UTF8 . GetBytes ( data ?? "" ),
Encoding . UTF8 . GetBytes ( salt ?? "" ),
repeats );
}
/// <summary>
/// Repeatedly hash a value with a salt.
/// This effectively masks the original value,
/// and destroys lookup methods, like rainbow tables
/// </summary>
/// <param name="data">The data to hash</param>
/// <param name="salt">The salt to apply</param>
/// <returns>The salted hash</returns>
public static byte [] RepeatedHashWithSalt ( byte [] data , byte [] salt , int repeats = 1200 )
{
2025-09-05 14:07:35 +02:00
// We avoid storing the passphrase directly,
2025-09-05 14:06:55 +02:00
// instead we salt and rehash repeatedly
using ( var h = System . Security . Cryptography . SHA256 . Create ())
{
h . Initialize ();
h . TransformBlock ( salt , 0 , salt . Length , salt , 0 );
h . TransformFinalBlock ( data , 0 , data . Length );
var buf = h . Hash ?? throw new CryptographicUnexpectedOperationException ( "Computed hash was null?" );
for ( var i = 0 ; i < repeats ; i ++)
{
h . Initialize ();
h . TransformBlock ( salt , 0 , salt . Length , salt , 0 );
h . TransformFinalBlock ( buf , 0 , buf . Length );
buf = h . Hash ;
}
return buf ;
}
}
/// <summary>
/// Gets the drive letter from the given volume guid.
/// This method cannot be inlined since the System.Management types are not implemented in Mono
/// </summary>
/// <param name="volumeGuid">Volume guid</param>
/// <returns>Drive letter, as a single character, or null if the volume wasn't found</returns>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
public static string? GetDriveLetterFromVolumeGuid ( Guid volumeGuid )
{
// Based on this answer:
// https://stackoverflow.com/questions/10186277/how-to-get-drive-information-by-volume-id
using ( var searcher = new System . Management . ManagementObjectSearcher ( "Select * from Win32_Volume" ))
{
string targetId = string . Format ( @"\\?\Volume{{{0}}}\" , volumeGuid );
foreach ( var obj in searcher . Get ())
{
if ( string . Equals ( obj [ "DeviceID" ]. ToString (), targetId , StringComparison . OrdinalIgnoreCase ))
{
object driveLetter = obj [ "DriveLetter" ];
if ( driveLetter != null )
{
return obj [ "DriveLetter" ]. ToString ();
}
else
{
// The volume was found, but doesn't have a drive letter associated with it.
break ;
}
}
}
return null ;
}
}
/// <summary>
/// Gets all volume guids and their associated drive letters.
/// This method cannot be inlined since the System.Management types are not implemented in Mono
/// </summary>
/// <returns>Pairs of drive letter to volume guids</returns>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
public static IEnumerable < KeyValuePair < string , string >> GetVolumeGuidsAndDriveLetters ()
{
using ( var searcher = new System . Management . ManagementObjectSearcher ( "Select * from Win32_Volume" ))
{
foreach ( var obj in searcher . Get ())
{
var deviceIdObj = obj [ "DeviceID" ];
var driveLetterObj = obj [ "DriveLetter" ];
if ( deviceIdObj != null && driveLetterObj != null )
{
var deviceId = deviceIdObj . ToString ();
var driveLetter = driveLetterObj . ToString ();
if (! string . IsNullOrEmpty ( deviceId ) && ! string . IsNullOrEmpty ( driveLetter ))
{
yield return new KeyValuePair < string , string >( driveLetter + @"\" , deviceId );
}
}
}
}
}
/// <summary>
/// The regular expression matching all know non-quoted commandline characters
/// </summary>
private static readonly Regex COMMANDLINE_SAFE = new Regex ( @"[A-Za-z0-9\-_/:\.]*" );
/// <summary>
/// Special characters that needs to be escaped on Linux
/// </summary>
private static readonly Regex COMMANDLINE_ESCAPED_LINUX = new Regex ( @"[""$`\\!]" );
/// <summary>
/// Wraps a single argument in quotes suitable for the passing on the commandline
/// </summary>
/// <returns>The wrapped commandline element.</returns>
/// <param name="arg">The argument to wrap.</param>
/// <param name="allowEnvExpansion">A flag indicating if environment variables are allowed to be expanded</param>
[return: NotNullIfNotNull("arg")]
public static string? WrapCommandLineElement ( string? arg , bool allowEnvExpansion )
{
if ( string . IsNullOrWhiteSpace ( arg ))
return arg ;
if (! OperatingSystem . IsWindows ())
{
// We could consider using single quotes that prevents all expansions
//if (!allowEnvExpansion)
// return "'" + arg.Replace("'", "\\'") + "'";
// Linux is using backslash to escape, except for !
arg = COMMANDLINE_ESCAPED_LINUX . Replace ( arg , ( match ) =>
{
if ( match . Value == "!" )
return @"""'!'""" ;
if ( match . Value == "$" && allowEnvExpansion )
return match . Value ;
return @"\" + match . Value ;
});
}
else
{
// Windows needs only needs " replaced with "",
2025-09-05 14:07:35 +02:00
// but is prone to %var% expansion when used in
2025-09-05 14:06:55 +02:00
// immediate mode (i.e. from command prompt)
// Fortunately it does not expand when processes
// are started from within .Net
// TODO: I have not found a way to avoid escaping %varname%,
// and sadly it expands only if the variable exists
// making it even rarer and harder to diagnose when
// it happens
arg = arg . Replace ( @"""" , @"""""" );
// Also fix the case where the argument ends with a slash
if ( arg [ arg . Length - 1 ] == '\\' )
arg += @"\" ;
}
// Check that all characters are in the safe set
if ( COMMANDLINE_SAFE . Match ( arg ). Length != arg . Length )
return @"""" + arg + @"""" ;
else
return arg ;
}
/// <summary>
/// Wrap a set of commandline arguments suitable for the commandline
/// </summary>
/// <returns>A commandline string.</returns>
/// <param name="args">The arguments to create into a commandline.</param>
/// <param name="allowEnvExpansion">A flag indicating if environment variables are allowed to be expanded</param>
public static string WrapAsCommandLine ( IEnumerable < string > args , bool allowEnvExpansion = false )
{
return string . Join ( " " , args . Select ( x => WrapCommandLineElement ( x , allowEnvExpansion )));
}
/// <summary>
/// Utility method that emulates C#'s built in await keyword without requiring the calling method to be async.
/// This method should be preferred over using Task.Result, as it doesn't wrap singular exceptions in AggregateExceptions.
/// (It uses Task.GetAwaiter().GetResult(), which is the same thing that await uses under the covers.)
/// https://stackoverflow.com/questions/17284517/is-task-result-the-same-as-getawaiter-getresult
/// </summary>
/// <param name="task">Task to await</param>
public static void Await ( this Task task )
{
task . ConfigureAwait ( false ). GetAwaiter (). GetResult ();
}
/// <summary>
/// Utility method that emulates C#'s built in await keyword without requiring the calling method to be async.
/// This method should be preferred over using Task.Result, as it doesn't wrap singular exceptions in AggregateExceptions.
/// (It uses Task.GetAwaiter().GetResult(), which is the same thing that await uses under the covers.)
/// https://stackoverflow.com/questions/17284517/is-task-result-the-same-as-getawaiter-getresult
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="task">Task to await</param>
/// <returns>Task result</returns>
public static T Await < T >( this Task < T > task )
{
return task . ConfigureAwait ( false ). GetAwaiter (). GetResult ();
}
2025-12-09 21:17:01 +01:00
/// <summary>
/// Utility method that emulates C#'s built in await keyword without requiring the calling method to be async.
/// This method should be preferred over using Task.Result, as it doesn't wrap singular exceptions in AggregateExceptions.
/// (It uses Task.GetAwaiter().GetResult(), which is the same thing that await uses under the covers.)
/// https://stackoverflow.com/questions/17284517/is-task-result-the-same-as-getawaiter-getresult
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="task">Task to await</param>
/// <returns>Task result</returns>
public static T Await < T >( this ValueTask < T > task )
{
return task . ConfigureAwait ( false ). GetAwaiter (). GetResult ();
}
2025-09-05 14:06:55 +02:00
/// <summary>
/// Utility that computes the delay before the next retry of an operation, optionally using exponential backoff.
/// Note: when using exponential backoff, the exponent is clamped at 10.
/// </summary>
/// <param name="retryDelay">Value of one delay unit</param>
/// <param name="retryAttempt">The attempt number (e.g. 1 for the first retry, 2 for the second retry, etc.)</param>
/// <param name="useExponentialBackoff">Whether to use exponential backoff</param>
/// <returns>The computed delay</returns>
public static TimeSpan GetRetryDelay ( TimeSpan retryDelay , int retryAttempt , bool useExponentialBackoff )
{
if ( retryAttempt < 1 )
{
throw new ArgumentException ( "The attempt number must not be less than 1." , nameof ( retryAttempt ));
}
TimeSpan delay ;
if ( useExponentialBackoff )
{
var delayTicks = retryDelay . Ticks << Math . Min ( retryAttempt - 1 , 10 );
delay = TimeSpan . FromTicks ( delayTicks );
}
else
{
delay = retryDelay ;
}
return delay ;
}
/// <summary>
/// Loads the pfxcertificate from bytes into an exportable format.
/// </summary>
/// <remarks>This method masks a problem with loading certificates with EC based keys by using a temporary file</remarks>
/// <param name="pfxcertificate">The certificate as a byte array</param>
/// <param name="password">The password used to protect the PFX file</param>
/// <param name="allowUnsafeCertificateLoad">A flag indicating if unsafe certificate loading is allowed</param>
/// <returns>The loaded certificate</returns>
public static X509Certificate2Collection LoadPfxCertificate ( ReadOnlySpan < byte > pfxcertificate , string? password , bool allowUnsafeCertificateLoad = false )
{
if ( string . IsNullOrWhiteSpace ( password ) && ! allowUnsafeCertificateLoad )
throw new ArgumentException ( "Refusing to write unencryped certificate to disk" );
using var tempfile = new TempFile ();
File . WriteAllBytes ( tempfile , pfxcertificate . ToArray ());
return LoadPfxCertificate ( tempfile , password );
}
/// <summary>
/// Loads a PFX certificate from a file into an exportable format.
/// </summary>
/// <param name="pfxPath">The path to the file</param>
/// <param name="password">The password used to protect the PFX file</param>
/// <returns>The loaded certificate</returns>
public static X509Certificate2Collection LoadPfxCertificate ( string pfxPath , string? password )
{
if ( string . IsNullOrEmpty ( pfxPath ))
throw new ArgumentNullException ( nameof ( pfxPath ));
if (! File . Exists ( pfxPath ))
throw new FileNotFoundException ( "The specified PFX file does not exist." , pfxPath );
2025-11-14 16:09:05 +01:00
return X509CertificateLoader . LoadPkcs12CollectionFromFile (
pfxPath ,
password ,
X509KeyStorageFlags . Exportable | X509KeyStorageFlags . PersistKeySet );
2025-09-05 14:06:55 +02:00
}
/// <summary>
/// Probes the system for the presence of a loopback address on IPv4
/// </summary>
public static bool HasIPv4Loopback =>
NetworkInterface . GetAllNetworkInterfaces ()
. Where ( ni => ni . OperationalStatus == OperationalStatus . Up )
. SelectMany ( ni => ni . GetIPProperties (). UnicastAddresses )
. Any ( addr => addr . Address . AddressFamily == AddressFamily . InterNetwork
&& addr . Address . Equals ( IPAddress . Loopback ));
/// <summary>
/// On systems that have IPV4 and IPV6, the method will return the default loopback ( 127, 0, 0, 1)
/// On systems with IPV6 only, the method will return the IPV6 loopback (::1)
/// </summary>
/// <returns></returns>
public static string IpVersionCompatibleLoopback =>
HasIPv4Loopback ? IPAddress . Loopback . ToString () : $"[{IPAddress.IPv6Loopback.ToString()}]" ;
/// <summary>
/// Guesses the URL scheme and returns it
/// </summary>
/// <param name="url">The URL to guess the scheme for</param>
/// <returns>The guessed scheme, or null if no scheme was found</returns>
public static string? GuessScheme ( string url )
{
if ( string . IsNullOrWhiteSpace ( url ))
return null ;
var idx = url . IndexOf ( "://" );
2026-02-07 16:13:52 +01:00
if ( idx < 0 || idx > 15 )
2025-09-05 14:06:55 +02:00
return null ;
2026-02-07 16:13:52 +01:00
return url [.. idx ];
2025-09-05 14:06:55 +02:00
}
/// <summary>
/// Returns a url that is safe to display, by removing any credentials
/// </summary>
/// <param name="url">The url to sanitize</param>
/// <returns>The sanitized url</returns>
public static string GetUrlWithoutCredentials ( string url )
{
// Assumed safe part of the url to show
const int maxShown = 25 ;
if ( string . IsNullOrWhiteSpace ( url ))
return url ;
// Use a reportable url without credentials
var sepIndex = Math . Max ( 0 , url . IndexOf ( '|' )) + 1 ;
var length = url . Length - sepIndex ;
var shown = Math . Min ( length , maxShown );
2026-01-06 09:48:23 +01:00
var hidden = length - shown ;
2025-09-05 14:06:55 +02:00
var sanitizedUrl = $"{url[sepIndex..(sepIndex + shown)]}{new string('*', hidden)}" ;
// If we can parse it, this result is better
try
{
var uri = new Uri ( url [ sepIndex ..]);
sanitizedUrl = new Uri ( $"{uri.Scheme}://{uri.Host}" ). SetPath ( uri . Path ). ToString ();
}
catch
{
}
return sanitizedUrl ;
}
/// <summary>
/// Formats the string using the invariant culture
/// </summary>
/// <param name="formattable">The formattable string</param>
/// <returns>The formatted string</returns>
2025-09-25 11:33:56 +02:00
public static string FormatInvariantFormattable ( FormattableString formattable )
2025-09-05 14:06:55 +02:00
=> formattable . ToString ( CultureInfo . InvariantCulture );
2025-09-05 14:11:05 +02:00
/// <summary>
/// Formats the value using the invariant culture.
/// </summary>
/// <remarks>This is a shortcut for FormatInvariant("{0}", value).</remarks>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="value">The value to format.</param>
/// <returns>The formatted string.</returns>
2025-09-25 11:33:56 +02:00
public static string FormatInvariantValue < T >( T value )
2025-09-25 11:14:54 +02:00
=> value is IFormattable f
? f . ToString ( null , CultureInfo . InvariantCulture )
: string . Format ( CultureInfo . InvariantCulture , "{0}" , value );
2025-09-05 14:11:05 +02:00
2025-09-05 14:06:55 +02:00
/// <summary>
/// Performs the function with an additional timeout
/// </summary>
/// <param name="timeout">The timeout to observe</param>
/// <param name="token">The cancellation token</param>
/// <param name="func">The function to invoke</param>
/// <returns>The task</returns>
public static async Task WithTimeout ( TimeSpan timeout , CancellationToken token , Action < CancellationToken > func )
{
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( token );
cts . CancelAfter ( timeout );
try
{
await Task . Run (() => func ( cts . Token ), cts . Token ). ConfigureAwait ( false );
}
catch ( TaskCanceledException )
{
if ( cts . IsCancellationRequested )
throw new TimeoutException ();
throw ;
}
}
/// <summary>
/// Performs the function with an additional timeout
/// </summary>
/// <param name="timeout">The timeout to observe</param>
/// <param name="token">The cancellation token</param>
/// <param name="func">The function to invoke</param>
/// <returns>The task</returns>
public static async Task < T > WithTimeout < T >( TimeSpan timeout , CancellationToken token , Func < CancellationToken , T > func )
{
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( token );
cts . CancelAfter ( timeout );
try
{
return await Task . Run (() => func ( cts . Token ), cts . Token ). ConfigureAwait ( false );
}
catch ( TaskCanceledException )
{
if ( cts . IsCancellationRequested )
throw new TimeoutException ();
throw ;
}
}
/// <summary>
/// Performs the function with an additional timeout
/// </summary>
/// <param name="timeout">The timeout to observe</param>
/// <param name="token">The cancellation token</param>
/// <param name="func">The function to invoke</param>
/// <returns>The task</returns>
public static async Task WithTimeout ( TimeSpan timeout , CancellationToken token , Func < CancellationToken , Task > func )
{
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( token );
cts . CancelAfter ( timeout );
try
{
await func ( cts . Token );
}
catch ( TaskCanceledException )
{
if ( cts . IsCancellationRequested )
throw new TimeoutException ();
throw ;
}
}
/// <summary>
/// Performs the function with an additional timeout
/// </summary>
/// <typeparam name="T">The return type</typeparam>
/// <param name="timeout">The timeout to observe</param>
/// <param name="token">The cancellation token</param>
/// <param name="func">The function to invoke</param>
/// <returns>The task</returns>
public static async Task < T > WithTimeout < T >( TimeSpan timeout , CancellationToken token , Func < CancellationToken , Task < T >> func )
{
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( token );
cts . CancelAfter ( timeout );
try
{
return await func ( cts . Token );
}
catch ( TaskCanceledException )
{
if ( cts . IsCancellationRequested )
throw new TimeoutException ();
throw ;
}
}
/// <summary>
/// Wraps an async enumerable in a timeout observing enumerable
/// </summary>
/// <typeparam name="T">The type of the items in the enumerable</typeparam>
/// <param name="source">The source enumerable</param>
/// <param name="timeoutPerItem">The timeout to observe for each item</param>
/// <param name="outerToken">The cancellation token for the outer operation</param>
/// <returns>The wrapped enumerable</returns>
public static async IAsyncEnumerable < T > WithPerItemTimeout < T >(
IAsyncEnumerable < T > source ,
TimeSpan timeoutPerItem ,
[EnumeratorCancellation] CancellationToken outerToken )
{
using var timeoutPolicy = new CancellationTokenSource ();
using var linked = CancellationTokenSource . CreateLinkedTokenSource ( timeoutPolicy . Token , outerToken );
await using var enumerator = await WithTimeout ( timeoutPerItem , outerToken , _ => source . GetAsyncEnumerator ( linked . Token )). ConfigureAwait ( false );
while ( true )
{
timeoutPolicy . CancelAfter ( timeoutPerItem );
Task < bool > moveNextTask ;
try
{
moveNextTask = enumerator . MoveNextAsync (). AsTask ();
var completed = await Task . WhenAny ( moveNextTask , Task . Delay ( Timeout . Infinite , timeoutPolicy . Token ));
if ( completed != moveNextTask || timeoutPolicy . IsCancellationRequested )
throw new TimeoutException ( $"Timeout while waiting for next item ({timeoutPerItem.TotalSeconds}s)" );
if (! moveNextTask . Result )
break ;
yield return enumerator . Current ;
}
finally
{
timeoutPolicy . Token . ThrowIfCancellationRequested ();
timeoutPolicy . CancelAfter ( Timeout . Infinite ); // Reset for next item
}
}
}
/// <summary>
/// Wraps the stream in a timeout observing stream
/// </summary>
/// <param name="stream">The stream to wrap</param>
/// <param name="timeout">The timeout to observe</param>
/// <param name="disposeBaseStream">A flag indicating if the base stream should be disposed</param>
/// <returns>The wrapped stream</returns>
public static Stream ObserveReadTimeout ( this Stream stream , TimeSpan timeout , bool disposeBaseStream = true )
{
if ( stream == null )
throw new ArgumentNullException ( nameof ( stream ));
return new TimeoutObservingStream ( stream , disposeBaseStream )
{
ReadTimeout = timeout . Ticks > 0 && timeout != Timeout . InfiniteTimeSpan
? ( int ) timeout . TotalMilliseconds
: Timeout . Infinite
};
}
/// <summary>
/// Wraps the stream in a timeout observing stream
/// </summary>
/// <param name="stream">The stream to wrap</param>
/// <param name="timeout">The timeout to observe</param>
/// <param name="disposeBaseStream">A flag indicating if the base stream should be disposed</param>
/// <returns>The wrapped stream</returns>
public static Stream ObserveWriteTimeout ( this Stream stream , TimeSpan timeout , bool disposeBaseStream = true )
{
if ( stream == null )
throw new ArgumentNullException ( nameof ( stream ));
return new TimeoutObservingStream ( stream , disposeBaseStream )
{
WriteTimeout = timeout . Ticks > 0 && timeout != Timeout . InfiniteTimeSpan
? ( int ) timeout . TotalMilliseconds
: Timeout . Infinite
};
}
/// <summary>
/// The types of streams that are considered basic (i.e. not wrapped)
/// </summary>
private static readonly IReadOnlySet < Type > _basicStreamTypes = new HashSet < Type >
{
typeof ( FileStream ),
typeof ( MemoryStream ),
typeof ( NetworkStream ),
typeof ( BufferedStream ),
typeof ( System . IO . Compression . DeflateStream ),
typeof ( System . IO . Compression . GZipStream ),
typeof ( System . IO . Compression . ZLibStream )
};
/// <summary>
/// Unwraps a stream from layers of wrapping streams
/// </summary>
/// <param name="stream">The stream to unwrap</param>
/// <returns>The unwrapped stream</returns>
public static Stream UnwrapThrottledStream ( this Stream stream )
{
if ( stream == null )
throw new ArgumentNullException ( nameof ( stream ));
var previousStream = stream ;
do
{
previousStream = stream ;
while ( stream is WrappingStream wrappingStream )
stream = wrappingStream . BaseStream ;
while ( stream is OverrideableStream overrideableStream )
stream = overrideableStream . BaseStream ;
} while ( stream != previousStream );
#if DEBUG
if (! _basicStreamTypes . Contains ( stream . GetType ()))
throw new InvalidOperationException ( $"The unwrapped stream is not a basic stream, but a {stream.GetType()}" );
#endif
return stream ;
}
/// <summary>
/// Calculates the hash of a throttled stream and returns the stream to read
/// </summary>
/// <param name="stream">The source stream
/// <param name="hashalgorithm">The hash algorithm to use</param>
/// <param name="cancelToken">The cancellation token to observe</param>
/// <returns>A tuple with the stream, the hash and a temporary file if used</returns>
public static async Task <( Stream content , string hash , TempFile ? tmpfile )> CalculateThrottledStreamHash ( Stream stream , string hashalgorithm , CancellationToken cancelToken )
{
var res = await CalculateThrottledStreamHash ( stream , new [] { hashalgorithm }, cancelToken ). ConfigureAwait ( false );
return ( res . content , res . hashes . First (), res . tmpfile );
}
/// <summary>
/// Calculates the hash of a throttled stream and returns the stream to read
/// </summary>
/// <param name="stream">The source stream
/// <param name="hashalgorithms">The hash algorithms to use</param>
/// <param name="cancelToken">The cancellation token to observe</param>
/// <returns>A tuple with the stream, the hash and a temporary file if used</returns>
public static async Task <( Stream content , string [] hashes , TempFile ? tmpfile )> CalculateThrottledStreamHash ( Stream stream , IEnumerable < string > hashalgorithms , CancellationToken cancelToken )
{
TempFile ? tmp = null ;
List < HashAlgorithm >? hashalgs = null ;
try
{
string [] contentHashes ;
var measure = stream . UnwrapThrottledStream ();
if (! measure . CanSeek )
tmp = new TempFile ();
var p = measure . Position ;
hashalgs = hashalgorithms . Select ( ha => HashFactory . CreateHasher ( ha )). ToList ();
await using ( var sr = tmp == null ? null : File . OpenWrite ( tmp ))
{
int read ;
var buffer = new byte [ 1024 * 8 ];
while (( read = await measure . ReadAsync ( buffer , 0 , buffer . Length , cancelToken ). ConfigureAwait ( false )) > 0 )
{
// Feed the data to all hashers
foreach ( var hasher in hashalgs )
hasher . TransformBlock ( buffer , 0 , read , null , 0 );
if ( sr != null )
await sr . WriteAsync ( buffer , 0 , read , cancelToken ). ConfigureAwait ( false );
}
}
// Finalize the hashes
foreach ( var hasher in hashalgs )
hasher . TransformFinalBlock ( Array . Empty < byte >(), 0 , 0 );
// Get the final hash strings
contentHashes = hashalgs . Select ( hashalg => ByteArrayAsHexString ( hashalg . Hash !)). ToArray ();
foreach ( var hasher in hashalgs )
hasher . Dispose ();
// Reset the position of the stream if it was seekable
if ( measure . CanSeek )
measure . Position = p ;
return ( tmp == null ? stream : File . OpenRead ( tmp ), contentHashes , tmp );
}
catch
{
tmp ?. Dispose ();
throw ;
}
finally
{
if ( hashalgs != null )
foreach ( var hasher in hashalgs )
hasher . Dispose ();
}
}
2025-11-30 23:01:57 +01:00
/// <summary>
/// Reads a secret value from the console, masking the input.
/// </summary>
/// <param name="prompt">The prompt to display.</param>
/// <returns>The value entered by the user.</returns>
/// <remarks>
/// Behavior:
/// - Input is masked with '*'.
/// - Backspace removes the last character and erases the mask.
/// - Pressing Escape cancels input and throws <see cref="Interface.CancelException"/>.
/// - Control characters (other than Enter/Escape/Backspace) are ignored and not added to the value.
/// </remarks>
public static string ReadSecretFromConsole ( string prompt )
{
Console . Write ( prompt );
var value = string . Empty ;
while ( true )
{
var keyInfo = Console . ReadKey ( true );
// Allow cancellation with ESC
if ( keyInfo . Key == ConsoleKey . Escape )
{
throw new Interface . CancelException ( "Console secret input canceled by user." );
}
if ( keyInfo . Key == ConsoleKey . Enter )
{
break ;
}
if ( keyInfo . Key == ConsoleKey . Backspace )
{
if ( value . Length > 0 )
{
value = value . Substring ( 0 , value . Length - 1 );
Console . Write ( "\b \b" );
}
}
else if ( keyInfo . KeyChar != '\ u0000 ' && ! char . IsControl ( keyInfo . KeyChar ))
{
// Only accept printable characters; ignore other control characters
value += keyInfo . KeyChar ;
Console . Write ( "*" );
}
}
Console . WriteLine ();
return value ;
}
2026-02-06 10:37:27 +01:00
/// <summary>
/// Gets the free and total space available for the specified path.
/// Uses the same approach as FileBackend.GetQuotaInfoAsync.
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined</returns>
public static ( long FreeSpace , long TotalSpace )? GetFreeSpaceForPath ( string path )
{
try
{
// Get the drive info for the path
var root = Path . GetPathRoot ( path );
if ( string . IsNullOrEmpty ( root ))
return null ;
// On Windows, DriveInfo is only valid for lettered drives
if ( OperatingSystem . IsWindows () && root . Length > 0 && char . IsLetter ( root [ 0 ]))
{
try
{
var driveInfo = new DriveInfo ( root );
if ( driveInfo . TotalSize > 0 )
return ( driveInfo . AvailableFreeSpace , driveInfo . TotalSize );
}
catch ( ArgumentException )
{
// Fall through to Win32 API fallback
}
// Fallback to Win32 API for UNC paths and other cases
return GetDiskFreeSpaceWin32 ( path );
}
else if (! OperatingSystem . IsWindows ())
{
var driveInfo = new DriveInfo ( root );
if ( driveInfo . TotalSize > 0 )
return ( driveInfo . AvailableFreeSpace , driveInfo . TotalSize );
}
else
{
// Windows but not a lettered drive (e.g., UNC path)
return GetDiskFreeSpaceWin32 ( path );
}
}
catch
{
// Ignore errors and return null
}
return null ;
}
/// <summary>
/// Gets the free and total disk space using the Win32 API's GetDiskFreeSpaceEx function.
/// </summary>
/// <param name="directory">Directory</param>
/// <returns>A tuple with (freeSpace, totalSpace) in bytes, or null if it could not be determined</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
private static ( long FreeSpace , long TotalSpace )? GetDiskFreeSpaceWin32 ( string directory )
{
if (! OperatingSystem . IsWindows ())
return null ;
try
{
if ( GetDiskFreeSpaceEx ( directory , out var available , out var total , out _ ))
return (( long ) available , ( long ) total );
}
catch
{
// Ignore errors
}
return null ;
}
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
[SupportedOSPlatform("windows")]
private static extern bool GetDiskFreeSpaceEx (
string lpDirectoryName ,
out ulong lpFreeBytesAvailable ,
out ulong lpTotalNumberOfBytes ,
out ulong lpTotalNumberOfFreeBytes );
2025-09-05 14:06:55 +02:00
}
}