Skip to content

Commit 1dcb8a5

Browse files
committed
Implement string escaping
1 parent db6fdcb commit 1dcb8a5

7 files changed

Lines changed: 136 additions & 63 deletions

File tree

fuzz/src/parser.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ impl Default for Config {
102102
fn default() -> Self {
103103
Config {
104104
average_ws_length: 5.0,
105-
average_tree_size: 10.0,
105+
average_tree_size: 2.0,
106106
max_stringy_regex_repeats: 15,
107107
tree_depth_limit: 15,
108108
tree_node_limit: 1_000,
@@ -222,18 +222,20 @@ fn gen_elem(
222222
}
223223
PatternElement::Seq { pattern, delimiter } => {
224224
out.push(ast::Elem::SeqStart);
225-
// TODO: Handle depth limiting better than this
226-
if depth < data.tree_depth_limit && state.nodes_generated < data.tree_node_limit {
227-
let mut is_first_node = true;
228-
while state.rng.gen_range(0.0..1.0) > data.one_minus_new_segment_prob {
229-
// Add delimiter between nodes
230-
if !is_first_node {
231-
out.push(ast::Elem::SeqDelim(*delimiter, table.gen_ws(state.rng)));
232-
is_first_node = false;
233-
}
234-
// Add segment
235-
gen_pattern(pattern, out, state, depth);
225+
let mut is_first_node = true;
226+
while state.rng.gen_range(0.0..1.0) > data.one_minus_new_segment_prob {
227+
// TODO: Handle depth limiting better than this
228+
if depth > data.tree_depth_limit || state.nodes_generated > data.tree_node_limit {
229+
break;
236230
}
231+
232+
// Add delimiter between nodes
233+
if !is_first_node {
234+
out.push(ast::Elem::SeqDelim(*delimiter, table.gen_ws(state.rng)));
235+
}
236+
// Add segment
237+
gen_pattern(pattern, out, state, depth);
238+
is_first_node = false;
237239
}
238240
out.push(ast::Elem::SeqEnd);
239241
}

grammar/src/char_set.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{convert::identity, iter::FromIterator, ops::RangeInclusive};
22

33
use rand::Rng;
44

5-
const FIRST_NON_ASCII_CHAR: char = '\u{128}';
5+
const FIRST_NON_ASCII_CHAR: char = '\u{80}';
66

77
/// An set of [`char`], optimised for sets where adjacent [`char`]s are very likely to either both
88
/// be included or both excluded.

grammar/src/grammar.rs

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use std::{
22
collections::HashSet,
3-
fmt::{Debug, Formatter},
3+
fmt::{Debug, Formatter, Write},
44
};
55

66
use bimap::BiMap;
77
use index_vec::{IndexSlice, IndexVec};
88
use itertools::Itertools;
99
use regex::Regex;
10-
use serde::Deserialize;
1110

1211
use crate::{
1312
char_set::{self, CharSet},
@@ -195,13 +194,20 @@ impl Stringy {
195194
pub fn create_display_string(&self, contents: &str) -> String {
196195
let mut display_str = String::with_capacity(contents.len() + 20);
197196
display_str.push_str(&self.delim_start);
198-
for c in contents.chars() {
199-
// TODO: handle escaping
200-
display_str.push(c);
197+
for ch in contents.chars() {
198+
self.write_escaped_char(ch, &mut display_str);
201199
}
202200
display_str.push_str(&self.delim_end);
203201
display_str
204202
}
203+
204+
/// Writes the escaped version of `c` to a [`String`]
205+
pub fn write_escaped_char(&self, ch: char, out: &mut String) {
206+
match &self.escape_rules {
207+
Some(r) => r.write_escaped_char(ch, out),
208+
None => out.push(ch),
209+
}
210+
}
205211
}
206212

207213
/// The [`Regex`]es required to specify the valid strings of a [`Stringy`] node
@@ -212,29 +218,49 @@ pub struct Regexes {
212218
pub(crate) anchored_both: Regex,
213219
}
214220

215-
#[derive(Debug, Clone, Deserialize)]
216-
#[serde(deny_unknown_fields)]
221+
/// Rules for how `char`s should be escaped
222+
#[derive(Debug, Clone)]
217223
pub struct EscapeRules {
218224
/// A non-empty string that all escape sequences must start with. For example, in JSON strings
219225
/// this is `\`
220226
pub(crate) start_sequence: String,
221-
/// Maps escape sequences (to go after `start_sequence`) to the de-escaped [`String`]. For
222-
/// example, for JSON strings this is:
227+
/// Maps chars to their escape sequence (i.e. the string which goes after
228+
/// `self.start_sequence`). For example, for JSON strings this is:
223229
/// ```text
224-
/// `\` -> '\\' (i.e. `\\` de-escapes to `\`)
225-
/// `"` -> '"' (i.e. `\"` de-escapes to `"`)
226-
/// `/` -> '/'
227-
/// `n` -> '\n'
228-
/// `t` -> '\t'
229-
/// `b` -> '\u{8}'
230-
/// `f` -> '\u{c}'
231-
/// `r` -> '\r'
230+
/// '\\' <-> "\\" (i.e. `\\` de-escapes to `\`)
231+
/// '"' <-> "\"" (i.e. `\"` de-escapes to `"`)
232+
/// '/' <-> "/"
233+
/// '\n' <-> "n"
234+
/// '\t' <-> "t"
235+
/// '\u{8}' <-> "b"
236+
/// '\u{c}' <-> "f"
237+
/// '\r' <-> "r"
232238
/// ```
233-
pub(crate) rules: BiMap<String, String>,
239+
pub(crate) rules: BiMap<char, String>,
234240
/// The prefix which takes 4 hex symbols and de-escapes them to that unicode code-point. For
235241
/// example, in JSON strings this is `u` (i.e. `\uABCD` would turn into the unicode code point
236242
/// `0xABCD`).
237243
pub(crate) unicode_hex_4: Option<String>,
244+
/// A set of `char`s which should not be escaped, even if a rule for them exists.
245+
pub(crate) dont_escape: CharSet,
246+
}
247+
248+
impl EscapeRules {
249+
fn write_escaped_char(&self, ch: char, out: &mut String) {
250+
if self.dont_escape.contains(ch) {
251+
// Char escaping overriden by `self.dont_escape`
252+
out.push(ch);
253+
} else if let Some(escape_seq) = self.rules.get_by_left(&ch) {
254+
// Char can be escaped using a specific escape sequence
255+
out.push_str(&self.start_sequence);
256+
out.push_str(escape_seq);
257+
} else if let Some(unicode_hex_4_prefix) = &self.unicode_hex_4 {
258+
// If 4-digit hex encoding is defined, then it's valid for all chars
259+
out.push_str(&self.start_sequence);
260+
out.push_str(unicode_hex_4_prefix);
261+
write!(out, "{:04X}", ch as u32).unwrap();
262+
}
263+
}
238264
}
239265

240266
//////////////

grammar/src/spec/convert.rs

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -156,35 +156,19 @@ fn convert_type(
156156
} => {
157157
assert!(stringy); // stringy should always be set to `true`
158158

159-
let validity_regex = validity_regex
159+
let regexes = validity_regex
160160
// Compile two copies of the regex
161-
.map(|regex_str| {
162-
macro_rules! compile_regex {
163-
($string: expr) => {
164-
Regex::new(&$string).map_err(|inner| ConvertError::Regex {
165-
type_name: name.to_owned(),
166-
regex: $string,
167-
inner,
168-
})?;
169-
};
170-
}
171-
172-
let str_unanchored = format!("(?x: {} )", regex_str);
173-
let str_anchor_start = format!("^{}", str_unanchored);
174-
let str_anchor_both = format!("^{}$", str_unanchored);
175-
176-
Ok(grammar::Regexes {
177-
unanchored: compile_regex!(str_unanchored),
178-
anchored_start: compile_regex!(str_anchor_start),
179-
anchored_both: compile_regex!(str_anchor_both),
180-
})
181-
})
182-
// Convert the `Option<Result<R, E>>` into a `Result<Option<R>, E>`
161+
.map(|regex_str| convert_validity_regex(&regex_str, &name))
162+
// Use `?` on the `Result` inside the `Option`. I.e. convert a
163+
// `Option<Result<T, E>>` to `Option<T>`, returning `Err(E)` if needed
164+
.transpose()?;
165+
let escape_rules = escape_rules
166+
.map(|rules| convert_escape_rules(rules))
183167
.transpose()?;
184168
let inner = grammar::Stringy {
185169
delim_start,
186170
delim_end,
187-
regexes: validity_regex,
171+
regexes,
188172
default_content,
189173
escape_rules,
190174
};
@@ -208,6 +192,21 @@ fn convert_type(
208192
})
209193
}
210194

195+
fn convert_escape_rules(rules: super::EscapeRules) -> ConvertResult<grammar::EscapeRules> {
196+
let super::EscapeRules {
197+
start_sequence,
198+
rules,
199+
unicode_hex_4,
200+
dont_escape,
201+
} = rules;
202+
Ok(grammar::EscapeRules {
203+
start_sequence,
204+
rules,
205+
unicode_hex_4,
206+
dont_escape: convert_char_set(dont_escape)?,
207+
})
208+
}
209+
211210
//////////////////////
212211
// TYPE DESCENDANTS //
213212
//////////////////////
@@ -287,9 +286,9 @@ fn enumerate_type_descendants(
287286
Ok(())
288287
}
289288

290-
/////////////////////////
291-
// PATTERNS/WHITESPACE //
292-
/////////////////////////
289+
//////////////
290+
// PATTERNS //
291+
//////////////
293292

294293
fn compile_pattern(
295294
elems: super::Pattern,
@@ -321,6 +320,32 @@ fn compile_pattern_element(
321320
})
322321
}
323322

323+
///////////////////////////////
324+
// REGEX/WHITESPACE/CHAR SET //
325+
///////////////////////////////
326+
327+
fn convert_validity_regex(regex_str: &str, type_name: &str) -> ConvertResult<grammar::Regexes> {
328+
macro_rules! compile_regex {
329+
($string: expr) => {
330+
Regex::new(&$string).map_err(|inner| ConvertError::Regex {
331+
type_name: type_name.to_owned(),
332+
regex: $string,
333+
inner,
334+
})?;
335+
};
336+
}
337+
338+
let str_unanchored = format!("(?x: {} )", regex_str);
339+
let str_anchor_start = format!("^{}", str_unanchored);
340+
let str_anchor_both = format!("^{}$", str_unanchored);
341+
342+
Ok(grammar::Regexes {
343+
unanchored: compile_regex!(str_unanchored),
344+
anchored_start: compile_regex!(str_anchor_start),
345+
anchored_both: compile_regex!(str_anchor_both),
346+
})
347+
}
348+
324349
fn convert_whitespace(ws_chars: super::CharSet) -> ConvertResult<grammar::Whitespace> {
325350
convert_char_set(ws_chars).map(grammar::Whitespace::from)
326351
}

grammar/src/spec/mod.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ pub(crate) mod convert;
1212

1313
use std::collections::HashMap;
1414

15+
use bimap::BiMap;
1516
use serde::Deserialize;
1617

17-
use crate::{grammar, Grammar};
18+
use crate::Grammar;
1819

1920
use self::convert::ConvertResult;
2021

@@ -83,7 +84,7 @@ pub(crate) enum Type {
8384
validity_regex: Option<String>,
8485

8586
#[serde(rename = "escape")]
86-
escape_rules: Option<grammar::EscapeRules>,
87+
escape_rules: Option<self::EscapeRules>,
8788
},
8889
}
8990

@@ -114,6 +115,23 @@ pub(crate) enum PatternElement {
114115
},
115116
}
116117

118+
/// See [`grammar::EscapeRules`] for docs.
119+
#[derive(Debug, Clone, Deserialize)]
120+
#[serde(deny_unknown_fields)]
121+
pub struct EscapeRules {
122+
/// A non-empty string that all escape sequences must start with. For example, in JSON strings
123+
/// this is `\`
124+
pub(crate) start_sequence: String,
125+
/// See [`grammar::EscapeRules::rules`] for docs.
126+
pub(crate) rules: BiMap<char, String>,
127+
/// The prefix which takes 4 hex symbols and de-escapes them to that unicode code-point. For
128+
/// example, in JSON strings this is `u` (i.e. `\uABCD` would turn into the unicode code point
129+
/// `0xABCD`).
130+
pub(crate) unicode_hex_4: Option<String>,
131+
/// A set of `char`s which should not be escaped, even if a rule for them exists.
132+
pub(crate) dont_escape: self::CharSet,
133+
}
134+
117135
/// A set of `char`s, expressed as the contents of `[`, `]` in a regex (e.g. `a-zA-Z` will
118136
/// correspond to the regex `[a-zA-Z]`).
119137
#[derive(Debug, Clone, Deserialize)]

grammar/src/tokenizer/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,9 @@ fn eat_stringy<'s>(
235235
// Check if we have an escape string
236236
if ch == esc_start_char {
237237
// Consume explicit escapes if needed
238-
for (escaped, content) in &esc_rules.rules {
238+
for (content, escaped) in &esc_rules.rules {
239239
if iter.eat(escaped) {
240-
contents.push_str(content);
240+
contents.push(*content);
241241
continue 'outer;
242242
}
243243
}

json.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@ default = ""
5050

5151
escape.start_sequence = '\'
5252
# `\b` is backspace, `\f` is form feed
53-
escape.rules = { '"'='"', '\'='\', '/'='/', 'b'="\b", 'f'="\f", 'r'="\r", 'n'="\n", 't'="\t" }
53+
escape.rules = { '"'='"', '\'='\', '/'='/', "\b"='b', "\f"='f', "\r"='r', "\n"='n', "\t"='t' }
5454
escape.unicode_hex_4 = 'u' # `\uWXYZ` -> `<unicode codepoint 0xWXYZ>`
55+
escape.dont_escape = '[[:print:]]--["\\]' # Always escape quote and backslash, but don't escape any
56+
# other printable char

0 commit comments

Comments
 (0)