fix: make locale-independence real, not assumed — parsing, casing, comparison
Follow-up to the retail-text fix. "Green under sv-SE" is not the same as "runs on any locale", so this establishes the latter by running the suite under cultures chosen to break different things, and fixing what they broke. ar-SA found a genuine defect the Swedish runner cannot see: the resolution parser read "1920x-1" through the ambient culture, and ar-SA's negative sign is not ASCII '-', so the parse failed and the height silently became 0 instead of -1. Both copies of that parser (App settings targets and the UI settings store) now parse invariantly. Audited every remaining culture-sensitive operation in src/ rather than fixing only what a test happened to catch: - Numeric Parse/TryParse with no IFormatProvider: 11 sites, all reading MACHINE-readable input — env vars (ACDREAM_LIGHT_DEBUG, ACDREAM_NET_DROP_*, streaming/quality knobs), CLI arguments, "1920x1080" settings keys, a chat command's price argument, and the launcher's bake thread count, which is handed straight to a child process command line. All pinned to InvariantCulture. - ToUpper()/ToLower() with no culture: none. The Turkish-I class was already clean, and tr-TR confirms it. - StartsWith/EndsWith/IndexOf(string) with no StringComparison: one — ChatInputParser's "@" prefix test, which is a culture-sensitive comparison for a single ASCII character. Now the ordinal char overload. Verified: 13,958 tests pass identically under the machine default, sv-SE, tr-TR, ar-SA, and de-DE. (The two launcher test assemblies are excluded from this run only because a running acdream-launcher.exe holds its own binary; the one launcher change here is the thread-count parse.) Dates remain on the current culture by intent, unchanged from the previous commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6a15dd063c
commit
955c618013
11 changed files with 64 additions and 16 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -134,8 +135,10 @@ internal static class DisplayModeCatalog
|
||||||
mode = default;
|
mode = default;
|
||||||
string[] parts = spec.Split('x', 2);
|
string[] parts = spec.Split('x', 2);
|
||||||
if (parts.Length == 2
|
if (parts.Length == 2
|
||||||
&& int.TryParse(parts[0], out int w)
|
&& int.TryParse(
|
||||||
&& int.TryParse(parts[1], out int h)
|
parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int w)
|
||||||
|
&& int.TryParse(
|
||||||
|
parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int h)
|
||||||
&& w > 0
|
&& w > 0
|
||||||
&& h > 0)
|
&& h > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AcDream.App.Audio;
|
using AcDream.App.Audio;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
|
|
@ -206,9 +207,14 @@ internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarg
|
||||||
if (string.IsNullOrWhiteSpace(spec))
|
if (string.IsNullOrWhiteSpace(spec))
|
||||||
return false;
|
return false;
|
||||||
string[] parts = spec.Split('x', 2);
|
string[] parts = spec.Split('x', 2);
|
||||||
|
// Invariant: a resolution spec is a machine-readable settings key, not
|
||||||
|
// localized input. Parsing it with the ambient culture made "1920x-1"
|
||||||
|
// fail outright under ar-SA, whose negative sign is not ASCII "-".
|
||||||
return parts.Length == 2
|
return parts.Length == 2
|
||||||
&& int.TryParse(parts[0], out width)
|
&& int.TryParse(
|
||||||
&& int.TryParse(parts[1], out height)
|
parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out width)
|
||||||
|
&& int.TryParse(
|
||||||
|
parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out height)
|
||||||
&& width > 0
|
&& width > 0
|
||||||
&& height > 0;
|
&& height > 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
@ -73,5 +74,8 @@ internal static class StreamingDiagnostics
|
||||||
/// at the parser rather than let the two halves of one commit disagree.
|
/// at the parser rather than let the two halves of one commit disagree.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static int? ParseRadius(string? raw) =>
|
internal static int? ParseRadius(string? raw) =>
|
||||||
int.TryParse(raw, out int value) && value >= 1 ? value : null;
|
int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)
|
||||||
|
&& value >= 1
|
||||||
|
? value
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Ui;
|
using AcDream.Core.Ui;
|
||||||
using AcDream.Core.Social;
|
using AcDream.Core.Social;
|
||||||
|
|
@ -724,7 +725,8 @@ public sealed class ClientCommandController
|
||||||
uint maximumPrice = 0u;
|
uint maximumPrice = 0u;
|
||||||
foreach (string part in parts)
|
foreach (string part in parts)
|
||||||
{
|
{
|
||||||
if (uint.TryParse(part, out uint price))
|
if (uint.TryParse(
|
||||||
|
part, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint price))
|
||||||
{
|
{
|
||||||
if (price == 0)
|
if (price == 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using AcDream.Cli;
|
using AcDream.Cli;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
|
|
@ -96,7 +97,7 @@ if (args.Length >= 1 && args[0] == "probe")
|
||||||
{
|
{
|
||||||
// probe <in.png> <x0> <y0> <x1> <y1>
|
// probe <in.png> <x0> <y0> <x1> <y1>
|
||||||
if (args.Length < 6) { Console.Error.WriteLine("usage: AcDream.Cli probe <in.png> <x0> <y0> <x1> <y1>"); return 2; }
|
if (args.Length < 6) { Console.Error.WriteLine("usage: AcDream.Cli probe <in.png> <x0> <y0> <x1> <y1>"); return 2; }
|
||||||
return VitalsMockup.Probe(args[1], int.Parse(args[2]), int.Parse(args[3]), int.Parse(args[4]), int.Parse(args[5]));
|
return VitalsMockup.Probe(args[1], int.Parse(args[2], CultureInfo.InvariantCulture), int.Parse(args[3], CultureInfo.InvariantCulture), int.Parse(args[4], CultureInfo.InvariantCulture), int.Parse(args[5], CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Length >= 1 && args[0] == "crop")
|
if (args.Length >= 1 && args[0] == "crop")
|
||||||
|
|
@ -104,7 +105,7 @@ if (args.Length >= 1 && args[0] == "crop")
|
||||||
// crop <in.png> <x> <y> <w> <h> <zoom> <out.png>
|
// crop <in.png> <x> <y> <w> <h> <zoom> <out.png>
|
||||||
if (args.Length < 8) { Console.Error.WriteLine("usage: AcDream.Cli crop <in.png> <x> <y> <w> <h> <zoom> <out.png>"); return 2; }
|
if (args.Length < 8) { Console.Error.WriteLine("usage: AcDream.Cli crop <in.png> <x> <y> <w> <h> <zoom> <out.png>"); return 2; }
|
||||||
return VitalsMockup.Crop(args[1],
|
return VitalsMockup.Crop(args[1],
|
||||||
int.Parse(args[2]), int.Parse(args[3]), int.Parse(args[4]), int.Parse(args[5]), int.Parse(args[6]), args[7]);
|
int.Parse(args[2], CultureInfo.InvariantCulture), int.Parse(args[3], CultureInfo.InvariantCulture), int.Parse(args[4], CultureInfo.InvariantCulture), int.Parse(args[5], CultureInfo.InvariantCulture), int.Parse(args[6], CultureInfo.InvariantCulture), args[7]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Length >= 1 && args[0] == "dump-edges")
|
if (args.Length >= 1 && args[0] == "dump-edges")
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
namespace AcDream.Core.Net;
|
namespace AcDream.Core.Net;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -74,6 +75,8 @@ public static class NetDiagnostics
|
||||||
public static int NetDropSeed { get; set; } =
|
public static int NetDropSeed { get; set; } =
|
||||||
int.TryParse(
|
int.TryParse(
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_NET_DROP_SEED"),
|
Environment.GetEnvironmentVariable("ACDREAM_NET_DROP_SEED"),
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
out int seed)
|
out int seed)
|
||||||
? seed
|
? seed
|
||||||
: 1;
|
: 1;
|
||||||
|
|
@ -87,7 +90,8 @@ public static class NetDiagnostics
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_NET_DROP_DIR"));
|
Environment.GetEnvironmentVariable("ACDREAM_NET_DROP_DIR"));
|
||||||
|
|
||||||
internal static int ParseDropPercent(string? value) =>
|
internal static int ParseDropPercent(string? value) =>
|
||||||
int.TryParse(value, out int percent) && percent is >= 0 and <= 100
|
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int percent)
|
||||||
|
&& percent is >= 0 and <= 100
|
||||||
? percent
|
? percent
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -355,7 +356,13 @@ public static class RenderingDiagnostics
|
||||||
/// future DebugPanel mirror.
|
/// future DebugPanel mirror.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static int LightDebugMode { get; set; } =
|
public static int LightDebugMode { get; set; } =
|
||||||
int.TryParse(Environment.GetEnvironmentVariable("ACDREAM_LIGHT_DEBUG"), out var ldm) ? ldm : 0;
|
int.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_LIGHT_DEBUG"),
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out int ldm)
|
||||||
|
? ldm
|
||||||
|
: 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #176 stripe-hunt isolation (<c>ACDREAM_CLIP_DEBUG=1</c>) — throwaway
|
/// #176 stripe-hunt isolation (<c>ACDREAM_CLIP_DEBUG=1</c>) — throwaway
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using AcDream.Launcher.Core.Installation;
|
using AcDream.Launcher.Core.Installation;
|
||||||
using AcDream.Launcher.Core.Launching;
|
using AcDream.Launcher.Core.Launching;
|
||||||
|
|
@ -90,7 +91,9 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsThreadCountValid =>
|
public bool IsThreadCountValid =>
|
||||||
int.TryParse(ThreadsText, out int threads) && threads > 0;
|
int.TryParse(
|
||||||
|
ThreadsText, NumberStyles.Integer, CultureInfo.InvariantCulture, out int threads)
|
||||||
|
&& threads > 0;
|
||||||
|
|
||||||
public string ThreadCountValidation => IsThreadCountValid
|
public string ThreadCountValidation => IsThreadCountValid
|
||||||
? "Worker count is valid."
|
? "Worker count is valid."
|
||||||
|
|
@ -324,7 +327,12 @@ public sealed class FirstRunInstallerViewModel : ObservableObject, IDisposable
|
||||||
|
|
||||||
private async Task StartAsync()
|
private async Task StartAsync()
|
||||||
{
|
{
|
||||||
if (!int.TryParse(ThreadsText, out int threads) || threads <= 0)
|
if (!int.TryParse(
|
||||||
|
ThreadsText,
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out int threads)
|
||||||
|
|| threads <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ public static class ChatInputParser
|
||||||
// (e.g. @acehelp, @tele, @die), pass the literal @-prefixed
|
// (e.g. @acehelp, @tele, @die), pass the literal @-prefixed
|
||||||
// text through to the default channel so ACE's CommandManager
|
// text through to the default channel so ACE's CommandManager
|
||||||
// server-side handler intercepts it.
|
// server-side handler intercepts it.
|
||||||
if (trimmed.StartsWith("@"))
|
if (trimmed.StartsWith('@'))
|
||||||
{
|
{
|
||||||
string substituted = "/" + trimmed.Substring(1);
|
string substituted = "/" + trimmed.Substring(1);
|
||||||
string verb = ExtractVerb(substituted);
|
string verb = ExtractVerb(substituted);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
@ -632,8 +633,16 @@ public sealed class SettingsStore
|
||||||
height = 0;
|
height = 0;
|
||||||
int separator = key.IndexOf('x', StringComparison.OrdinalIgnoreCase);
|
int separator = key.IndexOf('x', StringComparison.OrdinalIgnoreCase);
|
||||||
return separator > 0
|
return separator > 0
|
||||||
&& int.TryParse(key.AsSpan(0, separator), out width)
|
&& int.TryParse(
|
||||||
&& int.TryParse(key.AsSpan(separator + 1), out height)
|
key.AsSpan(0, separator),
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out width)
|
||||||
|
&& int.TryParse(
|
||||||
|
key.AsSpan(separator + 1),
|
||||||
|
NumberStyles.Integer,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out height)
|
||||||
&& width > 0
|
&& width > 0
|
||||||
&& height > 0;
|
&& height > 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Globalization;
|
||||||
namespace AcDream.UI.Abstractions.Settings;
|
namespace AcDream.UI.Abstractions.Settings;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -62,6 +63,9 @@ public readonly record struct QualitySettings(
|
||||||
private static int TryParseEnvInt(string name, int defaultValue)
|
private static int TryParseEnvInt(string name, int defaultValue)
|
||||||
{
|
{
|
||||||
var s = System.Environment.GetEnvironmentVariable(name);
|
var s = System.Environment.GetEnvironmentVariable(name);
|
||||||
return s is not null && int.TryParse(s, out var v) ? v : defaultValue;
|
return s is not null
|
||||||
|
&& int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)
|
||||||
|
? v
|
||||||
|
: defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue