Skip to content

Commit a0002df

Browse files
committed
Add additional cliloc error handling for corrupted or invalid files.
1 parent 017f71d commit a0002df

3 files changed

Lines changed: 161 additions & 39 deletions

File tree

Ultima/Helpers/MythicDecompress.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,14 @@ public static byte[] InternalDecompress(Span<byte> input)
129129

130130
return output;
131131
}
132-
catch (Exception ex)
132+
catch (InvalidDataException)
133133
{
134-
Console.WriteLine($"Error during decompression: {ex.Message}");
135134
throw;
136135
}
136+
catch (Exception ex)
137+
{
138+
throw new InvalidDataException("Mythic decompression failed: " + ex.Message, ex);
139+
}
137140
}
138141

139142
//

Ultima/StringList.cs

Lines changed: 150 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ public sealed class StringList
1515
public List<StringEntry> Entries { get; private set; }
1616
public string Language { get; }
1717

18+
/// <summary>
19+
/// Non-null when the file was loaded but parsing did not consume the full file cleanly
20+
/// (e.g. a malformed entry). Contains a human-readable description of where parsing failed
21+
/// and how many entries were salvaged. Caller should surface this to the user.
22+
/// </summary>
23+
public string LoadWarning { get; private set; }
24+
1825
private Dictionary<int, string> _stringTable;
1926
private Dictionary<int, StringEntry> _entryTable;
2027
private static byte[] _buffer = new byte[1024];
@@ -56,60 +63,166 @@ private void LoadEntry(string path)
5663
byte[] buffer = new byte[fileStream.Length];
5764
_ = fileStream.Read(buffer, 0, buffer.Length);
5865

59-
if (!TryParse(buffer, _decompress))
66+
ParseResult primary = TryParse(buffer, _decompress);
67+
if (primary.Success)
6068
{
61-
bool fallback = !_decompress;
62-
if (!TryParse(buffer, fallback))
63-
{
64-
throw new InvalidDataException($"Failed to parse cliloc file '{path}' in either compressed or uncompressed format.");
65-
}
66-
_decompress = fallback;
69+
Apply(primary);
70+
return;
71+
}
72+
73+
ParseResult fallback = TryParse(buffer, !_decompress);
74+
if (fallback.Success)
75+
{
76+
_decompress = !_decompress;
77+
Apply(fallback);
78+
return;
79+
}
80+
81+
// Both attempts failed. Prefer whichever extracted more entries — that's the format
82+
// the file was actually in, just with a corrupt section somewhere.
83+
bool keepPrimary = primary.EntriesParsed >= fallback.EntriesParsed;
84+
ParseResult best = keepPrimary ? primary : fallback;
85+
if (!keepPrimary)
86+
{
87+
_decompress = !_decompress;
6788
}
89+
90+
if (best.EntriesParsed > 0)
91+
{
92+
Apply(best);
93+
LoadWarning =
94+
$"Cliloc '{path}' parsed partially as {FormatLabel(_decompress)}: " +
95+
$"{best.EntriesParsed} entries recovered before parsing failed. {best.ErrorMessage}";
96+
return;
97+
}
98+
99+
throw new InvalidDataException(
100+
$"Failed to parse cliloc file '{path}' in either compressed or uncompressed format." +
101+
$"{Environment.NewLine} As {FormatLabel(_decompress)}: {primary.ErrorMessage}" +
102+
$"{Environment.NewLine} As {FormatLabel(!_decompress)}: {fallback.ErrorMessage}");
68103
}
69104

70-
private bool TryParse(byte[] buffer, bool decompress)
105+
private void Apply(ParseResult result)
71106
{
107+
Entries = result.Entries;
108+
_stringTable = result.StringTable;
109+
_entryTable = result.EntryTable;
110+
_header1 = result.Header1;
111+
_header2 = result.Header2;
112+
}
113+
114+
private static string FormatLabel(bool decompress) => decompress ? "compressed" : "uncompressed";
115+
116+
private struct ParseResult
117+
{
118+
public bool Success;
119+
public int EntriesParsed;
120+
public List<StringEntry> Entries;
121+
public Dictionary<int, string> StringTable;
122+
public Dictionary<int, StringEntry> EntryTable;
123+
public int Header1;
124+
public short Header2;
125+
public string ErrorMessage;
126+
}
127+
128+
private static ParseResult TryParse(byte[] buffer, bool decompress)
129+
{
130+
var result = new ParseResult
131+
{
132+
Entries = new List<StringEntry>(),
133+
StringTable = new Dictionary<int, string>(),
134+
EntryTable = new Dictionary<int, StringEntry>(),
135+
};
136+
137+
byte[] clilocData;
72138
try
73139
{
74-
byte[] clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;
140+
clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;
141+
}
142+
catch (Exception ex)
143+
{
144+
result.ErrorMessage = $"decompression failed: {ex.Message}";
145+
return result;
146+
}
75147

76-
var entries = new List<StringEntry>();
77-
var stringTable = new Dictionary<int, string>();
78-
var entryTable = new Dictionary<int, StringEntry>();
148+
// Header is 4 + 2 bytes.
149+
if (clilocData.Length < 6)
150+
{
151+
result.ErrorMessage = $"file is {clilocData.Length} bytes, smaller than the 6-byte header.";
152+
return result;
153+
}
79154

80-
using var reader = new BinaryReader(new MemoryStream(clilocData));
81-
_header1 = reader.ReadInt32();
82-
_header2 = reader.ReadInt16();
155+
using var stream = new MemoryStream(clilocData);
156+
using var reader = new BinaryReader(stream);
157+
result.Header1 = reader.ReadInt32();
158+
result.Header2 = reader.ReadInt16();
83159

84-
while (reader.BaseStream.Length != reader.BaseStream.Position)
160+
int lastNumber = -1;
161+
while (stream.Position < stream.Length)
162+
{
163+
long entryStart = stream.Position;
164+
long remaining = stream.Length - entryStart;
165+
166+
// Each entry header is 4 (number) + 1 (flag) + 2 (length) = 7 bytes.
167+
if (remaining < 7)
85168
{
86-
int number = reader.ReadInt32();
87-
byte flag = reader.ReadByte();
88-
int length = reader.ReadInt16();
169+
result.ErrorMessage =
170+
$"unexpected {remaining} trailing byte(s) at offset 0x{entryStart:X} after entry #{lastNumber}; " +
171+
$"need 7 bytes for the next entry header.";
172+
return result;
173+
}
89174

90-
if (length > _buffer.Length)
91-
{
92-
_buffer = new byte[(length + 1023) & ~1023];
93-
}
175+
int number = reader.ReadInt32();
176+
byte flag = reader.ReadByte();
177+
// Writer emits ushort; reading as signed Int16 truncates strings ≥32768 bytes to a negative length.
178+
int length = reader.ReadUInt16();
94179

95-
reader.Read(_buffer, 0, length);
96-
string text = Encoding.UTF8.GetString(_buffer, 0, length);
180+
long bodyRemaining = stream.Length - stream.Position;
181+
if (length > bodyRemaining)
182+
{
183+
result.ErrorMessage =
184+
$"entry #{number} at offset 0x{entryStart:X} declares length {length}, " +
185+
$"but only {bodyRemaining} byte(s) remain in the file " +
186+
$"(previous entry was #{lastNumber}, parsed {result.EntriesParsed} so far).";
187+
return result;
188+
}
97189

98-
var se = new StringEntry(number, text, flag);
99-
entries.Add(se);
100-
stringTable[number] = text;
101-
entryTable[number] = se;
190+
if (length > _buffer.Length)
191+
{
192+
_buffer = new byte[(length + 1023) & ~1023];
102193
}
103194

104-
Entries = entries;
105-
_stringTable = stringTable;
106-
_entryTable = entryTable;
107-
return true;
108-
}
109-
catch
110-
{
111-
return false;
195+
int read = reader.Read(_buffer, 0, length);
196+
if (read != length)
197+
{
198+
result.ErrorMessage =
199+
$"entry #{number} at offset 0x{entryStart:X} expected {length} body byte(s) " +
200+
$"but only {read} were available.";
201+
return result;
202+
}
203+
204+
string text;
205+
try
206+
{
207+
text = Encoding.UTF8.GetString(_buffer, 0, length);
208+
}
209+
catch (Exception ex)
210+
{
211+
result.ErrorMessage =
212+
$"entry #{number} at offset 0x{entryStart:X} has {length} body bytes that are not valid UTF-8: {ex.Message}";
213+
return result;
214+
}
215+
216+
var se = new StringEntry(number, text, flag);
217+
result.Entries.Add(se);
218+
result.StringTable[number] = text;
219+
result.EntryTable[number] = se;
220+
result.EntriesParsed++;
221+
lastNumber = number;
112222
}
223+
224+
result.Success = true;
225+
return result;
113226
}
114227

115228
/// <summary>

UoFiddler.Controls/UserControls/ClilocControl.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ private int Lang
6969
_cliloc = new StringList("custom2", false);
7070
break;
7171
}
72+
73+
if (!string.IsNullOrEmpty(_cliloc?.LoadWarning))
74+
{
75+
MessageBox.Show(this, _cliloc.LoadWarning, "Cliloc parsed with warnings",
76+
MessageBoxButtons.OK, MessageBoxIcon.Warning);
77+
}
7278
}
7379
}
7480

0 commit comments

Comments
 (0)