sync15/bso/
mod.rs

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/// This module defines our core "bso" abstractions.
6/// In the terminology of this crate:
7/// * "bso" is an acronym for "basic storage object" and used extensively in the sync server docs.
8///   the record always has a well-defined "envelope" with metadata (eg, the ID of the record,
9///   the server timestamp of the resource,  etc) and a field called `payload`.
10///   A bso is serialized to and from JSON.
11/// * There's a "cleartext" bso:
12///   * The payload is a String, which itself is JSON encoded (ie, this string `payload` is
13///     always double JSON encoded in a server record)
14///   * This supplies helper methods for working with the "content" (some arbitrary <T>) in the
15///     payload.
16/// * There's an "encrypted" bso
17///   * The payload is an [crate::enc_payload::EncryptedPayload]
18///   * Only clients use this; as soon as practical we decrypt and as late as practical we encrypt
19///     to and from encrypted bsos.
20///    * The encrypted bsos etc are all in the [crypto] module and require the `crypto` feature.
21///
22/// Let's look at some real-world examples:
23/// # meta/global
24/// A "bso" (ie, record with an "envelope" and a "payload" with a JSON string) - but the payload
25/// is cleartext.
26/// ```json
27/// {
28///   "id":"global",
29///   "modified":1661564513.50,
30///   "payload": "{\"syncID\":\"p1z5_oDdOfLF\",\"storageVersion\":5,\"engines\":{\"passwords\":{\"version\":1,\"syncID\":\"6Y6JJkB074cF\"} /* snip */},\"declined\":[]}"
31/// }```
32///
33/// # encrypted bsos:
34/// Encrypted BSOs are still a "bso" (ie, a record with a field names `payload` which is a string)
35/// but the payload is in the form of an EncryptedPayload.
36/// For example, crypto/keys:
37/// ```json
38/// {
39///   "id":"keys",
40///   "modified":1661564513.74,
41///   "payload":"{\"IV\":\"snip-base-64==\",\"hmac\":\"snip-hex\",\"ciphertext\":\"snip-base64==\"}"
42/// }```
43/// (Note that as described above, most code working with bsos *do not* use that `payload`
44/// directly, but instead a decrypted cleartext bso.
45///
46/// Note all collection responses are the same shape as `crypto/keys` - a `payload` field with a
47/// JSON serialized EncryptedPayload, it's just that the final <T> content differs for each
48/// collection (eg, tabs and bookmarks have quite different <T>s JSON-encoded in the
49/// String payload.)
50///
51/// For completeness, some other "non-BSO" records - no "id", "modified" or "payload" fields in
52/// the response, just plain-old clear-text JSON.
53/// # Example
54/// ## `info/collections`
55/// ```json
56/// {
57///   "bookmarks":1661564648.65,
58///   "meta":1661564513.50,
59///   "addons":1661564649.09,
60///   "clients":1661564643.57,
61///   ...
62/// }```
63/// ## `info/configuration`
64/// ```json
65/// {
66///   "max_post_bytes":2097152,
67///   "max_post_records":100,
68///   "max_record_payload_bytes":2097152,
69///   ...
70/// }```
71///
72/// Given our definitions above, these are not any kind of "bso", so are
73/// not relevant to this module
74use crate::{Guid, ServerTimestamp};
75use serde::{Deserialize, Serialize};
76
77#[cfg(feature = "crypto")]
78mod crypto;
79#[cfg(feature = "crypto")]
80pub use crypto::{IncomingEncryptedBso, OutgoingEncryptedBso};
81
82mod content;
83
84// A feature for this would be ideal, but (a) the module is small and (b) it
85// doesn't really fit the "features" model for sync15 to have a dev-dependency
86// against itself but with a different feature set.
87pub mod test_utils;
88
89/// An envelope for an incoming item. Envelopes carry all the metadata for
90/// a Sync BSO record (`id`, `modified`, `sortindex`), *but not* the payload
91/// itself.
92#[derive(Debug, Clone, Deserialize)]
93pub struct IncomingEnvelope {
94    /// The ID of the record.
95    pub id: Guid,
96    // If we don't give it a default, a small handful of tests fail.
97    // XXX - we should probably fix the tests and kill this?
98    #[serde(default = "ServerTimestamp::default")]
99    pub modified: ServerTimestamp,
100    pub sortindex: Option<i32>,
101    pub ttl: Option<u32>,
102}
103
104/// An envelope for an outgoing item. This is conceptually identical to
105/// [IncomingEnvelope], but omits fields that are only set by the server,
106/// like `modified`.
107#[derive(Debug, Default, Clone, Serialize)]
108pub struct OutgoingEnvelope {
109    /// The ID of the record.
110    pub id: Guid,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub sortindex: Option<i32>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub ttl: Option<u32>,
115}
116
117/// Allow an outgoing envelope to be constructed with just a guid when default
118/// values for the other fields are OK.
119impl From<Guid> for OutgoingEnvelope {
120    fn from(id: Guid) -> Self {
121        OutgoingEnvelope {
122            id,
123            ..Default::default()
124        }
125    }
126}
127
128/// IncomingBso's can come from:
129/// * Directly from the server (ie, some records aren't encrypted, such as meta/global)
130/// * From environments where the encryption is done externally (eg, Rust syncing in Desktop
131///   Firefox has the encryption/decryption done by Firefox and the cleartext BSOs are passed in.
132/// * Read from the server as an EncryptedBso; see EncryptedBso description above.
133#[derive(Deserialize, Debug)]
134pub struct IncomingBso {
135    #[serde(flatten)]
136    pub envelope: IncomingEnvelope,
137    // payload is public for some edge-cases in some components, but in general,
138    // you should use into_content<> to get a record out of it.
139    pub payload: String,
140}
141
142impl IncomingBso {
143    pub fn new(envelope: IncomingEnvelope, payload: String) -> Self {
144        Self { envelope, payload }
145    }
146}
147
148#[derive(Serialize, Debug)]
149pub struct OutgoingBso {
150    #[serde(flatten)]
151    pub envelope: OutgoingEnvelope,
152    // payload is public for some edge-cases in some components, but in general,
153    // you should use into_content<> to get a record out of it.
154    pub payload: String,
155}
156
157impl OutgoingBso {
158    /// Most consumers will use `self.from_content` and `self.from_content_with_id`
159    /// but this exists for the few consumers for whom that doesn't make sense.
160    pub fn new<T: Serialize>(
161        envelope: OutgoingEnvelope,
162        val: &T,
163    ) -> Result<Self, serde_json::Error> {
164        Ok(Self {
165            envelope,
166            payload: serde_json::to_string(&val)?,
167        })
168    }
169}
170
171// We also have the concept of "content", which helps work with a `T` which
172// is represented inside the payload. Real-world examples of a `T` include
173// Bookmarks or Tabs.
174// See the content module for the implementations.
175//
176// So this all flows together in the following way:
177// * Incoming encrypted data:
178//   EncryptedIncomingBso -> IncomingBso -> [specific engine] -> IncomingContent<T>
179// * Incoming cleartext data:
180//   IncomingBso -> IncomingContent<T>
181//   (Note that incoming cleartext only happens for a few collections managed by
182//   the sync client and never by specific engines - engine BSOs are always encryted)
183// * Outgoing encrypted data:
184//   OutgoingBso (created in the engine) -> [this crate] -> EncryptedOutgoingBso
185//  * Outgoing cleartext data: just an OutgoingBso with no conversions needed.
186
187/// [IncomingContent] is the result of converting an [IncomingBso] into
188/// some <T> - it consumes the Bso, so you get the envelope, and the [IncomingKind]
189/// which reflects the state of parsing the json.
190#[derive(Debug)]
191pub struct IncomingContent<T> {
192    pub envelope: IncomingEnvelope,
193    pub kind: IncomingKind<T>,
194}
195
196/// The "kind" of incoming content after deserializing it.
197pub enum IncomingKind<T> {
198    /// A good, live T.
199    Content(T),
200    /// A record that used to be a T but has been replaced with a tombstone.
201    Tombstone,
202    /// Either not JSON, or can't be made into a T.
203    Malformed,
204}