@@ -152,24 +152,17 @@ fn emit_state_attributes(state: &Value, writer: &mut dyn ResponseWriter) -> Resu
152152 // Emit scalar values as individual attributes
153153 for ( key, value) in map {
154154 let val_str = match value {
155- Value :: String ( s) => s. clone ( ) ,
156- Value :: Number ( n) => n. to_string ( ) ,
157- Value :: Bool ( b) => b. to_string ( ) ,
155+ Value :: String ( s) => std:: borrow:: Cow :: Borrowed ( s. as_str ( ) ) ,
156+ Value :: Number ( n) => std:: borrow:: Cow :: Owned ( n. to_string ( ) ) ,
157+ Value :: Bool ( true ) => std:: borrow:: Cow :: Borrowed ( "true" ) ,
158+ Value :: Bool ( false ) => std:: borrow:: Cow :: Borrowed ( "false" ) ,
158159 _ => continue ,
159160 } ;
160161 let attr_name = camel_to_kebab ( key) ;
161162 writer. write ( " " ) ?;
162163 writer. write ( & attr_name) ?;
163164 writer. write ( "=\" " ) ?;
164- for ch in val_str. chars ( ) {
165- match ch {
166- '&' => writer. write ( "&" ) ?,
167- '"' => writer. write ( """ ) ?,
168- '<' => writer. write ( "<" ) ?,
169- '>' => writer. write ( ">" ) ?,
170- _ => writer. write ( & String :: from ( ch) ) ?,
171- }
172- }
165+ write_escaped_state_attr ( writer, val_str. as_ref ( ) ) ?;
173166 writer. write ( "\" " ) ?;
174167 }
175168
@@ -178,16 +171,36 @@ fn emit_state_attributes(state: &Value, writer: &mut dyn ResponseWriter) -> Resu
178171 if has_complex {
179172 let json_str = state. to_string ( ) ;
180173 writer. write ( " data-state=\" " ) ?;
181- for ch in json_str. chars ( ) {
182- match ch {
183- '&' => writer. write ( "&" ) ?,
184- '"' => writer. write ( """ ) ?,
185- '<' => writer. write ( "<" ) ?,
186- '>' => writer. write ( ">" ) ?,
187- _ => writer. write ( & String :: from ( ch) ) ?,
174+ write_escaped_state_attr ( writer, & json_str) ?;
175+ writer. write ( "\" " ) ?;
176+ }
177+
178+ Ok ( ( ) )
179+ }
180+
181+ fn write_escaped_state_attr ( writer : & mut dyn ResponseWriter , value : & str ) -> Result < ( ) > {
182+ let mut last = 0 ;
183+
184+ for ( index, ch) in value. char_indices ( ) {
185+ let escaped = match ch {
186+ '&' => Some ( "&" ) ,
187+ '"' => Some ( """ ) ,
188+ '<' => Some ( "<" ) ,
189+ '>' => Some ( ">" ) ,
190+ _ => None ,
191+ } ;
192+
193+ if let Some ( entity) = escaped {
194+ if last < index {
195+ writer. write ( & value[ last..index] ) ?;
188196 }
197+ writer. write ( entity) ?;
198+ last = index + ch. len_utf8 ( ) ;
189199 }
190- writer. write ( "\" " ) ?;
200+ }
201+
202+ if last < value. len ( ) {
203+ writer. write ( & value[ last..] ) ?;
191204 }
192205
193206 Ok ( ( ) )
@@ -5194,4 +5207,41 @@ mod tests {
51945207 "component attr should be on webui-route: {html}"
51955208 ) ;
51965209 }
5210+
5211+ #[ test]
5212+ fn test_route_state_attributes_escape_scalars_and_data_state ( ) {
5213+ let protocol = make_route_protocol ( ) ;
5214+ let state = test_json ! ( {
5215+ "title" : "Fish & Chips <\" special\" >" ,
5216+ "cartOpen" : true ,
5217+ "items" : [ { "name" : "A&B" } ]
5218+ } ) ;
5219+ let mut writer = TestWriter :: new ( ) ;
5220+
5221+ handle (
5222+ & protocol,
5223+ & state,
5224+ & RenderOptions :: new ( "index.html" , "/" ) ,
5225+ & mut writer,
5226+ )
5227+ . unwrap ( ) ;
5228+
5229+ let html = writer. get_content ( ) ;
5230+ assert ! (
5231+ html. contains( r#"title="Fish & Chips <"special">""# ) ,
5232+ "escaped title should be emitted: {html}"
5233+ ) ;
5234+ assert ! (
5235+ html. contains( r#"cart-open="true""# ) ,
5236+ "bool attrs should render: {html}"
5237+ ) ;
5238+ assert ! (
5239+ html. contains( r#"data-state=""# ) ,
5240+ "complex state should be emitted as data-state: {html}"
5241+ ) ;
5242+ assert ! (
5243+ html. contains( r#""name":"A&B""# ) ,
5244+ "complex state values should be escaped inside data-state: {html}"
5245+ ) ;
5246+ }
51975247}
0 commit comments