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