diff --git a/src/LogExpert.Core/Classes/Log/LogfileReader.cs b/src/LogExpert.Core/Classes/Log/LogfileReader.cs index a0c1fab79..82a6779b4 100644 --- a/src/LogExpert.Core/Classes/Log/LogfileReader.cs +++ b/src/LogExpert.Core/Classes/Log/LogfileReader.cs @@ -190,7 +190,11 @@ private LogfileReader ( { try { - _mmfReader = new MemoryMappedFileReader(_watchedILogFileInfo.FullName, EncodingOptions.Encoding ?? Encoding.Default); + using var stream = new FileStream(_watchedILogFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var (preambleLength, detectedEncoding) = PositionAwareStreamReaderBase.DetectPreambleLength(stream); + var encoding = PositionAwareStreamReaderBase.DetermineEncoding(EncodingOptions, detectedEncoding); + + _mmfReader = new MemoryMappedFileReader(_watchedILogFileInfo.FullName, encoding, preambleLength); } catch (IOException) { diff --git a/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs b/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs index 73f784553..d74ebca36 100644 --- a/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs +++ b/src/LogExpert.Core/Classes/Log/MemoryMappedFileReader.cs @@ -10,9 +10,10 @@ namespace LogExpert.Core.Classes.Log; /// Reads log lines via memory-mapped file access. Builds a line-offset index on load. /// Supports tail mode by re-mapping when the file grows. /// -internal sealed class MemoryMappedFileReader (string filePath, Encoding encoding) : IDisposable +internal sealed class MemoryMappedFileReader (string filePath, Encoding encoding, int preambleLength) : IDisposable { private readonly Encoding _encoding = encoding; + private readonly int _preambleLength = preambleLength; private readonly LineOffsetIndex _lineIndex = new(); private MemoryMappedFile _mmf; private MemoryMappedViewAccessor _accessor; @@ -33,7 +34,7 @@ public void BuildIndex (long startOffset = 0) if (startOffset == 0) { _lineIndex.Clear(); - _lineIndex.Add(0); // first line starts at offset 0 + _lineIndex.Add(_preambleLength); // Skip the BOM, when present, just like StreamReader. } fs.Position = startOffset; diff --git a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs index e186bc002..88479a78e 100644 --- a/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs +++ b/src/LogExpert.Core/Classes/Log/Streamreaders/PositionAwareStreamReaderBase.cs @@ -165,9 +165,9 @@ protected void MovePosition (int offset) public static Encoding DetermineEncoding (EncodingOptions options, Encoding detectedEncoding) { - return options?.Encoding != null - ? options.Encoding - : detectedEncoding ?? options?.DefaultEncoding ?? Encoding.Default; + // An explicit/persisted encoding also represents a manual Encoding-menu choice. + // Without one, use the file BOM before the configured and machine defaults. + return options?.Encoding ?? detectedEncoding ?? options?.DefaultEncoding ?? Encoding.Default; } /// diff --git a/src/LogExpert.Tests/Dialogs/SettingsDialogPortableModeTests.cs b/src/LogExpert.Tests/Dialogs/SettingsDialogPortableModeTests.cs index b23b8a8a4..4dda21f02 100644 --- a/src/LogExpert.Tests/Dialogs/SettingsDialogPortableModeTests.cs +++ b/src/LogExpert.Tests/Dialogs/SettingsDialogPortableModeTests.cs @@ -1,3 +1,6 @@ +using System.Text; + +using LogExpert; using LogExpert.Core.Config; using LogExpert.Core.Interfaces; using LogExpert.Dialogs; @@ -19,6 +22,16 @@ namespace LogExpert.Tests.Dialogs; [TestFixture] public class SettingsDialogPortableModeTests { + [Test] + public void AvailableEncodings_ContainsWindows1250 () + { + Program.RegisterEncodingProvider(); + + var encodings = SettingsDialog.GetAvailableEncodings(); + + Assert.That(encodings.Select(encoding => encoding.CodePage), Does.Contain(1250)); + } + private string _testDataPath = null!; private string _portableConfigDir = null!; diff --git a/src/LogExpert.Tests/IPC/OneInstanceIpcTests.cs b/src/LogExpert.Tests/IPC/OneInstanceIpcTests.cs index dcb7c4cbd..ae2f3326b 100644 --- a/src/LogExpert.Tests/IPC/OneInstanceIpcTests.cs +++ b/src/LogExpert.Tests/IPC/OneInstanceIpcTests.cs @@ -1,3 +1,6 @@ +using System.Text; + +using LogExpert; using LogExpert.Core.Classes.IPC; using Newtonsoft.Json; @@ -13,6 +16,14 @@ namespace LogExpert.Tests.IPC; [TestFixture] public class OneInstanceIpcTests { + [Test] + public void RegisterEncodingProvider_MakesWindows1252Available () + { + Program.RegisterEncodingProvider(); + + Assert.That(Encoding.GetEncoding(1252).CodePage, Is.EqualTo(1252)); + } + #region IPC Message Type Tests [Test] diff --git a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderBlockAllocationTests.cs b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderBlockAllocationTests.cs index 6bd1ae97a..521fad3f1 100644 --- a/src/LogExpert.Tests/StreamReaderTests/LogfileReaderBlockAllocationTests.cs +++ b/src/LogExpert.Tests/StreamReaderTests/LogfileReaderBlockAllocationTests.cs @@ -16,6 +16,7 @@ public class LogfileReaderBlockAllocationTests [SetUp] public void Setup () { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); _tempFile = Path.GetTempFileName(); _ = PluginRegistry.PluginRegistry.Create(Path.GetDirectoryName(_tempFile)!, 500); } @@ -229,6 +230,60 @@ public void ReadFiles_MultipleBufferRotations_AllLinesCorrect () } } + [Test] + public void ReadFiles_BomlessFile_UsesConfiguredDefaultEncoding () + { + var configuredEncoding = Encoding.GetEncoding(1252); + File.WriteAllText(_tempFile, "Euro: €\n", configuredEncoding); + + using var reader = CreateReader(new EncodingOptions { DefaultEncoding = configuredEncoding }); + reader.ReadFiles(); + + Assert.That(reader.CurrentEncoding.CodePage, Is.EqualTo(configuredEncoding.CodePage)); + Assert.That(reader.GetLogLineMemory(0)?.FullLine.Span.ToString(), Is.EqualTo("Euro: €")); + } + + [Test] + public void ReadFiles_Bom_OverridesConfiguredDefaultEncoding () + { + var configuredEncoding = Encoding.GetEncoding(1252); + File.WriteAllText(_tempFile, "Euro: €\n", Encoding.UTF8); + + using var reader = CreateReader(new EncodingOptions { DefaultEncoding = configuredEncoding }); + reader.ReadFiles(); + + Assert.That(reader.CurrentEncoding.WebName, Is.EqualTo(Encoding.UTF8.WebName)); + Assert.That(reader.GetLogLineMemory(0)?.FullLine.Span.ToString(), Is.EqualTo("Euro: €")); + } + + [Test] + public void ReadFiles_ExplicitEncoding_OverridesBom () + { + var explicitEncoding = Encoding.GetEncoding(1252); + File.WriteAllText(_tempFile, "ASCII text\n", Encoding.UTF8); + + using var reader = CreateReader(new EncodingOptions { Encoding = explicitEncoding }); + reader.ReadFiles(); + + Assert.That(reader.CurrentEncoding.CodePage, Is.EqualTo(explicitEncoding.CodePage)); + Assert.That(reader.GetLogLineMemory(0)?.FullLine.Span.ToString(), Is.EqualTo("ASCII text")); + } + + private LogfileReader CreateReader (EncodingOptions encodingOptions) + { + return new LogfileReader( + _tempFile, + encodingOptions, + multiFile: false, + bufferCount: 100, + linesPerBuffer: 500, + new MultiFileOptions(), + ReaderType.System, + PluginRegistry.PluginRegistry.Instance, + maximumLineLength: 500, + progressReporter: Core.Classes.Log.ProgressReporters.NullProgressReporter.Instance); + } + private static void VerifyLine (LogfileReader reader, int lineNum, int expectedIndex) { var line = reader.GetLogLineMemory(lineNum); diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs index 8207efdfb..1aae5b825 100644 --- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs +++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs @@ -86,8 +86,6 @@ private SettingsDialog (Preferences prefs, LogTabWindow logTabWin) LoadResources(); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - ResumeLayout(); } @@ -693,23 +691,38 @@ private void DisplayCurrentIcon () /// /// /// This method clears any existing items in the combo box and adds a selection of common encodings, including - /// ASCII, Default (UTF-8), ISO-8859-1, UTF-8, Unicode, and Windows-1252. The value member of the combo box is set + /// ASCII, Default (UTF-8), ISO-8859-1, UTF-8, Unicode, Windows-1250, and Windows-1252. The value member of the combo box is set /// to a specific header name defined in the resources. /// private void FillEncodingList () { comboBoxEncoding.Items.Clear(); - _ = comboBoxEncoding.Items.Add(Encoding.ASCII); - _ = comboBoxEncoding.Items.Add(Encoding.Default); - _ = comboBoxEncoding.Items.Add(Encoding.GetEncoding("iso-8859-1")); - _ = comboBoxEncoding.Items.Add(Encoding.UTF8); - _ = comboBoxEncoding.Items.Add(Encoding.Unicode); - _ = comboBoxEncoding.Items.Add(CodePagesEncodingProvider.Instance.GetEncoding(1252)); + foreach (var encoding in GetAvailableEncodings()) + { + _ = comboBoxEncoding.Items.Add(encoding); + } comboBoxEncoding.ValueMember = Resources.SettingsDialog_UI_ComboBox_Encoding_ValueMember_HeaderName; } + /// + /// Gets the encodings offered in the Preferences dropdown. + /// + internal static IReadOnlyList GetAvailableEncodings () + { + return + [ + Encoding.ASCII, + Encoding.Default, + Encoding.GetEncoding("iso-8859-1"), + Encoding.UTF8, + Encoding.Unicode, + Encoding.GetEncoding(1250), + Encoding.GetEncoding(1252) + ]; + } + /// /// Populates the language selection list with available language options. /// @@ -1622,4 +1635,4 @@ private void UpdateSampleAndPreview () } #endregion -} \ No newline at end of file +} diff --git a/src/LogExpert/Program.cs b/src/LogExpert/Program.cs index 2122957f8..9162d2b18 100644 --- a/src/LogExpert/Program.cs +++ b/src/LogExpert/Program.cs @@ -54,6 +54,8 @@ private static void Main (string[] args) Application.EnableVisualStyles(); Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); + RegisterEncodingProvider(); + // Register the plugin assembly resolver early so that settings deserialization // can find plugin types (e.g., CsvColumnizer) before PluginRegistry.Create() runs. PluginRegistry.PluginRegistry.RegisterAssemblyResolver(); @@ -247,8 +249,8 @@ or ArgumentNullException } } - [SupportedOSPlatform("windows")] - private static void SetDarkMode () + [SupportedOSPlatform("windows")] + private static void SetDarkMode () { var darkModeEnabled = ConfigManager.Instance.Settings.Preferences.DarkMode; if (darkModeEnabled) @@ -259,9 +261,17 @@ private static void SetDarkMode () { Application.SetColorMode(SystemColorMode.System); } - } - - [SupportedOSPlatform("windows")] + } + + /// + /// Makes legacy Windows code pages configured in preferences available before settings are read. + /// + internal static void RegisterEncodingProvider () + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + } + + [SupportedOSPlatform("windows")] private static void SetCulture () { var defaultCulture = CultureInfo.GetCultureInfo(ConfigManager.Instance.Settings.Preferences.DefaultLanguage ?? "en-US"); @@ -485,4 +495,4 @@ private static void CurrentDomain_UnhandledException (object sender, UnhandledEx } #endregion -} \ No newline at end of file +}