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
26 enum StorageChangeOp {
27 Clear,
28 Set(JsonValue),
29 SetWithoutQuota(JsonValue),
30 }
31
get_from_db(conn: &Connection, ext_id: &str) -> Result<Option<JsonMap>>32 fn get_from_db(conn: &Connection, ext_id: &str) -> Result<Option<JsonMap>> {
33 Ok(
34 match conn.try_query_one::<String>(
35 "SELECT data FROM storage_sync_data
36 WHERE ext_id = :ext_id",
37 &[(":ext_id", &ext_id)],
38 true,
39 )? {
40 Some(s) => match serde_json::from_str(&s)? {
41 JsonValue::Object(m) => Some(m),
42 // we could panic here as it's theoretically impossible, but we
43 // might as well treat it as not existing...
44 _ => None,
45 },
46 None => None,
47 },
48 )
49 }
50
save_to_db(tx: &Transaction<'_>, ext_id: &str, val: &StorageChangeOp) -> Result<()>51 fn save_to_db(tx: &Transaction<'_>, ext_id: &str, val: &StorageChangeOp) -> Result<()> {
52 // This function also handles removals. Either an empty map or explicit null
53 // is a removal. If there's a mirror record for this extension ID, then we
54 // must leave a tombstone behind for syncing.
55 let is_delete = match val {
56 StorageChangeOp::Clear => true,
57 StorageChangeOp::Set(JsonValue::Object(v)) => v.is_empty(),
58 StorageChangeOp::SetWithoutQuota(JsonValue::Object(v)) => v.is_empty(),
59 _ => false,
60 };
61 if is_delete {
62 let in_mirror = tx
63 .try_query_one(
64 "SELECT EXISTS(SELECT 1 FROM storage_sync_mirror WHERE ext_id = :ext_id);",
65 rusqlite::named_params! {
66 ":ext_id": ext_id,
67 },
68 true,
69 )?
70 .unwrap_or_default();
71 if in_mirror {
72 log::trace!("saving data for '{}': leaving a tombstone", ext_id);
73 tx.execute_named_cached(
74 "
75 INSERT INTO storage_sync_data(ext_id, data, sync_change_counter)
76 VALUES (:ext_id, NULL, 1)
77 ON CONFLICT (ext_id) DO UPDATE
78 SET data = NULL, sync_change_counter = sync_change_counter + 1",
79 rusqlite::named_params! {
80 ":ext_id": ext_id,
81 },
82 )?;
83 } else {
84 log::trace!("saving data for '{}': removing the row", ext_id);
85 tx.execute_named_cached(
86 "
87 DELETE FROM storage_sync_data WHERE ext_id = :ext_id",
88 rusqlite::named_params! {
89 ":ext_id": ext_id,
90 },
91 )?;
92 }
93 } else {
94 // Convert to bytes so we can enforce the quota if necessary.
95 let sval = match val {
96 StorageChangeOp::Set(v) => {
97 let sv = v.to_string();
98 if sv.len() > SYNC_QUOTA_BYTES {
99 return Err(ErrorKind::QuotaError(QuotaReason::TotalBytes).into());
100 }
101 sv
102 }
103 StorageChangeOp::SetWithoutQuota(v) => v.to_string(),
104 StorageChangeOp::Clear => unreachable!(),
105 };
106
107 log::trace!("saving data for '{}': writing", ext_id);
108 tx.execute_named_cached(
109 "INSERT INTO storage_sync_data(ext_id, data, sync_change_counter)
110 VALUES (:ext_id, :data, 1)
111 ON CONFLICT (ext_id) DO UPDATE
112 set data=:data, sync_change_counter = sync_change_counter + 1",
113 rusqlite::named_params! {
114 ":ext_id": ext_id,
115 ":data": &sval,
116 },
117 )?;
118 }
119 Ok(())
120 }
121
remove_from_db(tx: &Transaction<'_>, ext_id: &str) -> Result<()>122 fn remove_from_db(tx: &Transaction<'_>, ext_id: &str) -> Result<()> {
123 save_to_db(tx, ext_id, &StorageChangeOp::Clear)
124 }
125
126 // This is a "helper struct" for the callback part of the chrome.storage spec,
127 // but shaped in a way to make it more convenient from the rust side of the
128 // world.
129 #[derive(Debug, Clone, PartialEq, Serialize)]
130 #[serde(rename_all = "camelCase")]
131 pub struct StorageValueChange {
132 #[serde(skip_serializing)]
133 pub key: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub old_value: Option<JsonValue>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub new_value: Option<JsonValue>,
138 }
139
140 // This is, largely, a helper so that this serializes correctly as per the
141 // chrome.storage.sync spec. If not for custom serialization it should just
142 // be a plain vec
143 #[derive(Debug, Default, Clone, PartialEq)]
144 pub struct StorageChanges {
145 changes: Vec<StorageValueChange>,
146 }
147
148 impl StorageChanges {
new() -> Self149 pub fn new() -> Self {
150 Self::default()
151 }
152
with_capacity(n: usize) -> Self153 pub fn with_capacity(n: usize) -> Self {
154 Self {
155 changes: Vec::with_capacity(n),
156 }
157 }
158
is_empty(&self) -> bool159 pub fn is_empty(&self) -> bool {
160 self.changes.is_empty()
161 }
162
push(&mut self, change: StorageValueChange)163 pub fn push(&mut self, change: StorageValueChange) {
164 self.changes.push(change)
165 }
166 }
167
168 // and it serializes as a map.
169 impl Serialize for StorageChanges {
serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer,170 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
171 where
172 S: Serializer,
173 {
174 let mut map = serializer.serialize_map(Some(self.changes.len()))?;
175 for change in &self.changes {
176 map.serialize_entry(&change.key, change)?;
177 }
178 map.end()
179 }
180 }
181
182 // A helper to determine the size of a key/value combination from the
183 // perspective of quota and getBytesInUse().
get_quota_size_of(key: &str, v: &JsonValue) -> usize184 pub fn get_quota_size_of(key: &str, v: &JsonValue) -> usize {
185 // Reading the chrome docs literally re the quota, the length of the key
186 // is just the string len, but the value is the json val, as bytes.
187 key.len() + v.to_string().len()
188 }
189
190 /// The implementation of `storage[.sync].set()`. On success this returns the
191 /// StorageChanges defined by the chrome API - it's assumed the caller will
192 /// arrange to deliver this to observers as defined in that API.
set(tx: &Transaction<'_>, ext_id: &str, val: JsonValue) -> Result<StorageChanges>193 pub fn set(tx: &Transaction<'_>, ext_id: &str, val: JsonValue) -> Result<StorageChanges> {
194 let val_map = match val {
195 JsonValue::Object(m) => m,
196 // Not clear what the error semantics should be yet. For now, pretend an empty map.
197 _ => Map::new(),
198 };
199
200 let mut current = get_from_db(tx, ext_id)?.unwrap_or_default();
201
202 let mut changes = StorageChanges::with_capacity(val_map.len());
203
204 // iterate over the value we are adding/updating.
205 for (k, v) in val_map.into_iter() {
206 let old_value = current.remove(&k);
207 if current.len() >= SYNC_MAX_ITEMS {
208 return Err(ErrorKind::QuotaError(QuotaReason::MaxItems).into());
209 }
210 // Reading the chrome docs literally re the quota, the length of the key
211 // is just the string len, but the value is the json val, as bytes
212 if get_quota_size_of(&k, &v) > SYNC_QUOTA_BYTES_PER_ITEM {
213 return Err(ErrorKind::QuotaError(QuotaReason::ItemBytes).into());
214 }
215 let change = StorageValueChange {
216 key: k.clone(),
217 old_value,
218 new_value: Some(v.clone()),
219 };
220 changes.push(change);
221 current.insert(k, v);
222 }
223
224 save_to_db(
225 tx,
226 ext_id,
227 &StorageChangeOp::Set(JsonValue::Object(current)),
228 )?;
229 Ok(changes)
230 }
231
232 // A helper which takes a param indicating what keys should be returned and
233 // converts that to a vec of real strings. Also returns "default" values to
234 // be used if no item exists for that key.
get_keys(keys: JsonValue) -> Vec<(String, Option<JsonValue>)>235 fn get_keys(keys: JsonValue) -> Vec<(String, Option<JsonValue>)> {
236 match keys {
237 JsonValue::String(s) => vec![(s, None)],
238 JsonValue::Array(keys) => {
239 // because nothing with json is ever simple, each key may not be
240 // a string. We ignore any which aren't.
241 keys.iter()
242 .filter_map(|v| v.as_str().map(|s| (s.to_string(), None)))
243 .collect()
244 }
245 JsonValue::Object(m) => m.into_iter().map(|(k, d)| (k, Some(d))).collect(),
246 _ => vec![],
247 }
248 }
249
250 /// The implementation of `storage[.sync].get()` - on success this always
251 /// returns a Json object.
get(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<JsonValue>252 pub fn get(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<JsonValue> {
253 // key is optional, or string or array of string or object keys
254 let maybe_existing = get_from_db(conn, ext_id)?;
255 let mut existing = match (maybe_existing, keys.is_object()) {
256 (None, true) => return Ok(keys),
257 (None, false) => return Ok(JsonValue::Object(Map::new())),
258 (Some(v), _) => v,
259 };
260 // take the quick path for null, where we just return the entire object.
261 if keys.is_null() {
262 return Ok(JsonValue::Object(existing));
263 }
264 // OK, so we need to build a list of keys to get.
265 let keys_and_defaults = get_keys(keys);
266 let mut result = Map::with_capacity(keys_and_defaults.len());
267 for (key, maybe_default) in keys_and_defaults {
268 if let Some(v) = existing.remove(&key) {
269 result.insert(key, v);
270 } else if let Some(def) = maybe_default {
271 result.insert(key, def);
272 }
273 // else |keys| is a string/array instead of an object with defaults.
274 // Don't include keys without default values.
275 }
276 Ok(JsonValue::Object(result))
277 }
278
279 /// The implementation of `storage[.sync].remove()`. On success this returns the
280 /// StorageChanges defined by the chrome API - it's assumed the caller will
281 /// arrange to deliver this to observers as defined in that API.
remove(tx: &Transaction<'_>, ext_id: &str, keys: JsonValue) -> Result<StorageChanges>282 pub fn remove(tx: &Transaction<'_>, ext_id: &str, keys: JsonValue) -> Result<StorageChanges> {
283 let mut existing = match get_from_db(tx, ext_id)? {
284 None => return Ok(StorageChanges::new()),
285 Some(v) => v,
286 };
287
288 // Note: get_keys parses strings, arrays and objects, but remove()
289 // is expected to only be passed a string or array of strings.
290 let keys_and_defs = get_keys(keys);
291
292 let mut result = StorageChanges::with_capacity(keys_and_defs.len());
293 for (key, _) in keys_and_defs {
294 if let Some(v) = existing.remove(&key) {
295 result.push(StorageValueChange {
296 key,
297 old_value: Some(v),
298 new_value: None,
299 });
300 }
301 }
302 if !result.is_empty() {
303 save_to_db(
304 tx,
305 ext_id,
306 &StorageChangeOp::SetWithoutQuota(JsonValue::Object(existing)),
307 )?;
308 }
309 Ok(result)
310 }
311
312 /// The implementation of `storage[.sync].clear()`. On success this returns the
313 /// StorageChanges defined by the chrome API - it's assumed the caller will
314 /// arrange to deliver this to observers as defined in that API.
clear(tx: &Transaction<'_>, ext_id: &str) -> Result<StorageChanges>315 pub fn clear(tx: &Transaction<'_>, ext_id: &str) -> Result<StorageChanges> {
316 let existing = match get_from_db(tx, ext_id)? {
317 None => return Ok(StorageChanges::new()),
318 Some(v) => v,
319 };
320 let mut result = StorageChanges::with_capacity(existing.len());
321 for (key, val) in existing.into_iter() {
322 result.push(StorageValueChange {
323 key: key.to_string(),
324 new_value: None,
325 old_value: Some(val),
326 });
327 }
328 remove_from_db(tx, ext_id)?;
329 Ok(result)
330 }
331
332 /// The implementation of `storage[.sync].getBytesInUse()`.
get_bytes_in_use(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<usize>333 pub fn get_bytes_in_use(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<usize> {
334 let maybe_existing = get_from_db(conn, ext_id)?;
335 let existing = match maybe_existing {
336 None => return Ok(0),
337 Some(v) => v,
338 };
339 // Make an array of all the keys we we are going to count.
340 let keys: Vec<&str> = match &keys {
341 JsonValue::Null => existing.keys().map(|v| v.as_str()).collect(),
342 JsonValue::String(name) => vec![name.as_str()],
343 JsonValue::Array(names) => names.iter().filter_map(|v| v.as_str()).collect(),
344 // in the spirit of json-based APIs, silently ignore strange things.
345 _ => return Ok(0),
346 };
347 // We must use the same way of counting as our quota enforcement.
348 let mut size = 0;
349 for key in keys.into_iter() {
350 if let Some(v) = existing.get(key) {
351 size += get_quota_size_of(key, &v);
352 }
353 }
354 Ok(size)
355 }
356
357 /// Information about the usage of a single extension.
358 #[derive(Debug, Clone, PartialEq)]
359 pub struct UsageInfo {
360 /// The extension id.
361 pub ext_id: String,
362 /// The number of keys the extension uses.
363 pub num_keys: usize,
364 /// The number of bytes used by the extension. This result is somewhat rough
365 /// -- it doesn't bother counting the size of the extension ID, or data in
366 /// the mirror, and favors returning the exact number of bytes used by the
367 /// column (that is, the size of the JSON object) rather than replicating
368 /// the `get_bytes_in_use` return value for all keys.
369 pub num_bytes: usize,
370 }
371
372 /// Exposes information about per-collection usage for the purpose of telemetry.
373 /// (Doesn't map to an actual `chrome.storage.sync` API).
usage(db: &Connection) -> Result<Vec<UsageInfo>>374 pub fn usage(db: &Connection) -> Result<Vec<UsageInfo>> {
375 type JsonObject = Map<String, JsonValue>;
376 let sql = "
377 SELECT ext_id, data
378 FROM storage_sync_data
379 WHERE data IS NOT NULL
380 -- for tests and determinism
381 ORDER BY ext_id
382 ";
383 db.query_rows_into(sql, &[], |row| {
384 let ext_id: String = row.get("ext_id")?;
385 let data: String = row.get("data")?;
386 let num_bytes = data.len();
387 let num_keys = serde_json::from_str::<JsonObject>(&data)?.len();
388 Ok(UsageInfo {
389 ext_id,
390 num_bytes,
391 num_keys,
392 })
393 })
394 }
395
396 #[cfg(test)]
397 mod tests {
398 use super::*;
399 use crate::db::test::new_mem_db;
400 use serde_json::json;
401
402 #[test]
test_serialize_storage_changes() -> Result<()>403 fn test_serialize_storage_changes() -> Result<()> {
404 let c = StorageChanges {
405 changes: vec![StorageValueChange {
406 key: "key".to_string(),
407 old_value: Some(json!("old")),
408 new_value: None,
409 }],
410 };
411 assert_eq!(serde_json::to_string(&c)?, r#"{"key":{"oldValue":"old"}}"#);
412 let c = StorageChanges {
413 changes: vec![StorageValueChange {
414 key: "key".to_string(),
415 old_value: None,
416 new_value: Some(json!({"foo": "bar"})),
417 }],
418 };
419 assert_eq!(
420 serde_json::to_string(&c)?,
421 r#"{"key":{"newValue":{"foo":"bar"}}}"#
422 );
423 Ok(())
424 }
425
make_changes(changes: &[(&str, Option<JsonValue>, Option<JsonValue>)]) -> StorageChanges426 fn make_changes(changes: &[(&str, Option<JsonValue>, Option<JsonValue>)]) -> StorageChanges {
427 let mut r = StorageChanges::with_capacity(changes.len());
428 for (name, old_value, new_value) in changes {
429 r.push(StorageValueChange {
430 key: (*name).to_string(),
431 old_value: old_value.clone(),
432 new_value: new_value.clone(),
433 });
434 }
435 r
436 }
437
438 #[test]
test_simple() -> Result<()>439 fn test_simple() -> Result<()> {
440 let ext_id = "x";
441 let mut db = new_mem_db();
442 let tx = db.transaction()?;
443
444 // an empty store.
445 for q in vec![JsonValue::Null, json!("foo"), json!(["foo"])].into_iter() {
446 assert_eq!(get(&tx, &ext_id, q)?, json!({}));
447 }
448
449 // Default values in an empty store.
450 for q in vec![json!({ "foo": null }), json!({"foo": "default"})].into_iter() {
451 assert_eq!(get(&tx, &ext_id, q.clone())?, q.clone());
452 }
453
454 // Single item in the store.
455 set(&tx, &ext_id, json!({"foo": "bar" }))?;
456 for q in vec![
457 JsonValue::Null,
458 json!("foo"),
459 json!(["foo"]),
460 json!({ "foo": null }),
461 json!({"foo": "default"}),
462 ]
463 .into_iter()
464 {
465 assert_eq!(get(&tx, &ext_id, q)?, json!({"foo": "bar" }));
466 }
467
468 // Default values in a non-empty store.
469 for q in vec![
470 json!({ "non_existing_key": null }),
471 json!({"non_existing_key": 0}),
472 json!({"non_existing_key": false}),
473 json!({"non_existing_key": "default"}),
474 json!({"non_existing_key": ["array"]}),
475 json!({"non_existing_key": {"objectkey": "value"}}),
476 ]
477 .into_iter()
478 {
479 assert_eq!(get(&tx, &ext_id, q.clone())?, q.clone());
480 }
481
482 // more complex stuff, including changes checking.
483 assert_eq!(
484 set(&tx, &ext_id, json!({"foo": "new", "other": "also new" }))?,
485 make_changes(&[
486 ("foo", Some(json!("bar")), Some(json!("new"))),
487 ("other", None, Some(json!("also new")))
488 ])
489 );
490 assert_eq!(
491 get(&tx, &ext_id, JsonValue::Null)?,
492 json!({"foo": "new", "other": "also new"})
493 );
494 assert_eq!(get(&tx, &ext_id, json!("foo"))?, json!({"foo": "new"}));
495 assert_eq!(
496 get(&tx, &ext_id, json!(["foo", "other"]))?,
497 json!({"foo": "new", "other": "also new"})
498 );
499 assert_eq!(
500 get(&tx, &ext_id, json!({"foo": null, "default": "yo"}))?,
501 json!({"foo": "new", "default": "yo"})
502 );
503
504 assert_eq!(
505 remove(&tx, &ext_id, json!("foo"))?,
506 make_changes(&[("foo", Some(json!("new")), None)]),
507 );
508
509 assert_eq!(
510 set(&tx, &ext_id, json!({"foo": {"sub-object": "sub-value"}}))?,
511 make_changes(&[("foo", None, Some(json!({"sub-object": "sub-value"}))),])
512 );
513
514 // XXX - other variants.
515
516 assert_eq!(
517 clear(&tx, &ext_id)?,
518 make_changes(&[
519 ("foo", Some(json!({"sub-object": "sub-value"})), None),
520 ("other", Some(json!("also new")), None),
521 ]),
522 );
523 assert_eq!(get(&tx, &ext_id, JsonValue::Null)?, json!({}));
524
525 Ok(())
526 }
527
528 #[test]
test_check_get_impl() -> Result<()>529 fn test_check_get_impl() -> Result<()> {
530 // This is a port of checkGetImpl in test_ext_storage.js in Desktop.
531 let ext_id = "x";
532 let mut db = new_mem_db();
533 let tx = db.transaction()?;
534
535 let prop = "test-prop";
536 let value = "test-value";
537
538 set(&tx, ext_id, json!({ prop: value }))?;
539
540 // this is the checkGetImpl part!
541 let mut data = get(&tx, &ext_id, json!(null))?;
542 assert_eq!(value, json!(data[prop]), "null getter worked for {}", prop);
543
544 data = get(&tx, &ext_id, json!(prop))?;
545 assert_eq!(
546 value,
547 json!(data[prop]),
548 "string getter worked for {}",
549 prop
550 );
551 assert_eq!(
552 data.as_object().unwrap().len(),
553 1,
554 "string getter should return an object with a single property"
555 );
556
557 data = get(&tx, &ext_id, json!([prop]))?;
558 assert_eq!(value, json!(data[prop]), "array getter worked for {}", prop);
559 assert_eq!(
560 data.as_object().unwrap().len(),
561 1,
562 "array getter with a single key should return an object with a single property"
563 );
564
565 // checkGetImpl() uses `{ [prop]: undefined }` - but json!() can't do that :(
566 // Hopefully it's just testing a simple object, so we use `{ prop: null }`
567 data = get(&tx, &ext_id, json!({ prop: null }))?;
568 assert_eq!(
569 value,
570 json!(data[prop]),
571 "object getter worked for {}",
572 prop
573 );
574 assert_eq!(
575 data.as_object().unwrap().len(),
576 1,
577 "object getter with a single key should return an object with a single property"
578 );
579
580 Ok(())
581 }
582
583 #[test]
test_bug_1621162() -> Result<()>584 fn test_bug_1621162() -> Result<()> {
585 // apparently Firefox, unlike Chrome, will not optimize the changes.
586 // See bug 1621162 for more!
587 let mut db = new_mem_db();
588 let tx = db.transaction()?;
589 let ext_id = "xyz";
590
591 set(&tx, &ext_id, json!({"foo": "bar" }))?;
592
593 assert_eq!(
594 set(&tx, &ext_id, json!({"foo": "bar" }))?,
595 make_changes(&[("foo", Some(json!("bar")), Some(json!("bar")))]),
596 );
597 Ok(())
598 }
599
600 #[test]
test_quota_maxitems() -> Result<()>601 fn test_quota_maxitems() -> Result<()> {
602 let mut db = new_mem_db();
603 let tx = db.transaction()?;
604 let ext_id = "xyz";
605 for i in 1..SYNC_MAX_ITEMS + 1 {
606 set(
607 &tx,
608 &ext_id,
609 json!({ format!("key-{}", i): format!("value-{}", i) }),
610 )?;
611 }
612 let e = set(&tx, &ext_id, json!({"another": "another"})).unwrap_err();
613 match e.kind() {
614 ErrorKind::QuotaError(QuotaReason::MaxItems) => {}
615 _ => panic!("unexpected error type"),
616 };
617 Ok(())
618 }
619
620 #[test]
test_quota_bytesperitem() -> Result<()>621 fn test_quota_bytesperitem() -> Result<()> {
622 let mut db = new_mem_db();
623 let tx = db.transaction()?;
624 let ext_id = "xyz";
625 // A string 5 bytes less than the max. This should be counted as being
626 // 3 bytes less than the max as the quotes are counted. Plus the length
627 // of the key (no quotes) means we should come in 2 bytes under.
628 let val = "x".repeat(SYNC_QUOTA_BYTES_PER_ITEM - 5);
629
630 // Key length doesn't push it over.
631 set(&tx, &ext_id, json!({ "x": val }))?;
632 assert_eq!(
633 get_bytes_in_use(&tx, &ext_id, json!("x"))?,
634 SYNC_QUOTA_BYTES_PER_ITEM - 2
635 );
636
637 // Key length does push it over.
638 let e = set(&tx, &ext_id, json!({ "xxxx": val })).unwrap_err();
639 match e.kind() {
640 ErrorKind::QuotaError(QuotaReason::ItemBytes) => {}
641 _ => panic!("unexpected error type"),
642 };
643 Ok(())
644 }
645
646 #[test]
test_quota_bytes() -> Result<()>647 fn test_quota_bytes() -> Result<()> {
648 let mut db = new_mem_db();
649 let tx = db.transaction()?;
650 let ext_id = "xyz";
651 let val = "x".repeat(SYNC_QUOTA_BYTES + 1);
652
653 // Init an over quota db with a single key.
654 save_to_db(
655 &tx,
656 ext_id,
657 &StorageChangeOp::SetWithoutQuota(json!({ "x": val })),
658 )?;
659
660 // Adding more data fails.
661 let e = set(&tx, &ext_id, json!({ "y": "newvalue" })).unwrap_err();
662 match e.kind() {
663 ErrorKind::QuotaError(QuotaReason::TotalBytes) => {}
664 _ => panic!("unexpected error type"),
665 };
666
667 // Remove data does not fails.
668 remove(&tx, &ext_id, json!["x"])?;
669
670 // Restore the over quota data.
671 save_to_db(
672 &tx,
673 ext_id,
674 &StorageChangeOp::SetWithoutQuota(json!({ "y": val })),
675 )?;
676
677 // Overwrite with less data does not fail.
678 set(&tx, &ext_id, json!({ "y": "lessdata" }))?;
679
680 Ok(())
681 }
682
683 #[test]
test_get_bytes_in_use() -> Result<()>684 fn test_get_bytes_in_use() -> Result<()> {
685 let mut db = new_mem_db();
686 let tx = db.transaction()?;
687 let ext_id = "xyz";
688
689 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(null))?, 0);
690
691 set(&tx, &ext_id, json!({ "a": "a" }))?; // should be 4
692 set(&tx, &ext_id, json!({ "b": "bb" }))?; // should be 5
693 set(&tx, &ext_id, json!({ "c": "ccc" }))?; // should be 6
694 set(&tx, &ext_id, json!({ "n": 999_999 }))?; // should be 7
695
696 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("x"))?, 0);
697 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("a"))?, 4);
698 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("b"))?, 5);
699 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("c"))?, 6);
700 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!("n"))?, 7);
701
702 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a"]))?, 4);
703 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "x"]))?, 4);
704 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "b"]))?, 9);
705 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(["a", "c"]))?, 10);
706
707 assert_eq!(
708 get_bytes_in_use(&tx, &ext_id, json!(["a", "b", "c", "n"]))?,
709 22
710 );
711 assert_eq!(get_bytes_in_use(&tx, &ext_id, json!(null))?, 22);
712 Ok(())
713 }
714
715 #[test]
test_usage()716 fn test_usage() {
717 let mut db = new_mem_db();
718 let tx = db.transaction().unwrap();
719 // '{"a":"a","b":"bb","c":"ccc","n":999999}': 39 bytes
720 set(&tx, "xyz", json!({ "a": "a" })).unwrap();
721 set(&tx, "xyz", json!({ "b": "bb" })).unwrap();
722 set(&tx, "xyz", json!({ "c": "ccc" })).unwrap();
723 set(&tx, "xyz", json!({ "n": 999_999 })).unwrap();
724
725 // '{"a":"a"}': 9 bytes
726 set(&tx, "abc", json!({ "a": "a" })).unwrap();
727
728 tx.commit().unwrap();
729
730 let usage = usage(&db).unwrap();
731 let expect = [
732 UsageInfo {
733 ext_id: "abc".to_string(),
734 num_keys: 1,
735 num_bytes: 9,
736 },
737 UsageInfo {
738 ext_id: "xyz".to_string(),
739 num_keys: 4,
740 num_bytes: 39,
741 },
742 ];
743 assert_eq!(&usage, &expect);
744 }
745 }
746