using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] namespace AnaSemi { public sealed class NativeProcessResultV006 { public string schema_id { get; internal set; } public string process_kind { get; internal set; } public bool process_started { get; internal set; } public UInt32? process_id { get; internal set; } public UInt16 argv_count { get; internal set; } public string argv_fingerprint { get; internal set; } public string command_line_sha256 { get; internal set; } public string roundtrip_argv_fingerprint { get; internal set; } public DateTimeOffset? started_at { get; internal set; } public DateTimeOffset finished_at { get; internal set; } public Int32? exit_code { get; internal set; } public bool timed_out { get; internal set; } public string copy_state { get; internal set; } public string child_liveness { get; internal set; } public string stdout_path { get; internal set; } public UInt64 stdout_bytes { get; internal set; } public string stdout_sha256 { get; internal set; } public string stderr_path { get; internal set; } public UInt64 stderr_bytes { get; internal set; } public string stderr_sha256 { get; internal set; } public string result_contract_version { get; internal set; } public string wrapper_status { get; internal set; } internal NativeProcessResultV006() { } } public sealed class NativeProcessContractExceptionV006 : Exception { public string StopCode { get; private set; } internal NativeProcessContractExceptionV006(string stopCode) : base(stopCode) { StopCode = stopCode; } internal NativeProcessContractExceptionV006(string stopCode, Exception inner) : base(stopCode, inner) { StopCode = stopCode; } } public static class WindowsCommandLineV006 { [DllImport("shell32.dll", SetLastError = true)] private static extern IntPtr CommandLineToArgvW(string commandLine, out int argumentCount); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LocalFree(IntPtr memory); public static string QuoteAndJoin(string[] logicalArgv) { if (logicalArgv == null) { throw new ArgumentNullException("logicalArgv"); } StringBuilder joined = new StringBuilder(); for (int i = 0; i < logicalArgv.Length; i++) { if (logicalArgv[i] == null) { throw new ArgumentException("Logical argv contains null.", "logicalArgv"); } if (i != 0) { joined.Append(' '); } joined.Append(QuoteToken(logicalArgv[i])); } return joined.ToString(); } public static string Fingerprint(string[] logicalArgv) { if (logicalArgv == null) { throw new ArgumentNullException("logicalArgv"); } using (MemoryStream payload = new MemoryStream()) { WriteUtf8(payload, "ANA-SEMI-ARGV-V001"); for (int i = 0; i < logicalArgv.Length; i++) { if (logicalArgv[i] == null) { throw new ArgumentException("Logical argv contains null.", "logicalArgv"); } payload.WriteByte(0); WriteUtf8(payload, logicalArgv[i]); } return Sha256Hex(payload.ToArray()); } } internal static string[] RoundTrip(string commandLine) { int count; IntPtr pointer = CommandLineToArgvW(commandLine, out count); if (pointer == IntPtr.Zero) { throw new NativeProcessContractExceptionV006("STOP_ACTUAL_CHILD_ARGV_ROUNDTRIP_MISMATCH"); } try { string[] result = new string[count]; for (int i = 0; i < count; i++) { IntPtr item = Marshal.ReadIntPtr(pointer, i * IntPtr.Size); result[i] = Marshal.PtrToStringUni(item); } return result; } finally { LocalFree(pointer); } } internal static string Sha256Utf16Le(string text) { return Sha256Hex(Encoding.Unicode.GetBytes(text)); } internal static string Sha256File(string path, out ulong bytes) { using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None)) using (SHA256 sha = SHA256.Create()) { bytes = checked((ulong)stream.Length); return ToHex(sha.ComputeHash(stream)); } } internal static string Sha256Hex(byte[] bytes) { using (SHA256 sha = SHA256.Create()) { return ToHex(sha.ComputeHash(bytes)); } } private static string QuoteToken(string token) { if (token.Length == 0) { return "\"\""; } bool needsQuotes = token.Any(ch => ch == ' ' || ch == '\t' || ch == '\n' || ch == '\v' || ch == '"'); if (!needsQuotes) { return token; } StringBuilder quoted = new StringBuilder(); quoted.Append('"'); int slashCount = 0; foreach (char ch in token) { if (ch == '\\') { slashCount++; continue; } if (ch == '"') { quoted.Append('\\', slashCount * 2 + 1); quoted.Append('"'); slashCount = 0; continue; } quoted.Append('\\', slashCount); slashCount = 0; quoted.Append(ch); } quoted.Append('\\', slashCount * 2); quoted.Append('"'); return quoted.ToString(); } private static void WriteUtf8(Stream stream, string text) { byte[] bytes = Encoding.UTF8.GetBytes(text); stream.Write(bytes, 0, bytes.Length); } private static string ToHex(byte[] bytes) { StringBuilder text = new StringBuilder(bytes.Length * 2); for (int i = 0; i < bytes.Length; i++) { text.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture)); } return text.ToString(); } } public static class NativeProcessRunnerV006 { public static NativeProcessResultV006 Run( string processKind, string[] logicalArgv, string stdoutPath, string stderrPath, int timeoutMilliseconds, int killWaitMilliseconds, string syntheticFaultMode) { ValidateInputs(processKind, logicalArgv, stdoutPath, stderrPath, timeoutMilliseconds, killWaitMilliseconds, syntheticFaultMode); string fullCommandLine = WindowsCommandLineV006.QuoteAndJoin(logicalArgv); string[] roundTrip = WindowsCommandLineV006.RoundTrip(fullCommandLine); if (!OrdinalArrayEquals(logicalArgv, roundTrip)) { throw new NativeProcessContractExceptionV006("STOP_ACTUAL_CHILD_ARGV_ROUNDTRIP_MISMATCH"); } string argvFingerprint = WindowsCommandLineV006.Fingerprint(logicalArgv); string roundTripFingerprint = WindowsCommandLineV006.Fingerprint(roundTrip); if (!StringComparer.Ordinal.Equals(argvFingerprint, roundTripFingerprint)) { throw new NativeProcessContractExceptionV006("STOP_ACTUAL_CHILD_ARGV_ROUNDTRIP_MISMATCH"); } string stdoutAbsolute = ResolveRelativePath(stdoutPath); string stderrAbsolute = ResolveRelativePath(stderrPath); if (StringComparer.OrdinalIgnoreCase.Equals(stdoutAbsolute, stderrAbsolute)) { throw new NativeProcessContractExceptionV006("STOP_TARGET_ALREADY_EXISTS"); } FileStream stdoutWriter = null; FileStream stderrWriter = null; Process process = null; Task stdoutCopy = null; Task stderrCopy = null; bool processStarted = false; bool timedOut = false; bool copyFailed = false; uint? processId = null; DateTimeOffset? startedAt = null; int? exitCode = null; try { stdoutWriter = new FileStream(stdoutAbsolute, FileMode.CreateNew, FileAccess.Write, FileShare.Read); stderrWriter = new FileStream(stderrAbsolute, FileMode.CreateNew, FileAccess.Write, FileShare.Read); ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = logicalArgv[0]; startInfo.Arguments = WindowsCommandLineV006.QuoteAndJoin(logicalArgv.Skip(1).ToArray()); startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; process = new Process(); process.StartInfo = startInfo; try { if (!process.Start()) { return BuildStartFailed(processKind, logicalArgv, argvFingerprint, fullCommandLine, roundTripFingerprint, stdoutPath, stderrPath, ref stdoutWriter, ref stderrWriter); } } catch (Exception) { return BuildStartFailed(processKind, logicalArgv, argvFingerprint, fullCommandLine, roundTripFingerprint, stdoutPath, stderrPath, ref stdoutWriter, ref stderrWriter); } processStarted = true; processId = checked((uint)process.Id); startedAt = DateTimeOffset.UtcNow; bool injectCopyFailure = StringComparer.Ordinal.Equals(syntheticFaultMode, "FAIL_STDOUT_AFTER_86"); stdoutCopy = CopyStreamAsync(process.StandardOutput.BaseStream, stdoutWriter, injectCopyFailure, 86); stderrCopy = CopyStreamAsync(process.StandardError.BaseStream, stderrWriter, false, 0); if (!process.WaitForExit(timeoutMilliseconds)) { timedOut = true; try { process.Kill(); } catch (Exception ex) { throw new NativeProcessContractExceptionV006("STOP_CHILD_LIVENESS_UNCERTAIN", ex); } if (!process.WaitForExit(killWaitMilliseconds) || !process.HasExited) { throw new NativeProcessContractExceptionV006("STOP_CHILD_LIVENESS_UNCERTAIN"); } } if (!process.HasExited) { throw new NativeProcessContractExceptionV006("STOP_CHILD_LIVENESS_UNCERTAIN"); } exitCode = process.ExitCode; copyFailed = !WaitCopy(stdoutCopy, killWaitMilliseconds) | !WaitCopy(stderrCopy, killWaitMilliseconds); CloseWriter(ref stdoutWriter, ref copyFailed); CloseWriter(ref stderrWriter, ref copyFailed); ulong stdoutBytes; ulong stderrBytes; string stdoutHash; string stderrHash; try { stdoutHash = WindowsCommandLineV006.Sha256File(stdoutAbsolute, out stdoutBytes); stderrHash = WindowsCommandLineV006.Sha256File(stderrAbsolute, out stderrBytes); } catch (Exception ex) { throw new NativeProcessContractExceptionV006("STOP_RAW_FINALIZATION_UNCERTAIN", ex); } string wrapperStatus = timedOut ? "TIMED_OUT" : (copyFailed ? "COPY_FAILED" : "RETURNED"); NativeProcessResultV006 result = NewResult(processKind, logicalArgv, argvFingerprint, fullCommandLine, roundTripFingerprint, true, processId, startedAt, exitCode, timedOut, copyFailed ? "FAIL" : "PASS", "EXITED", stdoutPath, stdoutBytes, stdoutHash, stderrPath, stderrBytes, stderrHash, wrapperStatus); return ProcessResultEnvelopeValidatorV006.ValidateSingle(new object[] { result }); } catch (NativeProcessContractExceptionV006) { throw; } catch (Exception ex) { throw new NativeProcessContractExceptionV006("STOP_RAW_FINALIZATION_UNCERTAIN", ex); } finally { if (processStarted && process != null) { try { if (!process.HasExited) { process.Kill(); process.WaitForExit(killWaitMilliseconds); } } catch (Exception) { } } if (stdoutWriter != null) { try { stdoutWriter.Dispose(); } catch (Exception) { } } if (stderrWriter != null) { try { stderrWriter.Dispose(); } catch (Exception) { } } if (process != null) { process.Dispose(); } } } private static NativeProcessResultV006 BuildStartFailed( string processKind, string[] logicalArgv, string argvFingerprint, string fullCommandLine, string roundTripFingerprint, string stdoutPath, string stderrPath, ref FileStream stdoutWriter, ref FileStream stderrWriter) { bool closeFailed = false; CloseWriter(ref stdoutWriter, ref closeFailed); CloseWriter(ref stderrWriter, ref closeFailed); if (closeFailed) { throw new NativeProcessContractExceptionV006("STOP_RAW_FINALIZATION_UNCERTAIN"); } ulong stdoutBytes; ulong stderrBytes; string stdoutHash; string stderrHash; try { stdoutHash = WindowsCommandLineV006.Sha256File(ResolveRelativePath(stdoutPath), out stdoutBytes); stderrHash = WindowsCommandLineV006.Sha256File(ResolveRelativePath(stderrPath), out stderrBytes); } catch (Exception ex) { throw new NativeProcessContractExceptionV006("STOP_RAW_FINALIZATION_UNCERTAIN", ex); } string emptyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; if (stdoutBytes != 0 || stderrBytes != 0 || !StringComparer.Ordinal.Equals(stdoutHash, emptyHash) || !StringComparer.Ordinal.Equals(stderrHash, emptyHash)) { throw new NativeProcessContractExceptionV006("STOP_RAW_FINALIZATION_UNCERTAIN"); } NativeProcessResultV006 result = NewResult(processKind, logicalArgv, argvFingerprint, fullCommandLine, roundTripFingerprint, false, null, null, null, false, "NOT_STARTED", "NOT_STARTED", stdoutPath, stdoutBytes, stdoutHash, stderrPath, stderrBytes, stderrHash, "PROCESS_START_FAILED"); return ProcessResultEnvelopeValidatorV006.ValidateSingle(new object[] { result }); } private static NativeProcessResultV006 NewResult( string processKind, string[] logicalArgv, string argvFingerprint, string fullCommandLine, string roundTripFingerprint, bool processStarted, uint? processId, DateTimeOffset? startedAt, int? exitCode, bool timedOut, string copyState, string childLiveness, string stdoutPath, ulong stdoutBytes, string stdoutHash, string stderrPath, ulong stderrBytes, string stderrHash, string wrapperStatus) { return new NativeProcessResultV006 { schema_id = "ANA-SEMI-NATIVE-PROCESS-RESULT-V005", process_kind = processKind, process_started = processStarted, process_id = processId, argv_count = checked((ushort)logicalArgv.Length), argv_fingerprint = argvFingerprint, command_line_sha256 = WindowsCommandLineV006.Sha256Utf16Le(fullCommandLine), roundtrip_argv_fingerprint = roundTripFingerprint, started_at = startedAt, finished_at = DateTimeOffset.UtcNow, exit_code = exitCode, timed_out = timedOut, copy_state = copyState, child_liveness = childLiveness, stdout_path = stdoutPath, stdout_bytes = stdoutBytes, stdout_sha256 = stdoutHash, stderr_path = stderrPath, stderr_bytes = stderrBytes, stderr_sha256 = stderrHash, result_contract_version = "V005-001", wrapper_status = wrapperStatus }; } private static async Task CopyStreamAsync(Stream source, Stream destination, bool failAfterBytes, int byteLimit) { byte[] buffer = new byte[4096]; int written = 0; while (true) { int read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); if (read == 0) { break; } int offset = 0; while (offset < read) { if (failAfterBytes && written >= byteLimit) { throw new IOException("Synthetic copy failure."); } int allowed = failAfterBytes ? Math.Min(read - offset, byteLimit - written) : read - offset; if (allowed <= 0) { throw new IOException("Synthetic copy failure."); } await destination.WriteAsync(buffer, offset, allowed).ConfigureAwait(false); offset += allowed; written += allowed; } } } private static bool WaitCopy(Task task, int timeoutMilliseconds) { try { if (!task.Wait(timeoutMilliseconds)) { return false; } return task.Status == TaskStatus.RanToCompletion; } catch (Exception) { return false; } } private static void CloseWriter(ref FileStream writer, ref bool failed) { if (writer == null) { return; } try { writer.Flush(true); } catch (Exception) { failed = true; } try { writer.Dispose(); } catch (Exception) { failed = true; } writer = null; } private static void ValidateInputs(string processKind, string[] logicalArgv, string stdoutPath, string stderrPath, int timeoutMilliseconds, int killWaitMilliseconds, string syntheticFaultMode) { if (String.IsNullOrWhiteSpace(processKind) || logicalArgv == null || logicalArgv.Length == 0 || logicalArgv.Length > UInt16.MaxValue || String.IsNullOrWhiteSpace(logicalArgv[0])) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (timeoutMilliseconds <= 0 || killWaitMilliseconds <= 0) { throw new NativeProcessContractExceptionV006("STOP_CHILD_LIVENESS_UNCERTAIN"); } if (!StringComparer.Ordinal.Equals(syntheticFaultMode, "NONE") && !StringComparer.Ordinal.Equals(syntheticFaultMode, "FAIL_STDOUT_AFTER_86")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (StringComparer.Ordinal.Equals(syntheticFaultMode, "FAIL_STDOUT_AFTER_86") && !StringComparer.Ordinal.Equals(processKind, "SYNTHETIC_COPYFAIL")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } ResolveRelativePath(stdoutPath); ResolveRelativePath(stderrPath); } private static string ResolveRelativePath(string path) { if (String.IsNullOrWhiteSpace(path) || Path.IsPathRooted(path) || path.IndexOf(':') >= 0) { throw new NativeProcessContractExceptionV006("STOP_TARGET_ALREADY_EXISTS"); } string full = Path.GetFullPath(path); string current = Path.GetFullPath(Environment.CurrentDirectory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; if (!full.StartsWith(current, StringComparison.OrdinalIgnoreCase)) { throw new NativeProcessContractExceptionV006("STOP_TARGET_ALREADY_EXISTS"); } return full; } private static bool OrdinalArrayEquals(string[] left, string[] right) { if (left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (!StringComparer.Ordinal.Equals(left[i], right[i])) { return false; } } return true; } } public static class ProcessResultEnvelopeValidatorV006 { private static readonly string[] PropertyNames = new[] { "schema_id", "process_kind", "process_started", "process_id", "argv_count", "argv_fingerprint", "command_line_sha256", "roundtrip_argv_fingerprint", "started_at", "finished_at", "exit_code", "timed_out", "copy_state", "child_liveness", "stdout_path", "stdout_bytes", "stdout_sha256", "stderr_path", "stderr_bytes", "stderr_sha256", "result_contract_version", "wrapper_status" }; private static readonly Type[] PropertyTypes = new[] { typeof(string), typeof(string), typeof(bool), typeof(UInt32?), typeof(UInt16), typeof(string), typeof(string), typeof(string), typeof(DateTimeOffset?), typeof(DateTimeOffset), typeof(Int32?), typeof(bool), typeof(string), typeof(string), typeof(string), typeof(UInt64), typeof(string), typeof(string), typeof(UInt64), typeof(string), typeof(string), typeof(string) }; public static NativeProcessResultV006 ValidateSingle(object[] items) { if (items == null || items.Length != 1 || items[0] == null || !StringComparer.Ordinal.Equals(items[0].GetType().FullName, "AnaSemi.NativeProcessResultV006")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } PropertyInfo[] properties = items[0].GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) .OrderBy(property => property.MetadataToken).ToArray(); if (properties.Length != PropertyNames.Length) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } for (int i = 0; i < PropertyNames.Length; i++) { if (!StringComparer.Ordinal.Equals(properties[i].Name, PropertyNames[i]) || properties[i].PropertyType != PropertyTypes[i]) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } } NativeProcessResultV006 result = (NativeProcessResultV006)items[0]; ValidateFields(result); return result; } private static void ValidateFields(NativeProcessResultV006 result) { if (!StringComparer.Ordinal.Equals(result.schema_id, "ANA-SEMI-NATIVE-PROCESS-RESULT-V005") || !StringComparer.Ordinal.Equals(result.result_contract_version, "V005-001") || String.IsNullOrWhiteSpace(result.process_kind) || result.argv_count == 0 || !IsHex64(result.argv_fingerprint) || !IsHex64(result.command_line_sha256) || !IsHex64(result.roundtrip_argv_fingerprint) || !StringComparer.Ordinal.Equals(result.argv_fingerprint, result.roundtrip_argv_fingerprint) || String.IsNullOrWhiteSpace(result.stdout_path) || String.IsNullOrWhiteSpace(result.stderr_path) || !IsHex64(result.stdout_sha256) || !IsHex64(result.stderr_sha256)) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } bool returned = StringComparer.Ordinal.Equals(result.wrapper_status, "RETURNED"); bool startFailed = StringComparer.Ordinal.Equals(result.wrapper_status, "PROCESS_START_FAILED"); bool timedOut = StringComparer.Ordinal.Equals(result.wrapper_status, "TIMED_OUT"); bool copyFailed = StringComparer.Ordinal.Equals(result.wrapper_status, "COPY_FAILED"); if (!returned && !startFailed && !timedOut && !copyFailed) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (startFailed) { if (result.process_started || result.process_id.HasValue || result.started_at.HasValue || result.exit_code.HasValue || result.timed_out || !StringComparer.Ordinal.Equals(result.copy_state, "NOT_STARTED") || !StringComparer.Ordinal.Equals(result.child_liveness, "NOT_STARTED")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } } else { if (!result.process_started || !result.process_id.HasValue || !result.started_at.HasValue || !result.exit_code.HasValue || !StringComparer.Ordinal.Equals(result.child_liveness, "EXITED")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (timedOut != result.timed_out) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (copyFailed && !StringComparer.Ordinal.Equals(result.copy_state, "FAIL")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } if (returned && !StringComparer.Ordinal.Equals(result.copy_state, "PASS")) { throw new NativeProcessContractExceptionV006("STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT"); } } } private static bool IsHex64(string value) { if (value == null || value.Length != 64) { return false; } for (int i = 0; i < value.Length; i++) { char ch = value[i]; if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))) { return false; } } return true; } } public static class ProcessResultSidecarCsvV006 { public static void WriteCreateNew(NativeProcessResultV006 result, string sidecarPath) { NativeProcessResultV006 validated = ProcessResultEnvelopeValidatorV006.ValidateSingle(new object[] { result }); string[] header = new[] { "schema_id", "process_kind", "process_started", "process_id", "argv_count", "argv_fingerprint", "command_line_sha256", "roundtrip_argv_fingerprint", "started_at", "finished_at", "exit_code", "timed_out", "copy_state", "child_liveness", "stdout_path", "stdout_bytes", "stdout_sha256", "stderr_path", "stderr_bytes", "stderr_sha256", "result_contract_version", "wrapper_status" }; string[] values = new[] { validated.schema_id, validated.process_kind, validated.process_started ? "TRUE" : "FALSE", validated.process_id.HasValue ? validated.process_id.Value.ToString(CultureInfo.InvariantCulture) : String.Empty, validated.argv_count.ToString(CultureInfo.InvariantCulture), validated.argv_fingerprint, validated.command_line_sha256, validated.roundtrip_argv_fingerprint, validated.started_at.HasValue ? validated.started_at.Value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture) : String.Empty, validated.finished_at.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture), validated.exit_code.HasValue ? validated.exit_code.Value.ToString(CultureInfo.InvariantCulture) : String.Empty, validated.timed_out ? "TRUE" : "FALSE", validated.copy_state, validated.child_liveness, validated.stdout_path, validated.stdout_bytes.ToString(CultureInfo.InvariantCulture), validated.stdout_sha256, validated.stderr_path, validated.stderr_bytes.ToString(CultureInfo.InvariantCulture), validated.stderr_sha256, validated.result_contract_version, validated.wrapper_status }; string text = String.Join(",", header.Select(CsvEscape).ToArray()) + "\n" + String.Join(",", values.Select(CsvEscape).ToArray()) + "\n"; byte[] bytes = new UTF8Encoding(false).GetBytes(text); using (FileStream stream = new FileStream(sidecarPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { stream.Write(bytes, 0, bytes.Length); stream.Flush(true); } } private static string CsvEscape(string value) { string safe = value ?? String.Empty; if (safe.IndexOfAny(new[] { ',', '"', '\r', '\n' }) >= 0) { return "\"" + safe.Replace("\"", "\"\"") + "\""; } return safe; } } }