@@ -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>
0 commit comments