Skip to content

Latest commit

 

History

History
966 lines (772 loc) · 27.1 KB

File metadata and controls

966 lines (772 loc) · 27.1 KB

PowerCSharp.Helpers - Detailed Documentation

Overview

PowerCSharp.Helpers provides specialized helper classes for cryptography, JSON operations, and environment management. These focused utilities offer secure and reliable implementations of common programming tasks with comprehensive error handling.

Recent Improvements (v0.2.0):

  • Enhanced Security: Improved cryptographic implementations with better error handling
  • Performance Gains: Optimized JSON serialization and deserialization
  • Better Documentation: Comprehensive usage examples and security best practices
  • Environment Support: Enhanced environment variable management with fallback values

Architecture

Design Principles

  1. Security First: Cryptographic operations follow security best practices
  2. Error Resilience: All methods handle exceptions gracefully
  3. Performance Optimized: Efficient implementations for common scenarios
  4. Thread Safety: All operations are thread-safe when used correctly
  5. Zero Dependencies: Minimal external dependencies for maximum compatibility

Namespace Structure

PowerCSharp.Helpers/
├── CryptoHelper.cs
├── JsonHelper.cs
├── EnvironmentHelper.cs
└── (future helpers)

Class Documentation

CryptoHelper

Provides secure cryptographic operations for hashing and random string generation.

Security Considerations

  • SHA256: Used for secure hashing and data integrity
  • MD5: Provided only for legacy compatibility (not cryptographically secure)
  • Random Generation: Uses cryptographically secure random number generation
  • No Key Storage: No sensitive data is stored in memory longer than necessary

Methods

ComputeSHA256
public static string ComputeSHA256(string input)

Purpose: Computes SHA256 hash of a string using secure cryptographic algorithms.

Parameters:

  • input: The input string to hash

Returns: Base64 encoded SHA256 hash

Security Notes:

  • Uses System.Security.Cryptography.SHA256
  • Input is encoded using UTF-8
  • Output is Base64 encoded for safe transport
  • Suitable for password hashing with proper salting

Examples:

// Basic usage
string password = "userPassword123";
string hash = CryptoHelper.ComputeSHA256(password);
Console.WriteLine($"SHA256: {hash}");

// In a secure authentication system
public class SecurePasswordManager
{
    private readonly string _pepper;
    
    public SecurePasswordManager()
    {
        // Generate a server-wide pepper (store this securely)
        _pepper = CryptoHelper.GenerateRandomString(32);
    }
    
    public string HashPassword(string password, string salt)
    {
        // Combine password with salt and pepper for enhanced security
        string combined = $"{password}{salt}{_pepper}";
        return CryptoHelper.ComputeSHA256(combined);
    }
    
    public bool VerifyPassword(string password, string salt, string storedHash)
    {
        string computedHash = HashPassword(password, salt);
        return computedHash.Equals(storedHash, StringComparison.OrdinalIgnoreCase);
    }
}
ComputeMD5
public static string ComputeMD5(string input)

Purpose: Computes MD5 hash of a string (for legacy compatibility only).

Parameters:

  • input: The input string to hash

Returns: Hexadecimal MD5 hash

Security Notes:

  • NOT CRYPTographically SECURE - Use only for legacy compatibility
  • Suitable for file checksums and non-security applications
  • Do not use for password hashing or security-critical operations

Examples:

// Basic usage (legacy compatibility only)
string data = "test data";
string md5Hash = CryptoHelper.ComputeMD5(data);
Console.WriteLine($"MD5: {md5Hash}");

// In a file integrity checker (non-security use)
public class FileIntegrityChecker
{
    public string CalculateFileChecksum(string filePath)
    {
        string content = File.ReadAllText(filePath);
        return CryptoHelper.ComputeMD5(content);
    }
    
    public bool VerifyFileIntegrity(string filePath, string expectedChecksum)
    {
        try
        {
            string actualChecksum = CalculateFileChecksum(filePath);
            return actualChecksum.Equals(expectedChecksum, StringComparison.OrdinalIgnoreCase);
        }
        catch
        {
            return false;
        }
    }
}
GenerateRandomString
public static string GenerateRandomString(int length)

Purpose: Generates a cryptographically secure random string.

Parameters:

  • length: The desired length of the random string

Returns: Random alphanumeric string

Security Notes:

  • Uses System.Random with time-based seeding
  • Contains uppercase, lowercase, and digits
  • Suitable for session tokens, temporary passwords, etc.
  • Not suitable for cryptographic keys (use proper crypto libraries)

Examples:

// Basic usage
string randomString = CryptoHelper.GenerateRandomString(16);
Console.WriteLine($"Random: {randomString}");

// In a token generation system
public class TokenGenerator
{
    public string GenerateSessionToken()
    {
        return CryptoHelper.GenerateRandomString(64);
    }
    
    public string GenerateTemporaryPassword()
    {
        return CryptoHelper.GenerateRandomString(12);
    }
    
    public string GenerateApiKey()
    {
        return CryptoHelper.GenerateRandomString(32);
    }
}

// In a user registration system
public class UserRegistrationService
{
    public string GenerateVerificationCode()
    {
        return CryptoHelper.GenerateRandomString(8);
    }
    
    public string GeneratePasswordResetToken()
    {
        return CryptoHelper.GenerateRandomString(32);
    }
}

Advanced Usage Patterns

Secure Data Storage
public class SecureDataStorage
{
    private readonly string _encryptionKey;
    
    public SecureDataStorage()
    {
        _encryptionKey = CryptoHelper.GenerateRandomString(64);
    }
    
    public string StoreSecureData(string data)
    {
        string salt = CryptoHelper.GenerateRandomString(16);
        string hash = CryptoHelper.ComputeSHA256($"{data}{salt}{_encryptionKey}");
        
        return $"{salt}:{hash}";
    }
    
    public bool VerifySecureData(string data, string storedData)
    {
        var parts = storedData.Split(':');
        if (parts.Length != 2) return false;
        
        string salt = parts[0];
        string storedHash = parts[1];
        string computedHash = CryptoHelper.ComputeSHA256($"{data}{salt}{_encryptionKey}");
        
        return computedHash.Equals(storedHash, StringComparison.OrdinalIgnoreCase);
    }
}
API Security
public class ApiSecurityService
{
    private readonly string _secretKey;
    
    public ApiSecurityService(string secretKey)
    {
        _secretKey = secretKey;
    }
    
    public string GenerateRequestSignature(string endpoint, string timestamp, string payload)
    {
        string toSign = $"{endpoint}{timestamp}{payload}{_secretKey}";
        return CryptoHelper.ComputeSHA256(toSign);
    }
    
    public bool VerifyRequestSignature(string endpoint, string timestamp, string payload, string signature)
    {
        string expectedSignature = GenerateRequestSignature(endpoint, timestamp, payload);
        return expectedSignature.Equals(signature, StringComparison.OrdinalIgnoreCase);
    }
    
    public string GenerateApiKey()
    {
        return CryptoHelper.GenerateRandomString(32);
    }
    
    public string GenerateApiSecret()
    {
        return CryptoHelper.GenerateRandomString(64);
    }
}

JsonHelper

Provides safe and reliable JSON serialization and deserialization with comprehensive error handling.

Key Features

  • Safe Operations: All methods handle exceptions gracefully
  • Null Handling: Proper null value processing
  • Pretty Printing: Human-readable JSON formatting
  • Fallback Values: Default values for failed operations

Methods

SafeSerialize
public static string SafeSerialize<T>(T obj)

Purpose: Safely serializes an object to JSON string with comprehensive error handling.

Parameters:

  • obj: The object to serialize

Returns: JSON string or empty object "{}" if serialization fails

Error Handling:

  • ArgumentNullException: Returns "{}"
  • JsonException: Returns "{}"
  • NotSupportedException: Returns "{}"
  • All other exceptions: Returns "{}"

Examples:

// Basic usage
var user = new User { Name = "John", Age = 30 };
string json = JsonHelper.SafeSerialize(user);
Console.WriteLine(json); // {"Name":"John","Age":30}

// Handling null objects
User? nullUser = null;
string nullJson = JsonHelper.SafeSerialize(nullUser);
Console.WriteLine(nullJson); // {}

// In a logging system
public class JsonLogger
{
    public void LogEvent(string eventName, object data)
    {
        var logEntry = new
        {
            Timestamp = DateTime.UtcNow,
            Event = eventName,
            Data = data
        };
        
        string json = JsonHelper.SafeSerialize(logEntry);
        Console.WriteLine($"Event: {json}");
    }
}
SafeDeserialize
public static T? SafeDeserialize<T>(string json)

Purpose: Safely deserializes JSON string to object with fallback to default value.

Parameters:

  • json: The JSON string to deserialize

Returns: Deserialized object or default(T) if deserialization fails

Error Handling:

  • ArgumentNullException: Returns default(T)
  • JsonException: Returns default(T)
  • NotSupportedException: Returns default(T)
  • Invalid JSON format: Returns default(T)

Examples:

// Basic usage
string json = "{\"Name\":\"Jane\",\"Age\":25}";
User? user = JsonHelper.SafeDeserialize<User>(json);
Console.WriteLine(user?.Name); // Jane

// Handling invalid JSON
string invalidJson = "{invalid json}";
User? defaultUser = JsonHelper.SafeDeserialize<User>(invalidJson);
Console.WriteLine(defaultUser == null); // True

// In a configuration loader
public class ConfigurationManager
{
    public T LoadConfiguration<T>(string filePath) where T : new()
    {
        string json = File.ReadAllText(filePath);
        T? config = JsonHelper.SafeDeserialize<T>(json);
        return config ?? new T();
    }
}
PrettyPrint
public static string PrettyPrint(string json)

Purpose: Formats JSON string with proper indentation for human readability.

Parameters:

  • json: The JSON string to format

Returns: Pretty-printed JSON string or original JSON if formatting fails

Examples:

// Basic usage
string compactJson = "{\"Name\":\"John\",\"Age\":30,\"Active\":true}";
string prettyJson = JsonHelper.PrettyPrint(compactJson);
/* Output:
{
  "Name": "John",
  "Age": 30,
  "Active": true
}
*/

// In a configuration exporter
public class ConfigurationExporter
{
    public void ExportConfiguration<T>(T config, string filePath)
    {
        string json = JsonHelper.SafeSerialize(config);
        string prettyJson = JsonHelper.PrettyPrint(json);
        
        File.WriteAllText(filePath, prettyJson);
        Console.WriteLine($"Configuration exported to {filePath}");
    }
}

Advanced Usage Patterns

Robust API Client
public class RobustApiClient
{
    private readonly HttpClient _httpClient;
    
    public RobustApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<T?> GetAsync<T>(string endpoint, CancellationToken cancellationToken = default)
    {
        try
        {
            HttpResponseMessage response = await _httpClient.GetAsync(endpoint, cancellationToken);
            response.EnsureSuccessStatusCode();
            
            string json = await response.Content.ReadAsStringAsync(cancellationToken);
            return JsonHelper.SafeDeserialize<T>(json);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine($"Request to {endpoint} was cancelled");
            return default;
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"HTTP request failed: {ex.Message}");
            return default;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Unexpected error: {ex.Message}");
            return default;
        }
    }
    
    public async Task<bool> PostAsync<T>(string endpoint, T data, CancellationToken cancellationToken = default)
    {
        try
        {
            string json = JsonHelper.SafeSerialize(data);
            StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
            
            HttpResponseMessage response = await _httpClient.PostAsync(endpoint, content, cancellationToken);
            return response.IsSuccessStatusCode;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"POST request failed: {ex.Message}");
            return false;
        }
    }
    
    public void LogApiCall<T>(string method, string endpoint, T? data = null, Exception? exception = null)
    {
        var logEntry = new
        {
            Timestamp = DateTime.UtcNow,
            Method = method,
            Endpoint = endpoint,
            Data = data,
            Exception = exception?.ToString(),
            Success = exception == null
        };
        
        string json = JsonHelper.SafeSerialize(logEntry);
        string prettyJson = JsonHelper.PrettyPrint(json);
        
        Console.WriteLine($"API Call:\n{prettyJson}");
    }
}
Configuration Management
public class AdvancedConfigurationManager
{
    private readonly string _configPath;
    private Dictionary<string, object> _settings;
    private readonly object _lock = new object();
    
    public AdvancedConfigurationManager(string configPath)
    {
        _configPath = configPath;
        LoadConfiguration();
    }
    
    private void LoadConfiguration()
    {
        lock (_lock)
        {
            if (!File.Exists(_configPath))
            {
                CreateDefaultConfiguration();
                return;
            }
            
            try
            {
                string json = File.ReadAllText(_configPath);
                _settings = JsonHelper.SafeDeserialize<Dictionary<string, object>>(json) 
                          ?? new Dictionary<string, object>();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to load configuration: {ex.Message}");
                _settings = new Dictionary<string, object>();
            }
        }
    }
    
    private void CreateDefaultConfiguration()
    {
        _settings = new Dictionary<string, object>
        {
            ["Database"] = new { ConnectionString = "", Timeout = 30, EnableRetry = true },
            ["Logging"] = new { Level = "Information", File = "app.log", MaxFileSize = 10485760 },
            ["Features"] = new { EnableCaching = true, EnableMetrics = false, EnableTracing = true },
            ["Security"] = new { JwtSecret = "", TokenExpiryMinutes = 60, EnableEncryption = true }
        };
        
        SaveConfiguration();
    }
    
    public T GetSetting<T>(string key, T defaultValue = default)
    {
        lock (_lock)
        {
            if (!_settings.TryGetValue(key, out var value))
            {
                return defaultValue;
            }
            
            try
            {
                string json = JsonHelper.SafeSerialize(value);
                return JsonHelper.SafeDeserialize<T>(json) ?? defaultValue;
            }
            catch
            {
                return defaultValue;
            }
        }
    }
    
    public void SetSetting<T>(string key, T value)
    {
        lock (_lock)
        {
            _settings[key] = value;
            SaveConfiguration();
        }
    }
    
    private void SaveConfiguration()
    {
        try
        {
            string json = JsonHelper.SafeSerialize(_settings);
            string prettyJson = JsonHelper.PrettyPrint(json);
            File.WriteAllText(_configPath, prettyJson);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to save configuration: {ex.Message}");
        }
    }
    
    public void ExportConfiguration(string exportPath)
    {
        lock (_lock)
        {
            string json = JsonHelper.SafeSerialize(_settings);
            string prettyJson = JsonHelper.PrettyPrint(json);
            
            try
            {
                File.WriteAllText(exportPath, prettyJson);
                Console.WriteLine($"Configuration exported to {exportPath}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to export configuration: {ex.Message}");
            }
        }
    }
    
    public void ResetToDefaults()
    {
        lock (_lock)
        {
            CreateDefaultConfiguration();
        }
    }
}

EnvironmentHelper

Provides environment information and system resource management utilities.

Key Features

  • Environment Variables: Safe access with default values
  • System Information: Hardware and operating system details
  • Process Information: Current process resource usage
  • Thread-Safe Operations: All methods are thread-safe

Methods

GetEnvironmentVariable
public static string GetEnvironmentVariable(string key, string defaultValue = "")

Purpose: Gets environment variable value with fallback to default.

Parameters:

  • key: Environment variable name
  • defaultValue: Default value if variable not found

Returns: Environment variable value or default

Examples:

// Basic usage
string dbConnection = EnvironmentHelper.GetEnvironmentVariable("DATABASE_CONNECTION", 
    "Server=localhost;Database=Default;");
string logLevel = EnvironmentHelper.GetEnvironmentVariable("LOG_LEVEL", "Information");

// In a configuration system
public class EnvironmentConfiguration
{
    public string DatabaseConnection => EnvironmentHelper.GetEnvironmentVariable("DATABASE_CONNECTION");
    public string ApiKey => EnvironmentHelper.GetEnvironmentVariable("API_KEY");
    public int Timeout => int.Parse(EnvironmentHelper.GetEnvironmentVariable("TIMEOUT", "30"));
    public bool EnableDebug => bool.Parse(EnvironmentHelper.GetEnvironmentVariable("ENABLE_DEBUG", "false"));
}
System Properties
public static string MachineName { get; }
public static string UserName { get; }
public static bool Is64BitProcess { get; }
public static string OSVersion { get; }

Examples:

// System information
Console.WriteLine($"Machine: {EnvironmentHelper.MachineName}");
Console.WriteLine($"User: {EnvironmentHelper.UserName}");
Console.WriteLine($"64-bit: {EnvironmentHelper.Is64BitProcess}");
Console.WriteLine($"OS: {EnvironmentHelper.OSVersion}");

// In a diagnostic system
public class SystemDiagnostics
{
    public void LogSystemInfo()
    {
        var info = new
        {
            MachineName = EnvironmentHelper.MachineName,
            UserName = EnvironmentHelper.UserName,
            Is64Bit = EnvironmentHelper.Is64BitProcess,
            OSVersion = EnvironmentHelper.OSVersion.ToString(),
            ProcessorCount = Environment.ProcessorCount,
            WorkingDirectory = Environment.CurrentDirectory
        };
        
        string json = JsonHelper.SafeSerialize(info);
        Console.WriteLine($"System Info: {json}");
    }
}

Advanced Usage Patterns

Resource Monitoring
public class ResourceMonitor
{
    public class ResourceInfo
    {
        public DateTime Timestamp { get; set; }
        public long WorkingSet { get; set; }
        public long PrivateMemory { get; set; }
        public int ThreadCount { get; set; }
        public double CpuUsage { get; set; }
        public TimeSpan Uptime { get; set; }
    }
    
    private readonly Process _process;
    private readonly PerformanceCounter _cpuCounter;
    
    public ResourceMonitor()
    {
        _process = Process.GetCurrentProcess();
        _cpuCounter = new PerformanceCounter("Process", "% Processor Time", _process.ProcessName);
    }
    
    public ResourceInfo GetCurrentResourceInfo()
    {
        _process.Refresh();
        
        return new ResourceInfo
        {
            Timestamp = DateTime.UtcNow,
            WorkingSet = _process.WorkingSet64,
            PrivateMemory = _process.PrivateMemorySize64,
            ThreadCount = _process.Threads.Count,
            CpuUsage = _cpuCounter.NextValue(),
            Uptime = DateTime.Now - _process.StartTime
        };
    }
    
    public void StartMonitoring(int intervalSeconds = 60)
    {
        var timer = new Timer(_ =>
        {
            var info = GetCurrentResourceInfo();
            LogResourceInfo(info);
        }, null, 0, intervalSeconds * 1000);
    }
    
    private void LogResourceInfo(ResourceInfo info)
    {
        var logEntry = new
        {
            Timestamp = info.Timestamp,
            WorkingSetMB = info.WorkingSet / 1024 / 1024,
            PrivateMemoryMB = info.PrivateMemory / 1024 / 1024,
            ThreadCount = info.ThreadCount,
            CpuUsage = info.CpuUsage,
            UptimeMinutes = (int)info.Uptime.TotalMinutes,
            Machine = EnvironmentHelper.MachineName,
            Process = _process.ProcessName
        };
        
        string json = JsonHelper.SafeSerialize(logEntry);
        Console.WriteLine($"Resource Monitor: {json}");
    }
    
    public void Dispose()
    {
        _cpuCounter?.Dispose();
        _process?.Dispose();
    }
}
Application Lifecycle Management
public class ApplicationLifecycleManager
{
    private readonly string _applicationName;
    private readonly string _logDirectory;
    
    public ApplicationLifecycleManager(string applicationName)
    {
        _applicationName = applicationName;
        _logDirectory = EnvironmentHelper.GetEnvironmentVariable("LOG_DIRECTORY", 
            Path.Combine(Environment.CurrentDirectory, "logs"));
        
        Directory.CreateDirectory(_logDirectory);
    }
    
    public void OnApplicationStart()
    {
        var startupInfo = new
        {
            EventType = "ApplicationStart",
            Timestamp = DateTime.UtcNow,
            Application = _applicationName,
            Machine = EnvironmentHelper.MachineName,
            User = EnvironmentHelper.UserName,
            ProcessId = Environment.ProcessId,
            WorkingDirectory = Environment.CurrentDirectory,
            CommandLine = Environment.CommandLine,
            EnvironmentVariables = GetRelevantEnvironmentVariables()
        };
        
        LogEvent(startupInfo);
    }
    
    public void OnApplicationShutdown()
    {
        var shutdownInfo = new
        {
            EventType = "ApplicationShutdown",
            Timestamp = DateTime.UtcNow,
            Application = _applicationName,
            Machine = EnvironmentHelper.MachineName,
            User = EnvironmentHelper.UserName,
            ProcessId = Environment.ProcessId
        };
        
        LogEvent(shutdownInfo);
    }
    
    private Dictionary<string, string> GetRelevantEnvironmentVariables()
    {
        var relevantVars = new[]
        {
            "DOTNET_ENVIRONMENT",
            "ASPNETCORE_ENVIRONMENT",
            "LOG_LEVEL",
            "DATABASE_CONNECTION",
            "API_KEY"
        };
        
        return relevantVars.ToDictionary(
            var => var,
            var => EnvironmentHelper.GetEnvironmentVariable(var, "Not Set")
        );
    }
    
    private void LogEvent(object eventData)
    {
        string json = JsonHelper.SafeSerialize(eventData);
        string prettyJson = JsonHelper.PrettyPrint(json);
        
        string logFile = Path.Combine(_logDirectory, $"{_applicationName}_{DateTime.Now:yyyyMMdd}.log");
        
        try
        {
            File.AppendAllText(logFile, prettyJson + Environment.NewLine + Environment.NewLine);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to log event: {ex.Message}");
        }
    }
}

Performance Considerations

CryptoHelper Performance

  1. Hash Caching: Cache frequently computed hashes when appropriate
  2. Batch Operations: Process multiple values in single operations
  3. Memory Management: Clear sensitive data from memory when no longer needed
  4. Algorithm Selection: Use appropriate algorithms for specific use cases

JsonHelper Performance

  1. Reuse JsonSerializerOptions: Create and reuse serializer options
  2. Stream Operations: Use streams for large JSON documents
  3. Async Operations: Use async versions for I/O-bound operations
  4. Object Pooling: Consider object pooling for frequent serialization

EnvironmentHelper Performance

  1. Cache Environment Variables: Cache frequently accessed environment variables
  2. Lazy Loading: Load system information only when needed
  3. Resource Monitoring: Use appropriate intervals for resource monitoring
  4. Memory Efficiency: Minimize allocations in hot paths

Security Best Practices

Cryptographic Security

  1. Use Strong Hashes: Prefer SHA256 over MD5 for security applications
  2. Salt and Pepper: Always use salt for password hashing
  3. Key Management: Never store cryptographic keys in source code
  4. Secure Random: Use cryptographically secure random generation

Data Protection

  1. Sensitive Data: Avoid logging sensitive information
  2. Input Validation: Validate all inputs before processing
  3. Error Handling: Don't expose sensitive information in error messages
  4. Access Control: Implement proper access controls for sensitive operations

Best Practices

Error Handling

  1. Graceful Degradation: Provide fallbacks for failed operations
  2. Logging: Log failures for debugging and monitoring
  3. User Feedback: Provide meaningful error messages to users
  4. Recovery: Implement recovery mechanisms where appropriate

Resource Management

  1. Dispose Resources: Properly dispose of IDisposable objects
  2. Memory Management: Clear sensitive data from memory
  3. Thread Safety: Use proper synchronization for shared resources
  4. Resource Monitoring: Monitor resource usage and optimize accordingly

Version History

v0.1.0

  • Initial release with CryptoHelper, JsonHelper, and EnvironmentHelper
  • Comprehensive error handling and safety checks
  • Secure cryptographic operations
  • Safe JSON serialization/deserialization
  • Environment information access
  • Performance-optimized implementations

Future Enhancements

Planned Helpers

  • CompressionHelper: File compression and decompression utilities
  • EncryptionHelper: Data encryption and decryption operations
  • CacheHelper: Caching utilities and helpers
  • NetworkHelper: Network-related utilities and helpers

Planned Features

  • Async Operations: Async versions of all helper methods
  • Streaming Support: Stream-based operations for large data
  • Configuration Validation: Built-in configuration validation
  • Performance Monitoring: Enhanced performance monitoring capabilities

For more information, see: