using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using BizTalkSapEnvironmentInventory.Configuration;
using BizTalkSapEnvironmentInventory.Infrastructure;
using BizTalkSapEnvironmentInventory.Models;
namespace BizTalkSapEnvironmentInventory.Collectors
{
///
/// Exportiert BizTalk-Gruppenbindings read-only und extrahiert sichere SAP-Endpunktparameter.
///
internal sealed class BindingExportCollector
{
private static readonly Regex ApplicationModuleName = new Regex(
@"^\[Application:(.+)\]$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly CommandLineOptions options;
private readonly InventoryDocument document;
private readonly ConsoleFileLogger logger;
private readonly TimeSpan processTimeout;
public BindingExportCollector(
CommandLineOptions options,
InventoryDocument document,
ConsoleFileLogger logger,
int timeoutSeconds)
{
this.options = options;
this.document = document;
this.logger = logger;
processTimeout = TimeSpan.FromSeconds(Math.Max(30, timeoutSeconds));
}
///
/// Verarbeitet entweder eine vorgegebene Binding-Datei oder einen temporären BTSTask-Export.
///
public void Collect()
{
if (!string.IsNullOrWhiteSpace(options.BindingFilePath))
{
logger.Info("Werte vorhandenen Binding-Export offline aus: " + options.BindingFilePath);
ParseBindingDocument(LoadXml(options.BindingFilePath), "Angegebene Binding-Datei");
return;
}
var btsTask = ResolveBtsTaskPath();
if (string.IsNullOrWhiteSpace(btsTask))
{
throw new FileNotFoundException(
"BTSTask.exe wurde nicht gefunden. --btstask oder --binding-file verwenden.");
}
var temporaryPath = Path.Combine(
Path.GetTempPath(),
"BizTalkSapInventory-" + Guid.NewGuid().ToString("N") + ".xml");
try
{
ExportBindings(btsTask, temporaryPath);
ParseBindingDocument(LoadXml(temporaryPath), "BTSTask GroupLevel Export");
}
finally
{
TryDelete(temporaryPath);
}
}
///
/// Parst ein Binding-Dokument; intern sichtbar, damit der Self-Test reale XML-Strukturen prüft.
///
internal void ParseBindingDocument(XDocument bindingDocument, string source)
{
if (bindingDocument == null || bindingDocument.Root == null)
{
throw new InvalidDataException("Binding-XML ist leer.");
}
AddApplicationsFromBinding(bindingDocument);
var parsed = 0;
foreach (var sendPort in bindingDocument.Descendants()
.Where(item => IsName(item, "SendPort") && HasAncestor(item, "SendPortCollection")))
{
var endpoint = ParseSendPort(sendPort, source);
if (endpoint != null)
{
MergeEndpoint(endpoint);
parsed++;
}
}
foreach (var receiveLocation in bindingDocument.Descendants()
.Where(item => IsName(item, "ReceiveLocation")
&& HasAncestor(item, "ReceiveLocations")))
{
var endpoint = ParseReceiveLocation(receiveLocation, source);
if (endpoint != null)
{
MergeEndpoint(endpoint);
parsed++;
}
}
logger.Info(parsed + " SAP-Endpunkt(e) aus dem Binding-Export ausgewertet.");
}
private SapEndpointRecord ParseSendPort(XElement sendPort, string source)
{
var primaryTransport = Child(sendPort, "PrimaryTransport");
var transportType = primaryTransport == null ? null : Child(primaryTransport, "TransportType");
var transportData = primaryTransport == null ? null : Child(primaryTransport, "TransportTypeData");
var properties = ParsePropertyBag(transportData);
var adapterName = Attribute(transportType, "Name");
var address = ChildValue(primaryTransport, "Address");
if (!LooksLikeSap(adapterName, address, properties))
{
return null;
}
var endpoint = new SapEndpointRecord
{
ApplicationName = FirstNonEmpty(
Attribute(sendPort, "ApplicationName"),
ChildValue(sendPort, "ApplicationName")),
Direction = "Senden",
Name = Attribute(sendPort, "Name"),
ParentPortName = Attribute(sendPort, "Name"),
AdapterName = adapterName,
HostName = FirstNonEmpty(
Attribute(Child(primaryTransport, "SendHandler"), "Name"),
ChildValue(primaryTransport, "SendHandler")),
Status = FirstNonEmpty(Attribute(sendPort, "Status"), "Konfiguriert"),
Address = SensitiveDataSanitizer.SanitizeText(address),
Source = source
};
AddProperties(endpoint, properties);
ParseSapAddress(endpoint, address);
FinalizeEndpoint(endpoint);
return endpoint;
}
private SapEndpointRecord ParseReceiveLocation(XElement receiveLocation, string source)
{
var transportType = Child(receiveLocation, "ReceiveLocationTransportType");
var transportData = Child(receiveLocation, "ReceiveLocationTransportTypeData");
var properties = ParsePropertyBag(transportData);
var adapterName = Attribute(transportType, "Name");
var address = FirstNonEmpty(
ChildValue(receiveLocation, "Address"),
Attribute(receiveLocation, "Address"));
if (!LooksLikeSap(adapterName, address, properties))
{
return null;
}
var receivePort = receiveLocation.Ancestors()
.FirstOrDefault(item => IsName(item, "ReceivePort"));
var endpoint = new SapEndpointRecord
{
ApplicationName = FirstNonEmpty(
Attribute(receiveLocation, "ApplicationName"),
ChildValue(receiveLocation, "ApplicationName"),
Attribute(receivePort, "ApplicationName"),
ChildValue(receivePort, "ApplicationName")),
Direction = "Empfangen",
Name = Attribute(receiveLocation, "Name"),
ParentPortName = Attribute(receivePort, "Name"),
AdapterName = adapterName,
HostName = FirstNonEmpty(
Attribute(Child(receiveLocation, "ReceiveHandler"), "Name"),
ChildValue(receiveLocation, "ReceiveHandler")),
Status = FirstNonEmpty(
Attribute(receiveLocation, "Enable"),
Attribute(receiveLocation, "Status"),
"Konfiguriert"),
Address = SensitiveDataSanitizer.SanitizeText(address),
Source = source
};
AddProperties(endpoint, properties);
ParseSapAddress(endpoint, address);
FinalizeEndpoint(endpoint);
return endpoint;
}
private static List ParsePropertyBag(XElement dataElement)
{
var result = new List();
if (dataElement == null)
{
return result;
}
var raw = dataElement.HasElements
? string.Concat(dataElement.Nodes().Select(item => item.ToString(SaveOptions.DisableFormatting)))
: dataElement.Value;
ParseXmlProperties(raw, "Binding", result, 0);
return result;
}
private static void ParseXmlProperties(
string raw,
string source,
List target,
int depth)
{
if (string.IsNullOrWhiteSpace(raw) || depth > 4)
{
return;
}
raw = WebUtility.HtmlDecode(raw.Trim());
if (!raw.StartsWith("<", StringComparison.Ordinal))
{
return;
}
XElement root;
try
{
root = XElement.Parse(raw, LoadOptions.None);
}
catch (XmlException)
{
return;
}
foreach (var element in root.DescendantsAndSelf())
{
foreach (var attribute in element.Attributes())
{
if (string.Equals(attribute.Name.LocalName, "vt", StringComparison.OrdinalIgnoreCase)
|| attribute.IsNamespaceDeclaration)
{
continue;
}
AddUnique(
target,
element.Name.LocalName + "." + attribute.Name.LocalName,
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
source);
AddUnique(
target,
NormalizePropertyName(attribute.Name.LocalName),
SensitiveDataSanitizer.RedactValue(attribute.Name.LocalName, attribute.Value),
source);
}
if (element.HasElements)
{
continue;
}
var name = element.Name.LocalName;
if (!SensitiveDataSanitizer.IsSensitiveName(name)
&& element.Value.TrimStart().StartsWith("<", StringComparison.Ordinal))
{
ParseXmlProperties(element.Value, name, target, depth + 1);
continue;
}
var value = SensitiveDataSanitizer.RedactValue(name, element.Value);
AddUnique(target, name, value, source);
}
}
private static void ParseSapAddress(SapEndpointRecord endpoint, string rawAddress)
{
if (string.IsNullOrWhiteSpace(rawAddress)
|| !rawAddress.StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
{
return;
}
var address = SensitiveDataSanitizer.SanitizeText(rawAddress);
var withoutScheme = address.Substring("sap://".Length);
var atIndex = withoutScheme.IndexOf('@');
var userInfo = atIndex >= 0 ? withoutScheme.Substring(0, atIndex) : string.Empty;
var hostAndQuery = atIndex >= 0 ? withoutScheme.Substring(atIndex + 1) : withoutScheme;
foreach (var item in userInfo.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
var pair = item.Split(new[] { '=' }, 2);
if (pair.Length == 2)
{
AddUnique(
endpoint.Properties,
NormalizePropertyName(pair[0]),
Decode(pair[1]),
"SAP-URI");
}
}
var queryIndex = hostAndQuery.IndexOf('?');
var hostPart = queryIndex >= 0
? hostAndQuery.Substring(0, queryIndex)
: hostAndQuery;
var query = queryIndex >= 0
? hostAndQuery.Substring(queryIndex + 1)
: string.Empty;
var hostSegments = hostPart.Split('/');
if (hostSegments.Length > 0)
{
AddUnique(endpoint.Properties, "ConnectionType", Decode(hostSegments[0]), "SAP-URI");
if (string.Equals(hostSegments[0], "A", StringComparison.OrdinalIgnoreCase))
{
if (hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "ApplicationServerHost", Decode(hostSegments[1]), "SAP-URI");
}
if (hostSegments.Length > 2)
{
AddUnique(endpoint.Properties, "SystemNumber", Decode(hostSegments[2]), "SAP-URI");
}
}
else if (string.Equals(hostSegments[0], "B", StringComparison.OrdinalIgnoreCase))
{
if (hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "MessageServerHost", Decode(hostSegments[1]), "SAP-URI");
}
if (hostSegments.Length > 2)
{
AddUnique(endpoint.Properties, "R3SystemName", Decode(hostSegments[2]), "SAP-URI");
}
}
else if (string.Equals(hostSegments[0], "D", StringComparison.OrdinalIgnoreCase)
&& hostSegments.Length > 1)
{
AddUnique(endpoint.Properties, "DestinationName", Decode(hostSegments[1]), "SAP-URI");
}
}
foreach (var item in query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
{
var pair = item.Split(new[] { '=' }, 2);
if (pair.Length == 2)
{
AddUnique(
endpoint.Properties,
NormalizePropertyName(Decode(pair[0])),
SensitiveDataSanitizer.RedactValue(pair[0], Decode(pair[1])),
"SAP-URI");
}
}
}
private void FinalizeEndpoint(SapEndpointRecord endpoint)
{
ResolveApplication(endpoint);
var useSnc = endpoint.GetProperty("UseSnc", "UseSNC");
var sncEnabled = IsTrue(useSnc);
var affiliateApplication = endpoint.GetProperty(
"AffiliateApplicationName",
"SsoAffiliateApplication",
"SSOApplication");
var userName = endpoint.GetProperty("UserName", "Username");
if (sncEnabled)
{
endpoint.SecurityMode = "SAP SNC";
}
else if (!string.IsNullOrWhiteSpace(affiliateApplication))
{
endpoint.SecurityMode = "Enterprise SSO";
}
else if (!string.IsNullOrWhiteSpace(userName))
{
endpoint.SecurityMode = "SAP-Benutzer";
}
else
{
endpoint.SecurityMode = "Nicht eindeutig aus Binding ableitbar";
}
if (!string.IsNullOrWhiteSpace(affiliateApplication))
{
endpoint.CredentialReference = "SSO Affiliate Application: " + affiliateApplication;
}
else if (!string.IsNullOrWhiteSpace(userName))
{
endpoint.CredentialReference = "SAP-Benutzer: " + userName
+ "; Kennwort wird nicht exportiert/dokumentiert.";
}
else
{
endpoint.CredentialReference =
"Keine Kennwortinformation im Binding-Export; Credentials separat kontrollieren.";
}
foreach (var property in endpoint.Properties.Where(item =>
item.Name.IndexOf("Action", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("Microsoft.LobServices.Sap", StringComparison.OrdinalIgnoreCase) >= 0))
{
var operation = SensitiveDataSanitizer.SanitizeText(property.Value);
if (!string.IsNullOrWhiteSpace(operation)
&& !endpoint.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
{
endpoint.Operations.Add(operation);
}
}
}
private void ResolveApplication(SapEndpointRecord endpoint)
{
if (!string.IsNullOrWhiteSpace(endpoint.ApplicationName))
{
EnsureApplication(endpoint.ApplicationName);
return;
}
foreach (var application in document.Applications)
{
if (application.Artifacts.Any(item =>
string.Equals(item.Name, endpoint.Name, StringComparison.OrdinalIgnoreCase)
|| (!string.IsNullOrWhiteSpace(endpoint.ParentPortName)
&& string.Equals(
item.Name,
endpoint.ParentPortName,
StringComparison.OrdinalIgnoreCase))))
{
endpoint.ApplicationName = application.Name;
return;
}
}
}
private void MergeEndpoint(SapEndpointRecord parsed)
{
var existing = document.SapEndpoints.FirstOrDefault(item =>
string.Equals(item.Direction, parsed.Direction, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Name, parsed.Name, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
document.SapEndpoints.Add(parsed);
return;
}
existing.ApplicationName = FirstNonEmpty(parsed.ApplicationName, existing.ApplicationName);
existing.ParentPortName = FirstNonEmpty(parsed.ParentPortName, existing.ParentPortName);
existing.AdapterName = FirstNonEmpty(parsed.AdapterName, existing.AdapterName);
existing.HostName = FirstNonEmpty(parsed.HostName, existing.HostName);
existing.Status = FirstNonEmpty(parsed.Status, existing.Status);
existing.Address = FirstNonEmpty(parsed.Address, existing.Address);
existing.SecurityMode = FirstNonEmpty(parsed.SecurityMode, existing.SecurityMode);
existing.CredentialReference = FirstNonEmpty(
parsed.CredentialReference,
existing.CredentialReference);
existing.Source = FirstNonEmpty(parsed.Source, existing.Source);
foreach (var property in parsed.Properties)
{
AddUnique(existing.Properties, property.Name, property.Value, property.Source);
}
foreach (var operation in parsed.Operations)
{
if (!existing.Operations.Contains(operation, StringComparer.OrdinalIgnoreCase))
{
existing.Operations.Add(operation);
}
}
}
private void AddApplicationsFromBinding(XDocument bindingDocument)
{
foreach (var module in bindingDocument.Descendants().Where(item => IsName(item, "ModuleRef")))
{
var match = ApplicationModuleName.Match(Attribute(module, "Name"));
if (match.Success)
{
EnsureApplication(match.Groups[1].Value);
}
}
foreach (var applicationName in bindingDocument.Descendants()
.Where(item => IsName(item, "ApplicationName"))
.Select(item => item.Value)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase))
{
EnsureApplication(applicationName);
}
}
private void EnsureApplication(string name)
{
if (string.IsNullOrWhiteSpace(name)
|| document.Applications.Any(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)))
{
return;
}
document.Applications.Add(new ApplicationRecord
{
Name = name.Trim(),
Status = "Aus Binding-Export erkannt"
});
}
private void ExportBindings(string btsTaskPath, string destination)
{
var arguments = new StringBuilder();
arguments.Append("ExportBindings /GroupLevel /Destination:\"")
.Append(EscapeCommandArgument(destination))
.Append("\"");
if (!string.IsNullOrWhiteSpace(document.System.ManagementServer))
{
arguments.Append(" /Server:\"")
.Append(EscapeCommandArgument(document.System.ManagementServer))
.Append("\"");
}
if (!string.IsNullOrWhiteSpace(document.System.ManagementDatabase))
{
arguments.Append(" /Database:\"")
.Append(EscapeCommandArgument(document.System.ManagementDatabase))
.Append("\"");
}
logger.Info("Exportiere BizTalk-Gruppenbindings read-only mit BTSTask.");
logger.Detail("BTSTask-Pfad: " + btsTaskPath);
logger.Detail("BTSTask-Timeout: "
+ processTimeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)
+ " Sekunden.");
var output = new StringBuilder();
var outputSync = new object();
var timer = Stopwatch.StartNew();
var startInfo = new ProcessStartInfo
{
FileName = btsTaskPath,
Arguments = arguments.ToString(),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(btsTaskPath)
};
using (var process = new Process { StartInfo = startInfo })
{
process.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
lock (outputSync)
{
output.AppendLine(safeLine);
}
logger.Detail("BTSTask: " + safeLine);
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
var safeLine = SensitiveDataSanitizer.SanitizeText(args.Data);
lock (outputSync)
{
output.AppendLine(safeLine);
}
logger.Detail("BTSTask: " + safeLine);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!process.WaitForExit((int)processTimeout.TotalMilliseconds))
{
try
{
process.Kill();
}
catch (InvalidOperationException)
{
}
throw new TimeoutException(
"BTSTask ExportBindings überschritt " + processTimeout.TotalSeconds + " Sekunden.");
}
process.WaitForExit();
timer.Stop();
if (process.ExitCode != 0)
{
string errorOutput;
lock (outputSync)
{
errorOutput = output.ToString();
}
throw new InvalidOperationException(
"BTSTask ExportBindings meldete Exitcode "
+ process.ExitCode
+ ": "
+ LastLines(errorOutput, 8));
}
logger.Info("BTSTask ExportBindings erfolgreich abgeschlossen ("
+ timer.ElapsedMilliseconds + " ms).");
}
if (!File.Exists(destination) || new FileInfo(destination).Length == 0)
{
throw new InvalidDataException("BTSTask erzeugte keine Binding-Datei.");
}
logger.Detail("Temporärer Binding-Export: "
+ new FileInfo(destination).Length
.ToString(CultureInfo.InvariantCulture)
+ " Bytes.");
}
private string ResolveBtsTaskPath()
{
var candidates = new List();
if (!string.IsNullOrWhiteSpace(options.BtsTaskPath))
{
candidates.Add(options.BtsTaskPath);
}
if (!string.IsNullOrWhiteSpace(document.System.BizTalkInstallPath))
{
candidates.Add(Path.Combine(document.System.BizTalkInstallPath, "BTSTask.exe"));
}
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
candidates.Add(Path.Combine(programFiles, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
candidates.Add(Path.Combine(programFilesX86, "Microsoft BizTalk Server 2020", "BTSTask.exe"));
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
candidates.AddRange(path.Split(Path.PathSeparator)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Select(item => Path.Combine(item.Trim(), "BTSTask.exe")));
return candidates.FirstOrDefault(File.Exists);
}
private static XDocument LoadXml(string path)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
IgnoreComments = true
};
using (var stream = File.OpenRead(path))
using (var reader = XmlReader.Create(stream, settings))
{
return XDocument.Load(reader, LoadOptions.None);
}
}
private static bool LooksLikeSap(
string adapterName,
string address,
IEnumerable properties)
{
if ((adapterName ?? string.Empty).IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| (address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return properties.Any(item =>
item.Name.IndexOf("SAP", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("sapBinding", StringComparison.OrdinalIgnoreCase) >= 0
|| item.Value.IndexOf("Microsoft.Adapters.SAP", StringComparison.OrdinalIgnoreCase) >= 0);
}
private static void AddProperties(
SapEndpointRecord endpoint,
IEnumerable properties)
{
foreach (var property in properties)
{
AddUnique(endpoint.Properties, property.Name, property.Value, property.Source);
}
}
private static void AddUnique(
List target,
string name,
string value,
string source)
{
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
{
return;
}
var safeValue = SensitiveDataSanitizer.RedactValue(name, value);
if (target.Any(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Value, safeValue, StringComparison.Ordinal)))
{
return;
}
target.Add(new NameValueRecord(name, safeValue, source));
}
private static string NormalizePropertyName(string name)
{
var normalized = (name ?? string.Empty).Trim();
var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ "Client", "Client" },
{ "lang", "Language" },
{ "Language", "Language" },
{ "GwHost", "GatewayHost" },
{ "GwServ", "GatewayService" },
{ "ListenerDest", "ListenerDestination" },
{ "ListenerGwHost", "ListenerGatewayHost" },
{ "ListenerGwServ", "ListenerGatewayService" },
{ "ListenerProgramId", "ListenerProgramId" },
{ "UseSnc", "UseSnc" },
{ "SAPROUTER", "SapRouter" }
};
string mapped;
return mappings.TryGetValue(normalized, out mapped) ? mapped : normalized;
}
private static XElement Child(XElement parent, string localName)
{
return parent == null
? null
: parent.Elements().FirstOrDefault(item => IsName(item, localName));
}
private static string ChildValue(XElement parent, string localName)
{
var child = Child(parent, localName);
return child == null ? string.Empty : child.Value;
}
private static string Attribute(XElement element, string localName)
{
if (element == null)
{
return string.Empty;
}
var attribute = element.Attributes().FirstOrDefault(item =>
string.Equals(item.Name.LocalName, localName, StringComparison.OrdinalIgnoreCase));
return attribute == null ? string.Empty : attribute.Value;
}
private static bool IsName(XElement element, string localName)
{
return string.Equals(
element.Name.LocalName,
localName,
StringComparison.OrdinalIgnoreCase);
}
private static bool HasAncestor(XElement element, string localName)
{
return element.Ancestors().Any(item => IsName(item, localName));
}
private static string FirstNonEmpty(params string[] values)
{
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
}
private static bool IsTrue(string value)
{
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "ja", StringComparison.OrdinalIgnoreCase);
}
private static string Decode(string value)
{
try
{
return Uri.UnescapeDataString((value ?? string.Empty).Replace("+", " "));
}
catch (UriFormatException)
{
return value ?? string.Empty;
}
}
private static string EscapeCommandArgument(string value)
{
return (value ?? string.Empty).Replace("\"", string.Empty)
.Replace("\r", string.Empty)
.Replace("\n", string.Empty);
}
private static string LastLines(string text, int count)
{
var lines = (text ?? string.Empty)
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
return string.Join(" | ", lines.Skip(Math.Max(0, lines.Length - count)));
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
}