1use crate::{
6 client::CollectionMetadata, client::CollectionSignature,
7 schema::RemoteSettingsConnectionInitializer, Attachment, Error, RemoteSettingsRecord, Result,
8};
9use camino::Utf8PathBuf;
10use rusqlite::{params, Connection, OpenFlags, OptionalExtension, Transaction};
11use serde_json;
12use sha2::{Digest, Sha256};
13use std::io;
14
15use sql_support::{open_database::open_database_with_flags, ConnExt};
16
17pub struct Storage {
31 path: Utf8PathBuf,
32 conn: ConnectionCell,
33}
34
35impl Storage {
36 pub fn new(path: Utf8PathBuf) -> Self {
37 Self {
38 path,
39 conn: ConnectionCell::Uninitialized,
40 }
41 }
42
43 fn transaction(&mut self) -> Result<Transaction<'_>> {
44 match &self.conn {
45 ConnectionCell::Uninitialized => {
46 self.ensure_dir()?;
47 self.conn = ConnectionCell::Initialized(open_database_with_flags(
48 &self.path,
49 OpenFlags::default(),
50 &RemoteSettingsConnectionInitializer,
51 )?);
52 }
53 ConnectionCell::Initialized(_) => (),
54 ConnectionCell::Closed => return Err(Error::DatabaseClosed),
55 }
56 match &mut self.conn {
57 ConnectionCell::Initialized(conn) => Ok(conn.transaction()?),
58 _ => unreachable!(),
59 }
60 }
61
62 pub fn ensure_dir(&self) -> Result<()> {
63 if self.path == ":memory:" {
64 return Ok(());
65 }
66 let Some(dir) = self.path.parent() else {
67 return Ok(());
68 };
69 if !std::fs::exists(dir).map_err(Error::CreateDirError)? {
70 match std::fs::create_dir(dir) {
71 Ok(()) => (),
72 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => (),
74 Err(e) => return Err(Error::CreateDirError(e)),
75 }
76 }
77 Ok(())
78 }
79
80 pub fn close(&mut self) {
81 self.conn = ConnectionCell::Closed;
82 }
83
84 pub fn get_last_modified_timestamp(&mut self, collection_url: &str) -> Result<Option<u64>> {
89 let tx = self.transaction()?;
90 let mut stmt =
91 tx.prepare("SELECT last_modified FROM collection_metadata WHERE collection_url = ?")?;
92 let result: Option<u64> = stmt
93 .query_row((collection_url,), |row| row.get(0))
94 .optional()?;
95 Ok(result)
96 }
97
98 pub fn get_records(
103 &mut self,
104 collection_url: &str,
105 ) -> Result<Option<Vec<RemoteSettingsRecord>>> {
106 let tx = self.transaction()?;
107
108 let fetched = tx.exists(
109 "SELECT 1 FROM collection_metadata WHERE collection_url = ?",
110 (collection_url,),
111 )?;
112 let result = if fetched {
113 let records: Vec<RemoteSettingsRecord> = tx
115 .prepare("SELECT data FROM records WHERE collection_url = ?")?
116 .query_map(params![collection_url], |row| row.get::<_, Vec<u8>>(0))?
117 .map(|data| serde_json::from_slice(&data.unwrap()).unwrap())
118 .collect();
119
120 Ok(Some(records))
121 } else {
122 Ok(None)
123 };
124
125 tx.commit()?;
126 result
127 }
128
129 pub fn get_collection_metadata(
134 &mut self,
135 collection_url: &str,
136 ) -> Result<Option<CollectionMetadata>> {
137 let tx = self.transaction()?;
138 let mut stmt_metadata = tx.prepare(
139 "SELECT bucket, signature, x5u FROM collection_metadata WHERE collection_url = ?",
140 )?;
141
142 if let Some(metadata) = stmt_metadata
143 .query_row(params![collection_url], |row| {
144 Ok(CollectionMetadata {
145 bucket: row.get(0).unwrap_or_default(),
146 signature: CollectionSignature {
147 signature: row.get(1).unwrap_or_default(),
148 x5u: row.get(2).unwrap_or_default(),
149 },
150 })
151 })
152 .optional()?
153 {
154 Ok(Some(metadata))
155 } else {
156 Ok(None)
157 }
158 }
159
160 pub fn get_attachment(
167 &mut self,
168 collection_url: &str,
169 metadata: Attachment,
170 ) -> Result<Option<Vec<u8>>> {
171 let tx = self.transaction()?;
172 let mut stmt =
173 tx.prepare("SELECT data FROM attachments WHERE id = ? AND collection_url = ?")?;
174
175 if let Some(data) = stmt
176 .query_row((metadata.location, collection_url), |row| {
177 row.get::<_, Vec<u8>>(0)
178 })
179 .optional()?
180 {
181 if data.len() as u64 != metadata.size {
183 return Ok(None);
184 }
185 let hash = format!("{:x}", Sha256::digest(&data));
186 if hash != metadata.hash {
187 return Ok(None);
188 }
189 Ok(Some(data))
190 } else {
191 Ok(None)
192 }
193 }
194
195 pub fn insert_collection_content(
197 &mut self,
198 collection_url: &str,
199 records: &[RemoteSettingsRecord],
200 last_modified: u64,
201 metadata: CollectionMetadata,
202 ) -> Result<()> {
203 let tx = self.transaction()?;
204
205 tx.execute(
210 "DELETE FROM records where collection_url <> ?",
211 [collection_url],
212 )?;
213 tx.execute(
214 "DELETE FROM collection_metadata where collection_url <> ?",
215 [collection_url],
216 )?;
217
218 Self::update_record_rows(&tx, collection_url, records)?;
219 Self::update_collection_metadata(&tx, collection_url, last_modified, metadata)?;
220 tx.commit()?;
221 Ok(())
222 }
223
224 fn update_record_rows(
228 tx: &Transaction<'_>,
229 collection_url: &str,
230 records: &[RemoteSettingsRecord],
231 ) -> Result<u64> {
232 let mut max_last_modified = 0;
234 {
235 let mut insert_stmt = tx.prepare(
236 "INSERT OR REPLACE INTO records (id, collection_url, data) VALUES (?, ?, ?)",
237 )?;
238 let mut delete_stmt = tx.prepare("DELETE FROM records WHERE id=?")?;
239 for record in records {
240 if record.deleted {
241 delete_stmt.execute(params![&record.id])?;
242 } else {
243 max_last_modified = max_last_modified.max(record.last_modified);
244 let data = serde_json::to_vec(&record)?;
245 insert_stmt.execute(params![record.id, collection_url, data])?;
246 }
247 }
248 }
249 Ok(max_last_modified)
250 }
251
252 fn update_collection_metadata(
254 tx: &Transaction<'_>,
255 collection_url: &str,
256 last_modified: u64,
257 metadata: CollectionMetadata,
258 ) -> Result<()> {
259 tx.execute(
261 "INSERT OR REPLACE INTO collection_metadata \
262 (collection_url, last_modified, bucket, signature, x5u) \
263 VALUES (?, ?, ?, ?, ?)",
264 (
265 collection_url,
266 last_modified,
267 metadata.bucket,
268 metadata.signature.signature,
269 metadata.signature.x5u,
270 ),
271 )?;
272 Ok(())
273 }
274
275 pub fn set_attachment(
277 &mut self,
278 collection_url: &str,
279 location: &str,
280 attachment: &[u8],
281 ) -> Result<()> {
282 let tx = self.transaction()?;
283
284 tx.execute(
286 "DELETE FROM attachments WHERE collection_url != ?",
287 params![collection_url],
288 )?;
289
290 tx.execute(
291 "INSERT OR REPLACE INTO ATTACHMENTS \
292 (id, collection_url, data) \
293 VALUES (?, ?, ?)",
294 params![location, collection_url, attachment,],
295 )?;
296
297 tx.commit()?;
298
299 Ok(())
300 }
301
302 pub fn empty(&mut self) -> Result<()> {
306 let tx = self.transaction()?;
307 tx.execute("DELETE FROM records", [])?;
308 tx.execute("DELETE FROM attachments", [])?;
309 tx.execute("DELETE FROM collection_metadata", [])?;
310 tx.commit()?;
311 Ok(())
312 }
313}
314
315enum ConnectionCell {
317 Uninitialized,
318 Initialized(Connection),
319 Closed,
320}
321
322#[cfg(test)]
323mod tests {
324 use super::Storage;
325 use crate::{
326 client::CollectionMetadata, client::CollectionSignature, Attachment, RemoteSettingsRecord,
327 Result, RsJsonObject,
328 };
329 use sha2::{Digest, Sha256};
330
331 #[test]
332 fn test_storage_set_and_get_records() -> Result<()> {
333 let mut storage = Storage::new(":memory:".into());
334
335 let collection_url = "https://example.com/api";
336 let records = vec![
337 RemoteSettingsRecord {
338 id: "1".to_string(),
339 last_modified: 100,
340 deleted: false,
341 attachment: None,
342 fields: serde_json::json!({"key": "value1"})
343 .as_object()
344 .unwrap()
345 .clone(),
346 },
347 RemoteSettingsRecord {
348 id: "2".to_string(),
349 last_modified: 200,
350 deleted: false,
351 attachment: None,
352 fields: serde_json::json!({"key": "value2"})
353 .as_object()
354 .unwrap()
355 .clone(),
356 },
357 ];
358
359 storage.insert_collection_content(
361 collection_url,
362 &records,
363 300,
364 CollectionMetadata::default(),
365 )?;
366
367 let fetched_records = storage.get_records(collection_url)?;
369 assert!(fetched_records.is_some());
370 let fetched_records = fetched_records.unwrap();
371 assert_eq!(fetched_records.len(), 2);
372 assert_eq!(fetched_records, records);
373
374 assert_eq!(fetched_records[0].fields["key"], "value1");
375
376 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
378 assert_eq!(last_modified, Some(300));
379
380 Ok(())
381 }
382
383 #[test]
384 fn test_storage_get_records_none() -> Result<()> {
385 let mut storage = Storage::new(":memory:".into());
386
387 let collection_url = "https://example.com/api";
388
389 let fetched_records = storage.get_records(collection_url)?;
391 assert!(fetched_records.is_none());
392
393 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
395 assert!(last_modified.is_none());
396
397 Ok(())
398 }
399
400 #[test]
401 fn test_storage_get_records_empty() -> Result<()> {
402 let mut storage = Storage::new(":memory:".into());
403
404 let collection_url = "https://example.com/api";
405
406 storage.insert_collection_content(
408 collection_url,
409 &Vec::<RemoteSettingsRecord>::default(),
410 42,
411 CollectionMetadata::default(),
412 )?;
413
414 let fetched_records = storage.get_records(collection_url)?;
416 assert_eq!(fetched_records, Some(Vec::new()));
417
418 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
420 assert_eq!(last_modified, Some(42));
421
422 Ok(())
423 }
424
425 #[test]
426 fn test_storage_set_and_get_attachment() -> Result<()> {
427 let mut storage = Storage::new(":memory:".into());
428
429 let attachment = &[0x18, 0x64];
430 let collection_url = "https://example.com/api";
431 let attachment_metadata = Attachment {
432 filename: "abc".to_string(),
433 mimetype: "application/json".to_string(),
434 location: "tmp".to_string(),
435 hash: format!("{:x}", Sha256::digest(attachment)),
436 size: attachment.len() as u64,
437 };
438
439 storage.set_attachment(collection_url, &attachment_metadata.location, attachment)?;
441
442 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata)?;
444 assert!(fetched_attachment.is_some());
445 let fetched_attachment = fetched_attachment.unwrap();
446 assert_eq!(fetched_attachment, attachment);
447
448 Ok(())
449 }
450
451 #[test]
452 fn test_storage_set_and_replace_attachment() -> Result<()> {
453 let mut storage = Storage::new(":memory:".into());
454
455 let collection_url = "https://example.com/api";
456
457 let attachment_1 = &[0x18, 0x64];
458 let attachment_2 = &[0x12, 0x48];
459
460 let attachment_metadata_1 = Attachment {
461 filename: "abc".to_string(),
462 mimetype: "application/json".to_string(),
463 location: "tmp".to_string(),
464 hash: format!("{:x}", Sha256::digest(attachment_1)),
465 size: attachment_1.len() as u64,
466 };
467
468 let attachment_metadata_2 = Attachment {
469 filename: "def".to_string(),
470 mimetype: "application/json".to_string(),
471 location: "tmp".to_string(),
472 hash: format!("{:x}", Sha256::digest(attachment_2)),
473 size: attachment_2.len() as u64,
474 };
475
476 storage.set_attachment(
478 collection_url,
479 &attachment_metadata_1.location,
480 attachment_1,
481 )?;
482
483 storage.set_attachment(
485 collection_url,
486 &attachment_metadata_2.location,
487 attachment_2,
488 )?;
489
490 let fetched_attachment = storage.get_attachment(collection_url, attachment_metadata_2)?;
492 assert!(fetched_attachment.is_some());
493 let fetched_attachment = fetched_attachment.unwrap();
494 assert_eq!(fetched_attachment, attachment_2);
495
496 Ok(())
497 }
498
499 #[test]
500 fn test_storage_set_attachment_delete_others() -> Result<()> {
501 let mut storage = Storage::new(":memory:".into());
502
503 let collection_url_1 = "https://example.com/api1";
504 let collection_url_2 = "https://example.com/api2";
505
506 let attachment_1 = &[0x18, 0x64];
507 let attachment_2 = &[0x12, 0x48];
508
509 let attachment_metadata_1 = Attachment {
510 filename: "abc".to_string(),
511 mimetype: "application/json".to_string(),
512 location: "first_tmp".to_string(),
513 hash: format!("{:x}", Sha256::digest(attachment_1)),
514 size: attachment_1.len() as u64,
515 };
516
517 let attachment_metadata_2 = Attachment {
518 filename: "def".to_string(),
519 mimetype: "application/json".to_string(),
520 location: "second_tmp".to_string(),
521 hash: format!("{:x}", Sha256::digest(attachment_2)),
522 size: attachment_2.len() as u64,
523 };
524
525 storage.set_attachment(
527 collection_url_1,
528 &attachment_metadata_1.location,
529 attachment_1,
530 )?;
531 storage.set_attachment(
532 collection_url_2,
533 &attachment_metadata_2.location,
534 attachment_2,
535 )?;
536
537 let fetched_attachment_1 =
539 storage.get_attachment(collection_url_1, attachment_metadata_1)?;
540 assert!(fetched_attachment_1.is_none());
541
542 let fetched_attachment_2 =
543 storage.get_attachment(collection_url_2, attachment_metadata_2)?;
544 assert!(fetched_attachment_2.is_some());
545 let fetched_attachment_2 = fetched_attachment_2.unwrap();
546 assert_eq!(fetched_attachment_2, attachment_2);
547
548 Ok(())
549 }
550
551 #[test]
552 fn test_storage_get_attachment_not_found() -> Result<()> {
553 let mut storage = Storage::new(":memory:".into());
554
555 let collection_url = "https://example.com/api";
556 let metadata = Attachment::default();
557
558 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
560 assert!(fetched_attachment.is_none());
561
562 Ok(())
563 }
564
565 #[test]
566 fn test_storage_empty() -> Result<()> {
567 let mut storage = Storage::new(":memory:".into());
568
569 let collection_url = "https://example.com/api";
570 let attachment = &[0x18, 0x64];
571
572 let records = vec![
573 RemoteSettingsRecord {
574 id: "1".to_string(),
575 last_modified: 100,
576 deleted: false,
577 attachment: None,
578 fields: serde_json::json!({"key": "value1"})
579 .as_object()
580 .unwrap()
581 .clone(),
582 },
583 RemoteSettingsRecord {
584 id: "2".to_string(),
585 last_modified: 200,
586 deleted: false,
587 attachment: Some(Attachment {
588 filename: "abc".to_string(),
589 mimetype: "application/json".to_string(),
590 location: "tmp".to_string(),
591 hash: format!("{:x}", Sha256::digest(attachment)),
592 size: attachment.len() as u64,
593 }),
594 fields: serde_json::json!({"key": "value2"})
595 .as_object()
596 .unwrap()
597 .clone(),
598 },
599 ];
600
601 let metadata = records[1]
602 .clone()
603 .attachment
604 .expect("No attachment metadata for record");
605
606 storage.insert_collection_content(
608 collection_url,
609 &records,
610 42,
611 CollectionMetadata::default(),
612 )?;
613 storage.set_attachment(collection_url, &metadata.location, attachment)?;
614
615 let fetched_records = storage.get_records(collection_url)?;
617 assert!(fetched_records.is_some());
618 let fetched_attachment = storage.get_attachment(collection_url, metadata.clone())?;
619 assert!(fetched_attachment.is_some());
620
621 storage.empty()?;
623
624 let fetched_records = storage.get_records(collection_url)?;
626 assert!(fetched_records.is_none());
627 let fetched_attachment = storage.get_attachment(collection_url, metadata)?;
628 assert!(fetched_attachment.is_none());
629
630 Ok(())
631 }
632
633 #[test]
634 fn test_storage_collection_url_isolation() -> Result<()> {
635 let mut storage = Storage::new(":memory:".into());
636
637 let collection_url1 = "https://example.com/api1";
638 let collection_url2 = "https://example.com/api2";
639 let records_collection_url1 = vec![RemoteSettingsRecord {
640 id: "1".to_string(),
641 last_modified: 100,
642 deleted: false,
643 attachment: None,
644 fields: serde_json::json!({"key": "value1"})
645 .as_object()
646 .unwrap()
647 .clone(),
648 }];
649 let records_collection_url2 = vec![RemoteSettingsRecord {
650 id: "2".to_string(),
651 last_modified: 200,
652 deleted: false,
653 attachment: None,
654 fields: serde_json::json!({"key": "value2"})
655 .as_object()
656 .unwrap()
657 .clone(),
658 }];
659
660 storage.insert_collection_content(
662 collection_url1,
663 &records_collection_url1,
664 42,
665 CollectionMetadata::default(),
666 )?;
667 let fetched_records = storage.get_records(collection_url1)?;
669 assert!(fetched_records.is_some());
670 let fetched_records = fetched_records.unwrap();
671 assert_eq!(fetched_records.len(), 1);
672 assert_eq!(fetched_records, records_collection_url1);
673
674 storage.insert_collection_content(
676 collection_url2,
677 &records_collection_url2,
678 300,
679 CollectionMetadata::default(),
680 )?;
681
682 let fetched_records = storage.get_records(collection_url1)?;
684 assert!(fetched_records.is_none());
685
686 let fetched_records = storage.get_records(collection_url2)?;
688 assert!(fetched_records.is_some());
689 let fetched_records = fetched_records.unwrap();
690 assert_eq!(fetched_records.len(), 1);
691 assert_eq!(fetched_records, records_collection_url2);
692
693 let last_modified1 = storage.get_last_modified_timestamp(collection_url1)?;
695 assert_eq!(last_modified1, None);
696 let last_modified2 = storage.get_last_modified_timestamp(collection_url2)?;
697 assert_eq!(last_modified2, Some(300));
698
699 Ok(())
700 }
701
702 #[test]
703 fn test_storage_insert_collection_content() -> Result<()> {
704 let mut storage = Storage::new(":memory:".into());
705
706 let collection_url = "https://example.com/api";
707 let initial_records = vec![RemoteSettingsRecord {
708 id: "2".to_string(),
709 last_modified: 200,
710 deleted: false,
711 attachment: None,
712 fields: serde_json::json!({"key": "value2"})
713 .as_object()
714 .unwrap()
715 .clone(),
716 }];
717
718 storage.insert_collection_content(
720 collection_url,
721 &initial_records,
722 42,
723 CollectionMetadata::default(),
724 )?;
725
726 let fetched_records = storage.get_records(collection_url)?;
728 assert!(fetched_records.is_some());
729 assert_eq!(fetched_records.unwrap(), initial_records);
730
731 let updated_records = vec![RemoteSettingsRecord {
733 id: "2".to_string(),
734 last_modified: 200,
735 deleted: false,
736 attachment: None,
737 fields: serde_json::json!({"key": "value2_updated"})
738 .as_object()
739 .unwrap()
740 .clone(),
741 }];
742 storage.insert_collection_content(
743 collection_url,
744 &updated_records,
745 300,
746 CollectionMetadata::default(),
747 )?;
748
749 let fetched_records = storage.get_records(collection_url)?;
751 assert!(fetched_records.is_some());
752 assert_eq!(fetched_records.unwrap(), updated_records);
753
754 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
756 assert_eq!(last_modified, Some(300));
757
758 Ok(())
759 }
760
761 fn test_fields(data: &str) -> RsJsonObject {
763 let mut map = serde_json::Map::new();
764 map.insert("data".into(), data.into());
765 map
766 }
767
768 #[test]
769 fn test_storage_merge_records() -> Result<()> {
770 let mut storage = Storage::new(":memory:".into());
771
772 let collection_url = "https://example.com/api";
773
774 let initial_records = vec![
775 RemoteSettingsRecord {
776 id: "a".into(),
777 last_modified: 100,
778 deleted: false,
779 attachment: None,
780 fields: test_fields("a"),
781 },
782 RemoteSettingsRecord {
783 id: "b".into(),
784 last_modified: 200,
785 deleted: false,
786 attachment: None,
787 fields: test_fields("b"),
788 },
789 RemoteSettingsRecord {
790 id: "c".into(),
791 last_modified: 300,
792 deleted: false,
793 attachment: None,
794 fields: test_fields("c"),
795 },
796 ];
797 let updated_records = vec![
798 RemoteSettingsRecord {
800 id: "d".into(),
801 last_modified: 1300,
802 deleted: false,
803 attachment: None,
804 fields: test_fields("d"),
805 },
806 RemoteSettingsRecord {
808 id: "b".into(),
809 last_modified: 1200,
810 deleted: true,
811 attachment: None,
812 fields: RsJsonObject::new(),
813 },
814 RemoteSettingsRecord {
816 id: "a".into(),
817 last_modified: 1100,
818 deleted: false,
819 attachment: None,
820 fields: test_fields("a-with-new-data"),
821 },
822 ];
824 let expected_records = vec![
825 RemoteSettingsRecord {
827 id: "a".into(),
828 last_modified: 1100,
829 deleted: false,
830 attachment: None,
831 fields: test_fields("a-with-new-data"),
832 },
833 RemoteSettingsRecord {
834 id: "c".into(),
835 last_modified: 300,
836 deleted: false,
837 attachment: None,
838 fields: test_fields("c"),
839 },
840 RemoteSettingsRecord {
841 id: "d".into(),
842 last_modified: 1300,
843 deleted: false,
844 attachment: None,
845 fields: test_fields("d"),
846 },
847 ];
848
849 storage.insert_collection_content(
851 collection_url,
852 &initial_records,
853 1000,
854 CollectionMetadata::default(),
855 )?;
856
857 let fetched_records = storage.get_records(collection_url)?.unwrap();
859 assert_eq!(fetched_records, initial_records);
860
861 storage.insert_collection_content(
863 collection_url,
864 &updated_records,
865 1300,
866 CollectionMetadata::default(),
867 )?;
868
869 let mut fetched_records = storage.get_records(collection_url)?.unwrap();
871 fetched_records.sort_by_cached_key(|r| r.id.clone());
872 assert_eq!(fetched_records, expected_records);
873
874 let last_modified = storage.get_last_modified_timestamp(collection_url)?;
876 assert_eq!(last_modified, Some(1300));
877 Ok(())
878 }
879 #[test]
880 fn test_storage_get_collection_metadata() -> Result<()> {
881 let mut storage = Storage::new(":memory:".into());
882
883 let collection_url = "https://example.com/api";
884 let initial_records = vec![RemoteSettingsRecord {
885 id: "2".to_string(),
886 last_modified: 200,
887 deleted: false,
888 attachment: None,
889 fields: serde_json::json!({"key": "value2"})
890 .as_object()
891 .unwrap()
892 .clone(),
893 }];
894
895 storage.insert_collection_content(
897 collection_url,
898 &initial_records,
899 1337,
900 CollectionMetadata {
901 bucket: "main".into(),
902 signature: CollectionSignature {
903 signature: "b64encodedsig".into(),
904 x5u: "http://15u/".into(),
905 },
906 },
907 )?;
908
909 let metadata = storage.get_collection_metadata(collection_url)?.unwrap();
910
911 assert_eq!(metadata.signature.signature, "b64encodedsig");
912 assert_eq!(metadata.signature.x5u, "http://15u/");
913
914 Ok(())
915 }
916}