Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text;
using System.Text;

using LogExpert.Core.Entities;

Expand Down Expand Up @@ -221,8 +221,19 @@ public static (int length, Encoding? detectedEncoding) DetectPreambleLength (Str
return (0, null);
}

/// <summary>
/// Bytes to advance the position by per character read, or 0 for "measure the character".
/// </summary>
/// <remarks>
/// The fallback is keyed on <see cref="Encoding.IsSingleByte"/>, not on "everything else is one
/// byte": a variable-width code page such as GB2312 (one byte per ASCII character, two per Chinese
/// one) is neither a <see cref="UTF8Encoding"/> nor a <see cref="UnicodeEncoding"/>, and counting it
/// as one byte per character drifts the position on the first non-ASCII character.
/// </remarks>
public static int GetPosIncPrecomputed (Encoding usedEncoding)
{
ArgumentNullException.ThrowIfNull(usedEncoding);

switch (usedEncoding)
{
case UTF8Encoding:
Expand All @@ -235,7 +246,7 @@ public static int GetPosIncPrecomputed (Encoding usedEncoding)
}
default:
{
return 1;
return usedEncoding.IsSingleByte ? 1 : 0;
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions src/LogExpert.Core/Helpers/EncodingRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,58 @@ namespace LogExpert.Core.Helpers;
/// </remarks>
public static class EncodingRegistry
{
/// <summary>
/// Code page of GB2312, the simplified-Chinese code page (issue #688).
/// </summary>
public const int CODE_PAGE_GB2312 = 936;

/// <summary>
/// The encodings LogExpert offers the user to choose from, in the order they are presented.
/// </summary>
/// <remarks>
/// The single list: the Preferences default-encoding combo box and the per-file View → Encoding
/// menu both draw their rows from it, so they cannot drift apart and adding an encoding is a
/// one-line change here. Three invariants, each pinned by a test:
/// <list type="bullet">
/// <item>
/// Every entry resolves from its own <see cref="Encoding.HeaderName"/>, because that name is
/// what gets persisted (in the settings JSON and in a .lxp) and resolved again on the next
/// start. An entry that did not would silently degrade to a fallback.
/// </item>
/// <item>
/// No two entries share a code page. Both UIs label a row with its header name, so two entries
/// for one code page are two rows the user cannot tell apart — which is exactly what
/// <c>Encoding.Default</c> alongside <see cref="Encoding.UTF8"/> produced (both are code page
/// 65001 on .NET, differing only in the BOM they emit, which a read-side encoding never uses).
/// Hence no <c>Encoding.Default</c> entry.
/// </item>
/// <item>
/// New entries are appended rather than sorted in, so an existing user's row does not move
/// under the cursor on upgrade.
/// </item>
/// </list>
/// </remarks>
public static IReadOnlyList<Encoding> OfferedEncodings => _offeredEncodings.Value;

/// <remarks>
/// Built once: <see cref="Encoding"/> instances are immutable and the ones from
/// <see cref="GetEncoding(int)"/> are cached by .NET anyway, so the list is safe to share. Lazy
/// rather than a plain initialiser so the code pages resolve through
/// <see cref="EnsureRegistered"/> rather than depending on when this class is first touched.
/// </remarks>
private static readonly Lazy<IReadOnlyList<Encoding>> _offeredEncodings = new(
() =>
[
Encoding.ASCII,
Encoding.Latin1,
Encoding.UTF8,
Encoding.Unicode,
GetEncoding(1250),
GetEncoding(1252),
GetEncoding(CODE_PAGE_GB2312)
],
LazyThreadSafetyMode.ExecutionAndPublication);

/// <summary>
/// Registers <see cref="CodePagesEncodingProvider"/> on first use.
/// </summary>
Expand Down
45 changes: 0 additions & 45 deletions src/LogExpert.Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 0 additions & 15 deletions src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1324,21 +1324,6 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingToolStripMenuItem" xml:space="preserve">
<value>Encoding</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem" xml:space="preserve">
<value>ASCII</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem" xml:space="preserve">
<value>ANSI</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem" xml:space="preserve">
<value>ISO-8859-1</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem" xml:space="preserve">
<value>UTF8</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem" xml:space="preserve">
<value>Unicode</value>
</data>
<data name="LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox" xml:space="preserve">
<value>+00:00:00.000</value>
</data>
Expand Down
15 changes: 0 additions & 15 deletions src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1347,21 +1347,6 @@ Checked tools will appear in the icon bar. All other tools are available in the
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingToolStripMenuItem" xml:space="preserve">
<value>Encoding</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem" xml:space="preserve">
<value>ASCII</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem" xml:space="preserve">
<value>ANSI</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem" xml:space="preserve">
<value>ISO-8859-1</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem" xml:space="preserve">
<value>UTF8</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem" xml:space="preserve">
<value>Unicode</value>
</data>
<data name="LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox" xml:space="preserve">
<value>+00:00:00.000</value>
</data>
Expand Down
15 changes: 0 additions & 15 deletions src/LogExpert.Resources/Resources.zh-CN.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1966,21 +1966,6 @@ YY[YY] = 年
<data name="LogTabWindow_UI_MenuStrip_mainMenuStrip" xml:space="preserve">
<value>菜单条1</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingASCIIToolStripMenuItem" xml:space="preserve">
<value>ASCII码</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem" xml:space="preserve">
<value>美国国家标准协会</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingISO88591toolStripMenuItem" xml:space="preserve">
<value>ISO-8859-1</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF8toolStripMenuItem" xml:space="preserve">
<value>UTF8</value>
</data>
<data name="LogTabWindow_UI_ToolStripMenuItem_encodingUTF16toolStripMenuItem" xml:space="preserve">
<value>统一码</value>
</data>
<data name="LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox" xml:space="preserve">
<value>+00:00:00.000</value>
</data>
Expand Down
120 changes: 31 additions & 89 deletions src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,122 +8,64 @@
namespace LogExpert.Tests.Dialogs;

/// <summary>
/// The Preferences encoding dropdown is the only way to set <c>Preferences.DefaultEncoding</c>, so the
/// list is asserted directly — building the dialog is not needed to know what it offers.
/// The Preferences encoding combo box, which is the only way to set <c>Preferences.DefaultEncoding</c>.
/// What it offers is <see cref="EncodingRegistry.OfferedEncodings"/> and is asserted in
/// <c>OfferedEncodingsTests</c>; what is left here is the part only a real
/// <see cref="ComboBox"/> can answer.
/// </summary>
[TestFixture]
public class SettingsDialogEncodingListTests
{
[Test]
[TestCase(1250, TestName = "GetAvailableEncodings_OffersWindows1250")]
[TestCase(1252, TestName = "GetAvailableEncodings_OffersWindows1252")]
public void GetAvailableEncodings_OffersLegacyCodePage (int codePage)
{
var encodings = SettingsDialog.GetAvailableEncodings();

Assert.That(encodings.Select(encoding => encoding.CodePage), Does.Contain(codePage));
}

/// <summary>
/// Every offered encoding is saved as its name and resolved from that name on the next start, so a
/// name that cannot be resolved again would silently degrade to
/// <see cref="SettingsDialog.FallbackEncoding"/>.
/// The persist/restore round trip against a real <see cref="ComboBox"/>, configured the way the
/// dialog configures it: the reselect goes through <c>Items.IndexOf</c>, and WinForms — not NUnit —
/// has the final say on whether a row is reachable. Deliberately kept alongside the plain assertion
/// in <c>OfferedEncodingsTests</c> rather than replacing it: this one needs an STA apartment and a
/// WinForms control, and the invariant should still be pinned where that is unavailable.
/// </summary>
[Test]
public void GetAvailableEncodings_EveryEntryResolvesByItsPersistedName ()
{
var encodings = SettingsDialog.GetAvailableEncodings();

Assert.Multiple(() =>
{
foreach (var encoding in encodings)
{
Assert.That(
EncodingRegistry.TryGetEncoding(encoding.HeaderName, out _),
Is.True,
$"'{encoding.HeaderName}' cannot be resolved back from a saved preference");
}
});
}

/// <summary>
/// The dropdown renders each entry as its <see cref="Encoding.HeaderName"/>, so two entries sharing a
/// code page are two rows the user cannot tell apart. <c>Encoding.Default</c> and
/// <see cref="Encoding.UTF8"/> are both code page 65001 on .NET and both read <c>utf-8</c>.
/// </summary>
[Test]
public void GetAvailableEncodings_NoTwoEntriesShareACodePage ()
[Apartment(ApartmentState.STA)]
public void EncodingComboBox_EveryOfferedRowIsReselectableAfterARestart ()
{
var codePages = SettingsDialog.GetAvailableEncodings().Select(encoding => encoding.CodePage);

Assert.That(codePages, Is.Unique);
}
var encodings = EncodingRegistry.OfferedEncodings;

/// <summary>
/// The selection is persisted by name and restored with
/// <c>comboBoxEncoding.SelectedItem = EncodingRegistry.GetEncoding(name, …)</c>. WinForms locates that
/// item with <see cref="object.Equals(object)"/>, so an entry whose name resolves back to an instance
/// that is not equal to it can never be reselected — the row goes dead after the first restart.
/// </summary>
[Test]
public void GetAvailableEncodings_EveryEntryIsReselectableAfterARoundTrip ()
{
var encodings = SettingsDialog.GetAvailableEncodings();
using var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FormattingEnabled = true };
comboBox.Items.AddRange([.. encodings]);

Assert.Multiple(() =>
{
foreach (var encoding in encodings)
for (var row = 0; row < encodings.Count; row++)
{
var restored = EncodingRegistry.GetEncoding(encoding.HeaderName, SettingsDialog.FallbackEncoding);
comboBox.SelectedIndex = row;
var saved = ((Encoding)comboBox.SelectedItem).HeaderName;

comboBox.SelectedIndex = -1;
comboBox.SelectedItem = EncodingRegistry.GetEncoding(saved, SettingsDialog.FallbackEncoding);

Assert.That(
restored,
Is.EqualTo(encoding),
$"picking '{encoding.HeaderName}' saves a name that reselects a different entry");
Assert.That(comboBox.SelectedIndex, Is.EqualTo(row), $"row {row} ('{saved}') is unreachable after a restart");
}
});
}

/// <summary>
/// <c>FillDialog</c> and <c>SavePreferences</c> fall back to this encoding when the persisted name is
/// unusable, so it has to be an offered entry — otherwise the dropdown shows no selection and OK
/// rewrites the preference to something that was never on the list.
/// </summary>
[Test]
public void GetAvailableEncodings_OffersTheFallbackEncoding ()
{
var encodings = SettingsDialog.GetAvailableEncodings();

Assert.That(encodings, Does.Contain(SettingsDialog.FallbackEncoding));
}

/// <summary>
/// The same round trip against a real <see cref="ComboBox"/>, because the reselect actually goes
/// through <c>Items.IndexOf</c> — WinForms, not NUnit, has the final say on whether a row is
/// reachable. Deliberately kept alongside the assertion above rather than replacing it: this one
/// needs an STA apartment and a WinForms control, and the invariant should still be pinned where
/// that is unavailable.
/// The combo box has no <c>DisplayMember</c> — the dialog only sets <c>ValueMember</c>, and WinForms
/// falls back to it for the display text. Pinned because without that fallback every row would render
/// as <c>Encoding.ToString()</c>, i.e. a .NET type name, and the rows the user picks between would be
/// indistinguishable for a different reason than the one issue #688 reported.
/// </summary>
[Test]
[Apartment(ApartmentState.STA)]
public void GetAvailableEncodings_EveryOfferedRowIsReselectableInARealComboBox ()
public void EncodingComboBox_RendersEachRowAsItsHeaderName ()
{
var encodings = SettingsDialog.GetAvailableEncodings();

using var comboBox = new ComboBox { FormattingEnabled = true };
comboBox.Items.AddRange([.. encodings]);
using var comboBox = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FormattingEnabled = true };
comboBox.ValueMember = "HeaderName";
comboBox.Items.AddRange([.. EncodingRegistry.OfferedEncodings]);

Assert.Multiple(() =>
{
for (var row = 0; row < encodings.Count; row++)
foreach (Encoding encoding in comboBox.Items)
{
comboBox.SelectedIndex = row;
var saved = ((Encoding)comboBox.SelectedItem).HeaderName;

comboBox.SelectedIndex = -1;
comboBox.SelectedItem = EncodingRegistry.GetEncoding(saved, SettingsDialog.FallbackEncoding);

Assert.That(comboBox.SelectedIndex, Is.EqualTo(row), $"row {row} ('{saved}') is unreachable after a restart");
Assert.That(comboBox.GetItemText(encoding), Is.EqualTo(encoding.HeaderName));
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class EncodingJsonConverterTests
[Test]
[TestCase("windows-1250", 1250)]
[TestCase("windows-1252", 1252)]
[TestCase("gb2312", 936)]
public void ReadJson_LegacyCodePageName_ResolvesInsteadOfFallingBack (string encodingName, int expectedCodePage)
{
var encoding = Deserialize($"\"{encodingName}\"");
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpert.Tests/Encodings/EncodingRegistryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class EncodingRegistryTests
[Test]
[TestCase(1250)]
[TestCase(1252)]
[TestCase(936)]
public void GetEncoding_LegacyCodePage_Resolves (int codePage)
{
var encoding = EncodingRegistry.GetEncoding(codePage);
Expand All @@ -30,6 +31,7 @@ public void GetEncoding_LegacyCodePage_Resolves (int codePage)
[TestCase("windows-1252")]
[TestCase("utf-8")]
[TestCase("iso-8859-1")]
[TestCase("gb2312")]
public void TryGetEncoding_SupportedName_ReturnsTrueAndEncoding (string name)
{
var resolved = EncodingRegistry.TryGetEncoding(name, out var encoding);
Expand Down
Loading
Loading