1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 use crate::error::*;
6 use rusqlite::{Connection, Transaction};
7 use serde::{ser::SerializeMap, Serialize, Serializer};
8
9 use serde_json::{Map, Value as JsonValue};
10 use sql_support::{self, ConnExt};
11
12 // These constants are defined by the chrome.storage.sync spec. We export them
13 // publicly from this module, then from the crate, so they wind up in the
14 // clients.
15 // Note the limits for `chrome.storage.sync` and `chrome.storage.local` are
16 // different, and these are from `.sync` - we'll have work to do if we end up
17 // wanting this to be used for `.local` too!
18 pub const SYNC_QUOTA_BYTES: usize = 102_400;
19 pub const SYNC_QUOTA_BYTES_PER_ITEM: usize = 8_192;
20 pub const SYNC_MAX_ITEMS: usize = 512;
21 // Note there are also constants for "operations per minute" etc, which aren't
22 // enforced here.
23
24 type JsonMap = Map<String, JsonValue>;
25
get_from_db(conn: &Connection, ext_id: &str) -> Result<Option<JsonMap>>26 fn get_from_db(conn: &Connection, ext_id: &str) -> Result<Option<JsonMap>> {
27 Ok(
28 match conn.try_query_one::<String>(
29 "SELECT data FROM storage_sync_data
30 WHERE ext_id = :ext_id",
31 &[(":ext_id", &ext_id)],
32 true,
33 )? {
34 Some(s) => match serde_json::from_str(&s)? {
35 JsonValue::Object(m) => Some(m),
36 // we could panic here as it's theoretically impossible, but we
37 // might as well treat it as not existing...
38 _ => None,
39 },
40 None => None,
41 },
42 )
43 }
44
save_to_db(tx: &Transaction<'_>, ext_id: &str, val: &JsonValue) -> Result<()>45 fn save_to_db(tx: &Transaction<'_>, ext_id: &str, val: &JsonValue) -> Result<()> {
46 // This function also handles removals. Either an empty map or explicit null
47 // is a removal. If there's a mirror record for this extension ID, then we
48 // must leave a tombstone behind for syncing.
49 let is_delete = match val {
50 JsonValue::Null => true,
51 JsonValue::Object(m) => m.is_empty(),
52 _ => false,
53 };
54 if is_delete {
55 let in_mirror = tx
56 .try_query_one(
57 "SELECT EXISTS(SELECT 1 FROM storage_sync_mirror WHERE ext_id = :ext_id);",
58 rusqlite::named_params! {
59 ":ext_id": ext_id,
60 },
61 true,
62 )?
63 .unwrap_or_default();
64 if in_mirror {
65 log::trace!("saving data for '{}': leaving a tombstone", ext_id);
66 tx.execute_named_cached(
67 "
68 INSERT INTO storage_sync_data(ext_id, data, sync_change_counter)
69 VALUES (:ext_id, NULL, 1)
70 ON CONFLICT (ext_id) DO UPDATE
71 SET data = NULL, sync_change_counter = sync_change_counter + 1",
72 rusqlite::named_params! {
73 ":ext_id": ext_id,
74 },
75 )?;
76 } else {
77 log::trace!("saving data for '{}': removing the row", ext_id);
78 tx.execute_named_cached(
79 "
80 DELETE FROM storage_sync_data WHERE ext_id = :ext_id",
81 rusqlite::named_params! {
82 ":ext_id": ext_id,
83 },
84 )?;
85 }
86 } else {
87 // Convert to bytes so we can enforce the quota.
88 let sval = val.to_string();
89 if sval.len() > SYNC_QUOTA_BYTES {
90 return Err(ErrorKind::QuotaError(QuotaReason::TotalBytes).into());
91 }
92 log::trace!("saving data for '{}': writing", ext_id);
93 tx.execute_named_cached(
94 "INSERT INTO storage_sync_data(ext_id, data, sync_change_counter)
95 VALUES (:ext_id, :data, 1)
96 ON CONFLICT (ext_id) DO UPDATE
97 set data=:data, sync_change_counter = sync_change_counter + 1",
98 rusqlite::named_params! {
99 ":ext_id": ext_id,
100 ":data": &sval,
101 },
102 )?;
103 }
104 Ok(())
105 }
106
remove_from_db(tx: &Transaction<'_>, ext_id: &str) -> Result<()>107 fn remove_from_db(tx: &Transaction<'_>, ext_id: &str) -> Result<()> {
108 save_to_db(tx, ext_id, &JsonValue::Null)
109 }
110
111 // This is a "helper struct" for the callback part of the chrome.storage spec,
112 // but shaped in a way to make it more convenient from the rust side of the
113 // world.
114 #[derive(Debug, Clone, PartialEq, Serialize)]
115 #[serde(rename_all = "camelCase")]
116 pub struct StorageValueChange {
117 #[serde(skip_serializing)]
118 pub key: String,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub old_value: Option<JsonValue>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub new_value: Option<JsonValue>,
123 }
124
125 // This is, largely, a helper so that this serializes correctly as per the
126 // chrome.storage.sync spec. If not for custom serialization it should just
127 // be a plain vec
128 #[derive(Debug, Default, Clone, PartialEq)]
129 pub struct StorageChanges {
130 changes: Vec<StorageValueChange>,
131 }
132
133 impl StorageChanges {
new() -> Self134 pub fn new() -> Self {
135 Self::default()
136 }
137
with_capacity(n: usize) -> Self138 pub fn with_capacity(n: usize) -> Self {
139 Self {
140 changes: Vec::with_capacity(n),
141 }
142 }
143
is_empty(&self) -> bool144 pub fn is_empty(&self) -> bool {
145 self.changes.is_empty()
146 }
147
push(&mut self, change: StorageValueChange)148 pub fn push(&mut self, change: StorageValueChange) {
149 self.changes.push(change)
150 }
151 }
152
153 // and it serializes as a map.
154 impl Serialize for StorageChanges {
serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer,155 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156 where
157 S: Serializer,
158 {
159 let mut map = serializer.serialize_map(Some(self.changes.len()))?;
160 for change in &self.changes {
161 map.serialize_entry(&change.key, change)?;
162 }
163 map.end()
164 }
165 }
166
167 // A helper to determine the size of a key/value combination from the
168 // perspective of quota and getBytesInUse().
get_quota_size_of(key: &str, v: &JsonValue) -> usize169 pub fn get_quota_size_of(key: &str, v: &JsonValue) -> usize {
170 // Reading the chrome docs literally re the quota, the length of the key
171 // is just the string len, but the value is the json val, as bytes.
172 key.len() + v.to_string().len()
173 }
174
175 /// The implementation of `storage[.sync].set()`. On success this returns the
176 /// StorageChanges defined by the chrome API - it's assumed the caller will
177 /// arrange to deliver this to observers as defined in that API.
set(tx: &Transaction<'_>, ext_id: &str, val: JsonValue) -> Result<StorageChanges>178 pub fn set(tx: &Transaction<'_>, ext_id: &str, val: JsonValue) -> Result<StorageChanges> {
179 let val_map = match val {
180 JsonValue::Object(m) => m,
181 // Not clear what the error semantics should be yet. For now, pretend an empty map.
182 _ => Map::new(),
183 };
184
185 let mut current = get_from_db(tx, ext_id)?.unwrap_or_default();
186
187 let mut changes = StorageChanges::with_capacity(val_map.len());
188
189 // iterate over the value we are adding/updating.
190 for (k, v) in val_map.into_iter() {
191 let old_value = current.remove(&k);
192 if current.len() >= SYNC_MAX_ITEMS {
193 return Err(ErrorKind::QuotaError(QuotaReason::MaxItems).into());
194 }
195 // Reading the chrome docs literally re the quota, the length of the key
196 // is just the string len, but the value is the json val, as bytes
197 if get_quota_size_of(&k, &v) > SYNC_QUOTA_BYTES_PER_ITEM {
198 return Err(ErrorKind::QuotaError(QuotaReason::ItemBytes).into());
199 }
200 let change = StorageValueChange {
201 key: k.clone(),
202 old_value,
203 new_value: Some(v.clone()),
204 };
205 changes.push(change);
206 current.insert(k, v);
207 }
208
209 save_to_db(tx, ext_id, &JsonValue::Object(current))?;
210 Ok(changes)
211 }
212
213 // A helper which takes a param indicating what keys should be returned and
214 // converts that to a vec of real strings. Also returns "default" values to
215 // be used if no item exists for that key.
get_keys(keys: JsonValue) -> Vec<(String, Option<JsonValue>)>216 fn get_keys(keys: JsonValue) -> Vec<(String, Option<JsonValue>)> {
217 match keys {
218 JsonValue::String(s) => vec![(s, None)],
219 JsonValue::Array(keys) => {
220 // because nothing with json is ever simple, each key may not be
221 // a string. We ignore any which aren't.
222 keys.iter()
223 .filter_map(|v| v.as_str().map(|s| (s.to_string(), None)))
224 .collect()
225 }
226 JsonValue::Object(m) => m.into_iter().map(|(k, d)| (k, Some(d))).collect(),
227 _ => vec![],
228 }
229 }
230
231 /// The implementation of `storage[.sync].get()` - on success this always
232 /// returns a Json object.
get(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<JsonValue>233 pub fn get(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<JsonValue> {
234 // key is optional, or string or array of string or object keys
235 let maybe_existing = get_from_db(conn, ext_id)?;
236 let mut existing = match (maybe_existing, keys.is_object()) {
237 (None, true) => return Ok(keys),
238 (None, false) => return Ok(JsonValue::Object(Map::new())),
239 (Some(v), _) => v,
240 };
241 // take the quick path for null, where we just return the entire object.
242 if keys.is_null() {
243 return Ok(JsonValue::Object(existing));
244 }
245 // OK, so we need to build a list of keys to get.
246 let keys_and_defaults = get_keys(keys);
247 let mut result = Map::with_capacity(keys_and_defaults.len());
248 for (key, maybe_default) in keys_and_defaults {
249 if let Some(v) = existing.remove(&key) {
250 result.insert(key, v);
251 } else if let Some(def) = maybe_default {
252 result.insert(key, def);
253 }
254 // else |keys| is a string/array instead of an object with defaults.
255 // Don't include keys without default values.
256 }
257 Ok(JsonValue::Object(result))
258 }
259
260 /// The implementation of `storage[.sync].remove()`. On success this returns the
261 /// StorageChanges defined by the chrome API - it's assumed the caller will
262 /// arrange to deliver this to observers as defined in that API.
remove(tx: &Transaction<'_>, ext_id: &str, keys: JsonValue) -> Result<StorageChanges>263 pub fn remove(tx: &Transaction<'_>, ext_id: &str, keys: JsonValue) -> Result<StorageChanges> {
264 let mut existing = match get_from_db(tx, ext_id)? {
265 None => return Ok(StorageChanges::new()),
266 Some(v) => v,
267 };
268
269 // Note: get_keys parses strings, arrays and objects, but remove()
270 // is expected to only be passed a string or array of strings.
271 let keys_and_defs = get_keys(keys);
272
273 let mut result = StorageChanges::with_capacity(keys_and_defs.len());
274 for (key, _) in keys_and_defs {
275 if let Some(v) = existing.remove(&key) {
276 result.push(StorageValueChange {
277 key,
278 old_value: Some(v),
279 new_value: None,
280 });
281 }
282 }
283 if !result.is_empty() {
284 save_to_db(tx, ext_id, &JsonValue::Object(existing))?;
285 }
286 Ok(result)
287 }
288
289 /// The implementation of `storage[.sync].clear()`. On success this returns the
290 /// StorageChanges defined by the chrome API - it's assumed the caller will
291 /// arrange to deliver this to observers as defined in that API.
clear(tx: &Transaction<'_>, ext_id: &str) -> Result<StorageChanges>292 pub fn clear(tx: &Transaction<'_>, ext_id: &str) -> Result<StorageChanges> {
293 let existing = match get_from_db(tx, ext_id)? {
294 None => return Ok(StorageChanges::new()),
295 Some(v) => v,
296 };
297 let mut result = StorageChanges::with_capacity(existing.len());
298 for (key, val) in existing.into_iter() {
299 result.push(StorageValueChange {
300 key: key.to_string(),
301 new_value: None,
302 old_value: Some(val),
303 });
304 }
305 remove_from_db(tx, ext_id)?;
306 Ok(result)
307 }
308
309 /// The implementation of `storage[.sync].getBytesInUse()`.
get_bytes_in_use(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<usize>310 pub fn get_bytes_in_use(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<usize> {
311 let maybe_existing = get_from_db(conn, ext_id)?;
312 let existing = match maybe_existing {
313 None => return Ok(0),
314 Some(v) => v,
315 };
316 // Make an array of all the keys we we are going to count.
317 let keys: Vec<&str> = match &keys {
318 JsonValue::Null => existing.keys().map(|v| v.as_str()).collect(),
319 JsonValue::String(name) => vec![name.as_str()],
320 JsonValue::Array(names) => names.iter().filter_map(|v| v.as_str()).collect(),
321 // in the spirit of json-based APIs, silently ignore strange things.
322 _ => return Ok(0),
323 };
324 // We must use the same way of counting as our quota enforcement.
325 let mut size = 0;
326 for key in keys.into_iter() {
327 if let Some(v) = existing.get(key) {
328 size += get_quota_size_of(key, &v);
329 }
330 }
331 Ok(size)
332 }
333
334 #[cfg(test)]
335 mod tests {
336 use super::*;
337 use crate::db::test::new_mem_db;
338 use serde_json::json;
339
340 #[test]
test_serialize_storage_changes() -> Result<()>341 fn test_serialize_storage_changes() -> Result<()> {
342 let c = StorageChanges {
343 changes: vec![StorageValueChange {
344 key: "key".to_string(),
345 old_value: Some(json!("old")),
346 new_value: None,
347 }],
348 };
349 assert_eq!(serde_json::to_string(&c)?, r#"{"key":{"oldValue":"old"}}"#);
350 let c = StorageChanges {
351 changes: vec![StorageValueChange {
352 key: "key".to_string(),
353 old_value: None,
354 new_value: Some(json!({"foo": "bar"})),
355 }],
356 };
357 assert_eq!(
358 serde_json::to_string(&c)?,
359 r#"{"key":{"newValue":{"foo":"bar"}}}"#
360 );
361 Ok(())
362 }
363
make_changes(changes: &[(&str, Option<JsonValue>, Option<JsonValue>)]) -> StorageChanges364 fn make_changes(changes: &[(&str, Option<JsonValue>, Option<JsonValue>)]) -> StorageChanges {
365 let mut r = StorageChanges::with_capacity(changes.len());
366 for (name, old_value, new_value) in changes {
367 r.push(StorageValueChange {
368 key: (*name).to_string(),
369 old_value: old_value.clone(),
370 new_value: new_value.clone(),
371 });
372 }
373 r
374 }
375
376 #[test]
test_simple() -> Result<()>377 fn test_simple() -> Result<()> {
378 let ext_id = "x";
379 let mut db = new_mem_db();
380 let tx = db.transaction()?;
381
382 // an empty store.
383 for q in vec![JsonValue::Null, json!("foo"), json!(["foo"])].into_iter() {
384 assert_eq!(get(&tx, &ext_id, q)?, json!({}));
385 }
386
387 // Default values in an empty store.
388 for q in vec![json!({ "foo": null }), json!({"foo": "default"})].into_iter() {
389 assert_eq!(get(&tx, &ext_id, q.clone())?, q.clone());
390 }
391
392 // Single item in the store.
393 set(&tx, &ext_id, json!({"foo": "bar" }))?;
394 for q in vec![
395 JsonValue::Null,
396 json!("foo"),
397 json!(["foo"]),
398 json!({ "foo": null }),
399 json!({"foo": "default"}),
400 ]
401 .into_iter()
402 {
403 assert_eq!(get(&tx, &ext_id, q)?, json!({"foo": "bar" }));
404 }
405
406 // Default values in a non-empty store.
407 for q in vec![
408 json!({ "non_existing_key": null }),
409 json!({"non_existing_key": 0}),
410 json!({"non_existing_key": false}),
411 json!({"non_existing_key": "default"}),
412 json!({"non_existing_key": ["array"]}),
413 json!({"non_existing_key": {"objectkey": "value"}}),
414 ]
415 .into_iter()
416 {
417 assert_eq!(get(&tx, &ext_id, q.clone())?, q.clone());
418 }
419
420 // more complex stuff, including changes checking.
421 assert_eq!(
422 set(&tx, &ext_id, json!({"foo": "new", "other": "also new" }))?,
423 make_changes(&[
424 ("foo", Some(json!("bar")), Some(json!("new"))),
425 ("other", None, Some(json!("also new")))
426 ])
427 );
428 assert_eq!(
429 get(&tx, &ext_id, JsonValue::Null)?,
430 json!({"foo": "new", "other": "also new"})
431 );
432 assert_eq!(get(&tx, &ext_id, json!("foo"))?, json!({"foo": "new"}));
433 assert_eq!(
434 get(&tx, &ext_id, json!(["foo", "other"]))?,
435 json!({"foo": "new", "other": "also new"})
436 );
437 assert_eq!(
438 get(&tx, &ext_id, json!({"foo": null, "default": "yo"}))?,
439 json!({"foo": "new", "default": "yo"})
440 );
441
442 assert_eq!(
443 remove(&tx, &ext_id, json!("foo"))?,
444 make_changes(&[("foo", Some(json!("new")), None)]),
445 );
446
447 assert_eq!(
448 set(&tx, &ext_id, json!({"foo": {"sub-object": "sub-value"}}))?,
449 make_changes(&[("foo", None, Some(json!({"sub-object": "sub-value"}))),])
450 );
451
452 // XXX - other variants.
453
454 assert_eq!(
455 clear(&tx, &ext_id)?,
456 make_changes(&[
457 ("foo", Some(json!({"sub-object": "sub-value"})), None),
458 ("other", Some(json!("also new")), None),
459 ]),
460 );
461 assert_eq!(get(&tx, &ext_id, JsonValue::Null)?, json!({}));
462
463 Ok(())
464 }
465
466 #[test]
test_check_get_impl() -> Result<()>467 fn test_check_get_impl() -> Result<()> {
468 // This is a port of checkGetImpl in test_ext_storage.js in Desktop.
469 let ext_id = "x";
470 let mut db = new_mem_db();
471 let tx = db.transaction()?;
472
473 let prop = "test-prop";
474 let value = "test-value";
475
476 set(&tx, ext_id, json!({ prop: value }))?;
477
478 // this is the checkGetImpl part!
479 let mut data = get(&tx, &ext_id, json!(null))?;
480 assert_eq!(value, json!(data[prop]), "null getter worked for {}", prop);
481
482 data = get(&tx, &ext_id, json!(prop))?;
483 assert_eq!(
484 value,
485 json!(data[prop]),
486 "string getter worked for {}",
487 prop
488 );
489 assert_eq!(
490 data.as_object().unwrap().len(),
491 1,
492 "string getter should return an object with a single property"
493 );
494
495 data = get(&tx, &ext_id, json!([prop]))?;
496 assert_eq!(value, json!(data[prop]), "array getter worked for {}", prop);
497 assert_eq!(
498 data.as_object().unwrap().len(),
499 1,
500 "array getter with a single key should return an object with a single property"
501 );
502
503 // checkGetImpl() uses `{ [prop]: undefined }` - but json!() can't do that :(
504 // Hopefully it's just testing a simple object, so we use `{ prop: null }`
505 data = get(&tx, &ext_id, json!({ prop: null }))?;
506 assert_eq!(
507 value,
508 json!(data[prop]),
509 "object getter worked for {}",
510 prop
511 );
512 assert_eq!(
513 data.as_object().unwrap().len(),
514 1,
515 "object getter with a single key should return an object with a single property"
516 );
517
518 Ok(())
519 }
520
521 #[test]
test_bug_1621162() -> Result<()>522 fn test_bug_1621162() -> Result<()> {
523 // apparently Firefox, unlike Chrome, will not optimize the changes.
524 // See bug 1621162 for more!
525 let mut db = new_mem_db();
526 let tx = db.transaction()?;
527 let ext_id = "xyz";
528
529 set(&tx, &ext_id, json!({"foo": "bar" }))?;
530
531 assert_eq!(
532 set(&tx, &ext_id, json!({"foo": "bar" }))?,
533 make_changes(&[("foo", Some(json!("bar")), Some(json!("bar")))]),
534 );
535 Ok(())
536 }
537
538 #[test]
test_quota_maxitems() -> Result<()>539 fn test_quota_maxitems() -> Result<()> {
540 let mut db = new_mem_db();
541 let tx = db.transaction()?;
542 let ext_id = "xyz";
543 for i in 1..SYNC_MAX_ITEMS + 1 {
544 set(
545 &tx,
546 &ext_id,
547 json!({ format!("key-{}", i): format!("value-{}", i) }),
548 )?;
549 }
550 let e = set(&tx, &ext_id, json!({"another": "another"})).unwrap_err();
551 match e.kind() {
552 ErrorKind::QuotaError(QuotaReason::MaxItems) => {}
553 _ => panic!("unexpected error type"),
554 };
555 Ok(())
556 }
557
558 #[test]
test_quota_bytesperitem() -> Result<()>559 fn test_quota_bytesperitem() -> Result<()> {
560 let mut db = new_mem_db();
561 let tx = db.transaction()?;
562 let ext_id = "xyz";
563 // A string 5 bytes less than the max. This should be counted as being
564 // 3 bytes less than the max as the quotes are counted. Plus the length
565 // of the key (no quotes) means we should come in 2 bytes under.
566 let val = "x".repeat(SYNC_QUOTA_BYTES_PER_ITEM - 5);
567
568 // Key length doesn't push it over.
569 set(&tx, &ext_id, json!({ "x": val }))?;
570 assert_eq!(
571 get_bytes_in_use(&tx, &ext_id, json!("x"))?,
572 SYNC_QUOTA_BYTES_PER_ITEM - 2
573 );
574
575 // Key length does push it over.
576 let e = set(&tx, &ext_id, json!({ "xxxx": val })).unwrap_err();
577 match e.kind() {
578 ErrorKind::QuotaError(QuotaReason::ItemBytes) => {}
579 _ => panic!("unexpected error type"),
580 };
581 Ok(())
582 }
583
584 #[test]
test_get_bytes_in_use() -> Result<()>585 fn test_get_bytes_in_use() -> Result<()> {
586 let mut db = new_mem_db();
587 let tx = db.transaction()?;
588 let ext_id = "xyz";
589
590 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(null))?, 0);
591
592 set(&tx, &ext_id, json!({ "a": "a" }))?; // should be 4
593 set(&tx, &ext_id, json!({ "b": "bb" }))?; // should be 5
594 set(&tx, &ext_id, json!({ "c": "ccc" }))?; // should be 6
595 set(&tx, &ext_id, json!({ "n": 999_999 }))?; // should be 7
596
597 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("x"))?, 0);
598 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("a"))?, 4);
599 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("b"))?, 5);
600 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("c"))?, 6);
601 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("n"))?, 7);
602
603 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a"]))?, 4);
604 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "x"]))?, 4);
605 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "b"]))?, 9);
606 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "c"]))?, 10);
607
608 assert_eq!(
609 get_bytes_in_use(&tx, &ext_id, json!(["a", "b", "c", "n"]))?,
610 22
611 );
612 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(null))?, 22);
613 Ok(())
614 }
615 }
616