@@ -25,6 +25,11 @@ pub enum Error {
2525}
2626use Error :: * ;
2727
28+ enum LossyEscape {
29+ Escaped { char : char , len : usize } ,
30+ Literal ( usize ) ,
31+ }
32+
2833/// Unescaper struct which holding the chars cache for unescaping.
2934#[ derive( Debug ) ]
3035pub struct Unescaper {
@@ -167,3 +172,179 @@ impl Unescaper {
167172pub fn unescape ( s : & str ) -> Result < String > {
168173 Unescaper :: new ( s) . unescape ( )
169174}
175+
176+ /// Unescape the given [`str`], preserving malformed escape sequences as written.
177+ ///
178+ /// This is useful for user-provided text where valid escape sequences should be interpreted, but
179+ /// malformed escape sequences should not make the whole string fail.
180+ pub fn unescape_lossy ( s : & str ) -> String {
181+ let mut unescaped = String :: new ( ) ;
182+ let mut cursor = 0 ;
183+
184+ while cursor < s. len ( ) {
185+ let remaining = & s[ cursor..] ;
186+ let c = remaining. chars ( ) . next ( ) . expect ( "cursor is in bounds; qed" ) ;
187+
188+ if c != '\\' {
189+ unescaped. push ( c) ;
190+ cursor += c. len_utf8 ( ) ;
191+
192+ continue ;
193+ }
194+
195+ match parse_lossy_escape ( remaining) {
196+ LossyEscape :: Escaped { char, len } => {
197+ unescaped. push ( char) ;
198+ cursor += len;
199+ } ,
200+ LossyEscape :: Literal ( len) => {
201+ unescaped. push_str ( & remaining[ ..len] ) ;
202+ cursor += len;
203+ } ,
204+ }
205+ }
206+
207+ unescaped
208+ }
209+
210+ fn parse_lossy_escape ( s : & str ) -> LossyEscape {
211+ let mut chars = s. char_indices ( ) ;
212+ let Some ( ( _, '\\' ) ) = chars. next ( ) else {
213+ unreachable ! ( "lossy escape parser is only called at escapes" )
214+ } ;
215+ let Some ( ( marker_pos, marker) ) = chars. next ( ) else {
216+ return LossyEscape :: Literal ( '\\' . len_utf8 ( ) ) ;
217+ } ;
218+
219+ match marker {
220+ 'b' => LossyEscape :: Escaped { char : '\u{0008}' , len : marker_pos + marker. len_utf8 ( ) } ,
221+ 'f' => LossyEscape :: Escaped { char : '\u{000c}' , len : marker_pos + marker. len_utf8 ( ) } ,
222+ 'n' => LossyEscape :: Escaped { char : '\n' , len : marker_pos + marker. len_utf8 ( ) } ,
223+ 'r' => LossyEscape :: Escaped { char : '\r' , len : marker_pos + marker. len_utf8 ( ) } ,
224+ 't' => LossyEscape :: Escaped { char : '\t' , len : marker_pos + marker. len_utf8 ( ) } ,
225+ 'v' => LossyEscape :: Escaped { char : '\u{000b}' , len : marker_pos + marker. len_utf8 ( ) } ,
226+ '\'' | '\"' | '\\' | '/' =>
227+ LossyEscape :: Escaped { char : marker, len : marker_pos + marker. len_utf8 ( ) } ,
228+ 'u' => parse_lossy_unicode_escape ( s, marker_pos + marker. len_utf8 ( ) ) ,
229+ 'x' => parse_lossy_byte_escape ( s, marker_pos + marker. len_utf8 ( ) ) ,
230+ '0' ..='7' => parse_lossy_octal_escape ( s, marker_pos, marker) ,
231+ _ => LossyEscape :: Literal ( marker_pos + marker. len_utf8 ( ) ) ,
232+ }
233+ }
234+
235+ fn parse_lossy_unicode_escape ( s : & str , after_marker : usize ) -> LossyEscape {
236+ if s[ after_marker..] . starts_with ( '{' ) {
237+ let digits_start = after_marker + '{' . len_utf8 ( ) ;
238+
239+ if let Some ( closing_offset) = s[ digits_start..] . find ( '}' ) {
240+ let closing = digits_start + closing_offset;
241+ let digits = & s[ digits_start..closing] ;
242+ let len = closing + '}' . len_utf8 ( ) ;
243+
244+ return u32:: from_str_radix ( digits, 16 )
245+ . ok ( )
246+ . and_then ( char:: from_u32)
247+ . map_or ( LossyEscape :: Literal ( len) , |char| LossyEscape :: Escaped { char, len } ) ;
248+ }
249+
250+ let len = incomplete_braced_unicode_escape_len ( s, digits_start) ;
251+
252+ if len == s. len ( ) {
253+ let digits = & s[ digits_start..] ;
254+
255+ return u32:: from_str_radix ( digits, 16 )
256+ . ok ( )
257+ . and_then ( char:: from_u32)
258+ . map_or ( LossyEscape :: Literal ( len) , |char| LossyEscape :: Escaped { char, len } ) ;
259+ }
260+
261+ return LossyEscape :: Literal ( len) ;
262+ }
263+
264+ let Some ( ( digits, len) ) = fixed_width_escape_digits ( & s[ after_marker..] , 4 ) else {
265+ return LossyEscape :: Literal ( incomplete_fixed_width_escape_len ( s, after_marker, 4 ) ) ;
266+ } ;
267+
268+ u32:: from_str_radix ( digits, 16 ) . ok ( ) . and_then ( char:: from_u32) . map_or (
269+ LossyEscape :: Literal ( after_marker + len) ,
270+ |char| LossyEscape :: Escaped { char, len : after_marker + len } ,
271+ )
272+ }
273+
274+ fn parse_lossy_byte_escape ( s : & str , after_marker : usize ) -> LossyEscape {
275+ let Some ( ( digits, len) ) = fixed_width_escape_digits ( & s[ after_marker..] , 2 ) else {
276+ return LossyEscape :: Literal ( incomplete_fixed_width_escape_len ( s, after_marker, 2 ) ) ;
277+ } ;
278+
279+ u8:: from_str_radix ( digits, 16 ) . map_or ( LossyEscape :: Literal ( after_marker + len) , |byte| {
280+ LossyEscape :: Escaped { char : byte as _ , len : after_marker + len }
281+ } )
282+ }
283+
284+ fn fixed_width_escape_digits ( s : & str , width : usize ) -> Option < ( & str , usize ) > {
285+ let mut count = 0 ;
286+
287+ for ( pos, c) in s. char_indices ( ) {
288+ if c == '\\' {
289+ break ;
290+ }
291+
292+ count += 1 ;
293+
294+ if count == width {
295+ let end = pos + c. len_utf8 ( ) ;
296+
297+ return Some ( ( & s[ ..end] , end) ) ;
298+ }
299+ }
300+
301+ None
302+ }
303+
304+ fn incomplete_fixed_width_escape_len ( s : & str , after_marker : usize , width : usize ) -> usize {
305+ let mut end = after_marker;
306+
307+ for ( count, ( pos, c) ) in s[ after_marker..] . char_indices ( ) . enumerate ( ) {
308+ if c == '\\' || count == width {
309+ break ;
310+ }
311+
312+ end = after_marker + pos + c. len_utf8 ( ) ;
313+ }
314+
315+ end
316+ }
317+
318+ fn incomplete_braced_unicode_escape_len ( s : & str , digits_start : usize ) -> usize {
319+ let mut end = digits_start;
320+
321+ for ( pos, c) in s[ digits_start..] . char_indices ( ) {
322+ if c == '\\' {
323+ break ;
324+ }
325+
326+ end = digits_start + pos + c. len_utf8 ( ) ;
327+ }
328+
329+ end
330+ }
331+
332+ fn parse_lossy_octal_escape ( s : & str , marker_pos : usize , marker : char ) -> LossyEscape {
333+ let max_extra_digits = if matches ! ( marker, '0' ..='3' ) { 2 } else { 1 } ;
334+ let mut octal = String :: from ( marker) ;
335+ let mut len = marker_pos + marker. len_utf8 ( ) ;
336+ let mut end = len;
337+
338+ for ( pos, c) in s[ len..] . char_indices ( ) . take ( max_extra_digits) {
339+ if !c. is_digit ( 8 ) {
340+ break ;
341+ }
342+
343+ octal. push ( c) ;
344+ end = len + pos + c. len_utf8 ( ) ;
345+ }
346+ len = end;
347+
348+ u8:: from_str_radix ( & octal, 8 )
349+ . map_or ( LossyEscape :: Literal ( len) , |byte| LossyEscape :: Escaped { char : byte as _ , len } )
350+ }
0 commit comments