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
+}