diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index 89895817..98f5491e 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -1709,8 +1709,8 @@ private ILogStreamReader CreateLogStreamReader (Stream stream, EncodingOptions e ReaderType.Legacy => new PositionAwareStreamReaderLegacy(stream, encodingOptions, _maximumLineLength), ReaderType.System => new PositionAwareStreamReaderSystem(stream, encodingOptions, _maximumLineLength), ReaderType.SystemDirect => new PositionAwareStreamReaderDirect(stream, encodingOptions, _maximumLineLength), - //Default will be System - _ => new PositionAwareStreamReaderSystem(stream, encodingOptions, _maximumLineLength), + //Default will be SystemDirect, because it is the best performing reader and should be used if not explicitly overridden by user. + _ => new PositionAwareStreamReaderDirect(stream, encodingOptions, _maximumLineLength), }; } diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs index aef38bb2..e3282c88 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderDirect.cs @@ -159,7 +159,15 @@ public List DetachBlocks () // Rent a fresh block and carry over any unscanned data (partial line in progress) var tailLength = _readBlockLength - _scanOffset; - var newBlock = ArrayPool.Shared.Rent(BLOCK_SIZE); + + // The tail may exceed BLOCK_SIZE after reading a long line (buffer was grown). + var newBlockSize = BLOCK_SIZE; + while (tailLength > newBlockSize) + { + newBlockSize *= 2; + } + + var newBlock = ArrayPool.Shared.Rent(newBlockSize); if (tailLength > 0) { @@ -183,8 +191,17 @@ private void RefillBlock (StreamReader reader) { var tailLength = _readBlockLength - _scanOffset; - // Rent a new block - var newBlock = ArrayPool.Shared.Rent(BLOCK_SIZE); + // Determine new block size: if the tail already fills a standard block, + // grow the buffer so there's room to read more data. This handles lines + // longer than BLOCK_SIZE (e.g. huge XML payloads). + var newBlockSize = BLOCK_SIZE; + while (tailLength >= newBlockSize) + { + newBlockSize *= 2; + } + + // Rent a new block (may be larger than BLOCK_SIZE for very long lines) + var newBlock = ArrayPool.Shared.Rent(newBlockSize); // Copy the tail (partial line) to the start of the new block if (tailLength > 0) @@ -199,7 +216,8 @@ private void RefillBlock (StreamReader reader) _scanOffset = 0; // Fill the rest of the block from the stream - var charsRead = reader.Read(newBlock, tailLength, BLOCK_SIZE - tailLength); + var available = newBlock.Length - tailLength; + var charsRead = reader.Read(newBlock, tailLength, available); _readBlockLength = tailLength + charsRead; diff --git a/src/LogExpert.Core/Enums/ReaderType.cs b/src/LogExpert.Core/Enums/ReaderType.cs index dbfabfb0..a16c38a5 100644 --- a/src/LogExpert.Core/Enums/ReaderType.cs +++ b/src/LogExpert.Core/Enums/ReaderType.cs @@ -6,9 +6,10 @@ namespace LogExpert.Core.Enums; public enum ReaderType { /// - /// System.IO.Pipelines based reader implementation (high performance). + /// Direct-read implementation: reads decoded chars directly into pooled blocks via + /// StreamReader.Read(char[], offset, count), eliminating per-line string allocation. /// - Pipeline, + SystemDirect, /// /// Legacy reader implementation (original). @@ -18,11 +19,5 @@ public enum ReaderType /// /// System.IO.StreamReader based implementation. /// - System, - - /// - /// Direct-read implementation: reads decoded chars directly into pooled blocks via - /// StreamReader.Read(char[], offset, count), eliminating per-line string allocation. - /// - SystemDirect + System } diff --git a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs index 3fbbc8ee..a3390aa3 100644 --- a/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/PositionAwareStreamReaderDirectTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text; using LogExpert.Core.Classes.Log.Streamreaders; @@ -287,4 +288,317 @@ public void DetachBlocks_ReturnsCompletedBlocks () } #endregion + + #region Lines longer than BLOCK_SIZE (32768 chars) + + private const int TEST_TIMEOUT_MS = 5000; + + private static void RunWithTimeout (Action action, int timeoutMs = TEST_TIMEOUT_MS) + { + var task = Task.Run(action); + Assert.That(task.Wait(timeoutMs), Is.True, + "Test timed out — reader is likely in an infinite loop growing buffers"); + } + + /// + /// Reproduces the bug: a single line longer than the internal BLOCK_SIZE (32768 chars) + /// causes the reader to loop endlessly, growing buffers without making progress. + /// The user's real-world case: a TSMP XML line with thousands of tetraTalkgroup entries (~100K+ chars). + /// + [Test] + public void TryReadLine_LineLongerThanBlockSize_ReadsAllLines () + { + RunWithTimeout(() => + { + // Simulate the real scenario: normal log lines, then one massive XML line, then more normal lines + const int longLineLength = 100_000; // well over BLOCK_SIZE (32768) + var sb = new StringBuilder(); + + // 10 normal lines before the long one + for (var i = 0; i < 10; i++) + { + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"2026-05-06 12:00:00.{i:D3} [INFO] Normal log line {i}"); + } + + // The problematic line: simulates a massive XML payload (no internal newlines) + _ = sb.Append(""); + _ = sb.Append(new string('X', longLineLength - 50)); // fill to target length + _ = sb.AppendLine(""); + + // 10 normal lines after the long one + for (var i = 0; i < 10; i++) + { + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"2026-05-06 12:00:01.{i:D3} [INFO] After long line {i}"); + } + + var text = sb.ToString(); + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), longLineLength + 100); + + var lines = new List(); + while (reader.TryReadLine(out var line)) + { + lines.Add(line.Span.ToString()); + } + + Assert.That(lines.Count, Is.EqualTo(21), "Should read all 21 lines (10 + 1 long + 10)"); + Assert.That(lines[0], Does.StartWith("2026-05-06 12:00:00.000")); + Assert.That(lines[10], Does.StartWith("")); + Assert.That(lines[10].Length, Is.GreaterThan(longLineLength - 50)); + Assert.That(lines[20], Does.StartWith("2026-05-06 12:00:01.009")); + }); + } + + [Test] + public void TryReadLine_LineExactlyAtBlockSize_ReadsCorrectly () + { + RunWithTimeout(() => + { + // Line of exactly 32768 chars (the BLOCK_SIZE boundary) + const int lineLength = 32_768; + var longLine = new string('Z', lineLength); + var text = $"before\n{longLine}\nafter\n"; + + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), lineLength + 100); + + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("before")); + + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(lineLength)); + + Assert.That(reader.TryReadLine(out var line3), Is.True); + Assert.That(line3.Span.ToString(), Is.EqualTo("after")); + + Assert.That(reader.TryReadLine(out _), Is.False); + }); + } + + [Test] + public void TryReadLine_LineSlightlyOverBlockSize_ReadsCorrectly () + { + RunWithTimeout(() => + { + // Line just 1 char over the block boundary + const int lineLength = 32_769; // BLOCK_SIZE + 1 + var longLine = new string('Y', lineLength); + var text = $"before\n{longLine}\nafter\n"; + + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), lineLength + 100); + + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("before")); + + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(lineLength)); + + Assert.That(reader.TryReadLine(out var line3), Is.True); + Assert.That(line3.Span.ToString(), Is.EqualTo("after")); + + Assert.That(reader.TryReadLine(out _), Is.False); + }); + } + + [Test] + public void TryReadLine_LineMultipleTimesBlockSize_ReadsCorrectly () + { + RunWithTimeout(() => + { + // Line that spans multiple block doublings: 200K chars (~6x BLOCK_SIZE) + const int lineLength = 200_000; + var longLine = new string('W', lineLength); + var text = $"first\n{longLine}\nlast\n"; + + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), lineLength + 100); + + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("first")); + + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(lineLength)); + Assert.That(line2.Span[0], Is.EqualTo('W')); + Assert.That(line2.Span[lineLength - 1], Is.EqualTo('W')); + + Assert.That(reader.TryReadLine(out var line3), Is.True); + Assert.That(line3.Span.ToString(), Is.EqualTo("last")); + + Assert.That(reader.TryReadLine(out _), Is.False); + }); + } + + [Test] + public void TryReadLine_LineLongerThanBlockSize_PositionParityWithSystemReader () + { + RunWithTimeout(() => + { + // Verify byte positions match between Direct and System readers for long lines + const int lineLength = 50_000; + var text = $"short\n{new string('Q', lineLength)}\nend\n"; + var bytes = Encoding.UTF8.GetBytes(text); + + using var s1 = new MemoryStream(bytes); + using var s2 = new MemoryStream(bytes); + // Note: System reader has a pre-existing bug in CharBlockAllocator.ReturnAll() for + // oversized lines (allocates with new[] but tries to return to ArrayPool). Suppress + // disposal to avoid that unrelated failure. + var systemReader = new PositionAwareStreamReaderSystem(s1, new EncodingOptions { Encoding = Encoding.UTF8 }, lineLength + 100); + using var directReader = new PositionAwareStreamReaderDirect(s2, new EncodingOptions { Encoding = Encoding.UTF8 }, lineLength + 100); + + try + { + var lineNum = 0; + while (true) + { + var systemHasLine = systemReader.TryReadLine(out var systemLine); + var directHasLine = directReader.TryReadLine(out var directLine); + + Assert.That(directHasLine, Is.EqualTo(systemHasLine), $"Line {lineNum}: EOF mismatch"); + if (!systemHasLine) + { + break; + } + + Assert.That(directLine.Span.ToString(), Is.EqualTo(systemLine.Span.ToString()), + $"Line {lineNum}: content mismatch"); + Assert.That(directReader.Position, Is.EqualTo(systemReader.Position), + $"Line {lineNum}: position mismatch (System={systemReader.Position}, Direct={directReader.Position})"); + + lineNum++; + } + + Assert.That(lineNum, Is.EqualTo(3)); + } + finally + { + // Don't call Dispose on systemReader — CharBlockAllocator has a pre-existing + // bug returning oversized (new[]) arrays to ArrayPool. + // The GC will reclaim the memory. + } + }); + } + + [Test] + public void TryReadLine_LineLongerThanBlockSize_WithMaxLineLengthTruncation () + { + RunWithTimeout(() => + { + // Long line that exceeds MaximumLineLength — should truncate but not hang + const int lineLength = 100_000; + const int maxLineLength = 20_000; // typical LogExpert setting + var longLine = new string('T', lineLength); + var text = $"before\n{longLine}\nafter\n"; + + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), maxLineLength); + + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("before")); + + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(maxLineLength), "Long line should be truncated to MaximumLineLength"); + + Assert.That(reader.TryReadLine(out var line3), Is.True); + Assert.That(line3.Span.ToString(), Is.EqualTo("after")); + + Assert.That(reader.TryReadLine(out _), Is.False); + }); + } + + [Test] + public void TryReadLine_LineLongerThanBlockSize_DetachBlocksAfterEachLine () + { + RunWithTimeout(() => + { + // Simulates the real LogfileReader pattern: DetachBlocks() is called after reading lines. + // This tests the interaction between grown buffers and DetachBlocks. + const int lineLength = 100_000; + var longLine = new string('D', lineLength); + var text = $"line1\n{longLine}\nline3\n"; + + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), lineLength + 100); + + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("line1")); + var blocks1 = reader.DetachBlocks(); + Assert.That(blocks1.Count, Is.GreaterThan(0)); + + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(lineLength)); + var blocks2 = reader.DetachBlocks(); + Assert.That(blocks2.Count, Is.GreaterThan(0)); + + Assert.That(reader.TryReadLine(out var line3), Is.True); + Assert.That(line3.Span.ToString(), Is.EqualTo("line3")); + + Assert.That(reader.TryReadLine(out _), Is.False); + }, 10000); + } + + [Test] + public void TryReadLine_LineLongerThanBlockSize_DetachBlocksWithLargeTail () + { + RunWithTimeout(() => + { + // KEY BUG TEST: After reading a long line, the internal buffer has grown (e.g. 128K). + // If the file has lots of data AFTER the long line, the "tail" (unscanned data after + // the long line's \n) can exceed BLOCK_SIZE. DetachBlocks() rents only BLOCK_SIZE + // for the carry-over buffer, causing a buffer overflow / ArgumentOutOfRangeException. + // + // To trigger this: line length of ~65536 forces buffer to grow to 131072 (via + // ArrayPool bucket). After reading the line, available space reads ~65K of subsequent + // data into the buffer. After _scanOffset advances past the line, tail > BLOCK_SIZE. + const int longLineLength = 65_536; // Forces buffer growth to 131072 + var sb = new StringBuilder(); + + // One short line first (so DetachBlocks resets to fresh BLOCK_SIZE before long line) + _ = sb.AppendLine("prefix"); + + // The long line — forces buffer to grow to 128K+ + _ = sb.Append(new string('L', longLineLength)); + _ = sb.Append('\n'); + + // Lots of short lines AFTER — ensures the stream has data to fill buffer tail + // We need > 32768 chars of data after the long line's \n + for (var i = 0; i < 1500; i++) + { + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"[{i:D4}] padding-line-to-fill-buffer-tail-past-BLOCK_SIZE-boundary"); + } + + var text = sb.ToString(); + using var stream = CreateStream(text); + using var reader = new PositionAwareStreamReaderDirect(stream, new EncodingOptions(), longLineLength + 100); + + // Read and detach the prefix (resets reader to fresh BLOCK_SIZE buffer) + Assert.That(reader.TryReadLine(out var line1), Is.True); + Assert.That(line1.Span.ToString(), Is.EqualTo("prefix")); + _ = reader.DetachBlocks(); + + // Read the long line — this grows the buffer to 131072 + Assert.That(reader.TryReadLine(out var line2), Is.True); + Assert.That(line2.Length, Is.EqualTo(longLineLength)); + + // THIS IS THE CRITICAL CALL: DetachBlocks must handle tail > BLOCK_SIZE + var blocks = reader.DetachBlocks(); + Assert.That(blocks.Count, Is.GreaterThan(0)); + + // Verify we can still read subsequent lines correctly + var count = 0; + while (reader.TryReadLine(out var line)) + { + count++; + if (count == 1) + { + Assert.That(line.Span.ToString(), Does.StartWith("[0000]")); + } + } + + Assert.That(count, Is.EqualTo(1500), "All lines after the long line should be readable"); + }, 10000); + } + + #endregion } \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index ad8671dc..54ce91fa 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -777,7 +777,7 @@ private void OnBtnOkClick (object sender, EventArgs e) Preferences.DefaultEncoding = comboBoxEncoding.SelectedItem != null ? (comboBoxEncoding.SelectedItem as Encoding).HeaderName : Encoding.Default.HeaderName; Preferences.DefaultLanguage = comboBoxLanguage.SelectedItem != null ? (comboBoxLanguage.SelectedItem as string) : CultureInfo.GetCultureInfo("en-US").Name; Preferences.ShowColumnFinder = checkBoxColumnFinder.Checked; - Preferences.ReaderType = comboBoxReaderType.SelectedItem != null ? (ReaderType)comboBoxReaderType.SelectedItem : ReaderType.Pipeline; + Preferences.ReaderType = comboBoxReaderType.SelectedItem != null ? (ReaderType)comboBoxReaderType.SelectedItem : ReaderType.SystemDirect; Preferences.MaximumFilterEntries = (int)upDownMaximumFilterEntries.Value; Preferences.MaximumFilterEntriesDisplayed = (int)upDownMaximumFilterEntriesDisplayed.Value; diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index 3e31f327..332bbcc0 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2026-05-04 10:41:14 UTC + /// Generated: 2026-05-06 14:47:19 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "FC795D0F77912FC8CA521912072A33A4526B5DD417091EE18F06191E02E019FD", + ["AutoColumnizer.dll"] = "FD61116D4A97900F68D049775D265469B9C6B3202035EA8B66084AE1944D5CF3", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "0BA0126080FAEC25281C2E009CECD28F9CE95B71FB5339A27ED8C2B3761BBABE", - ["CsvColumnizer.dll (x86)"] = "0BA0126080FAEC25281C2E009CECD28F9CE95B71FB5339A27ED8C2B3761BBABE", - ["DefaultPlugins.dll"] = "D836EFDF3CE6E700E1E304DF1C42FC27047F4FA97104F63CC6C83AF408ED435D", - ["FlashIconHighlighter.dll"] = "9E920F4B7A50EBA43BA1327987DB4C70D901FA2A91318B8994F81C1D0518A857", - ["GlassfishColumnizer.dll"] = "4075E5B23FB304B016A6B8C0CA976D891FA5338C2345E075DA72A21F2793FBDE", - ["JsonColumnizer.dll"] = "6551E65D7611D87B8E2C475D4FBC35D28FA5A12F0F1880C55FA31B33A14AC8B5", - ["JsonCompactColumnizer.dll"] = "99F820A8AA6A3B439AD579232FAB63B8F2934973A9761A1AB4BEEF21EE06471D", - ["Log4jXmlColumnizer.dll"] = "1C6238C0C0593675A37C9777869C9D7C3036352AEEA33C0DB8C659EEA90B4350", - ["LogExpert.Core.dll"] = "C99E67DF224EF6F3EFB81423DA0EF03CDBC035256679E628ACE57502C05CEE3D", - ["LogExpert.Resources.dll"] = "F7F10951B324469CEB20A71DFC7446EBA4F4EFD1E4F4041645D0D64DD3E2F1D7", + ["CsvColumnizer.dll"] = "5AEFECB027955EF4EB9274553F94081F17DD6457AAD57AF664AFFBD3932889CF", + ["CsvColumnizer.dll (x86)"] = "5AEFECB027955EF4EB9274553F94081F17DD6457AAD57AF664AFFBD3932889CF", + ["DefaultPlugins.dll"] = "0F7CE41008BBBD1BCDF4066C07D1F7D3B4DFEC9FFBF75CDFAE29E93E4EF994DC", + ["FlashIconHighlighter.dll"] = "C9889BE6722A38B9A58E7A1796985D68A24189A7D1EDAA7C5F929D85F8FC9BA7", + ["GlassfishColumnizer.dll"] = "AD4AE9B53F54D9E37B9F5C25FD8C1BF764FB58EE8DBF29842791552B7F414899", + ["JsonColumnizer.dll"] = "22777672274FD32FC36E4AEF7BA3EDBBC80C15B104E7E03F225AA21E485296A8", + ["JsonCompactColumnizer.dll"] = "8817DAFF4E5A574FC28D1099498E32BC7151C3FD5557BD9E8B955D633C2812A6", + ["Log4jXmlColumnizer.dll"] = "2A215605B7E093C83424AFCDA440BDD23C6732536B08AEA1EF081D1B540A48D4", + ["LogExpert.Core.dll"] = "AA2562DBEBC5508B119C79BB84D0F0B441704B15725E870F4802E8B319CBE613", + ["LogExpert.Resources.dll"] = "90CDAA06B3B2A9E7C521016EE3610FE3A0E8F71C0DC7BFE5DCEBC32BD866FE70", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "9456D9FEC4B63EAA67CB9E89FFAA1A69062F2BB384AE67749ED77569D5BA7D27", - ["SftpFileSystem.dll"] = "E3C7EBC2A51E2159A5CC049C0827F09BC3D5A15E344B31BB748BF40EF155248B", - ["SftpFileSystem.dll (x86)"] = "6A765CB8606E866DCEE9C1DB14A00FA04B799E60FBEAA4478E01FC2133256D8C", - ["SftpFileSystem.Resources.dll"] = "9A9512846AC9E511D5562A437462525F8736E3E4A62B24458A70288EF189593F", - ["SftpFileSystem.Resources.dll (x86)"] = "9A9512846AC9E511D5562A437462525F8736E3E4A62B24458A70288EF189593F", + ["RegexColumnizer.dll"] = "237C573356EA01DAA956375E9E9D6EA477C45BCB81E3F1E7035595AEBA558B7D", + ["SftpFileSystem.dll"] = "B34DC1E5CE32D6C5D0E4522A358471D451B455835D318400327122139601BA6E", + ["SftpFileSystem.dll (x86)"] = "AB972F0E77DA7706214B042908AF40BE3BBB3EA12123AD76946EA224061E8C27", + ["SftpFileSystem.Resources.dll"] = "D7F1419A2F25058D937309DECC8A02615F2A150B219A75D51667001124A32075", + ["SftpFileSystem.Resources.dll (x86)"] = "D7F1419A2F25058D937309DECC8A02615F2A150B219A75D51667001124A32075", }; }