Skip to main content

hidane_core/export/
entity.rs

1//! Documents as App Engine `EntityProto`s, the records of an export's `output-*` files.
2//!
3//! The mapping, as the official emulator writes it (`docs/export-format.md`):
4//!
5//! - the key names the project as the app `dev~{project}`, the database (unless `(default)`)
6//!   and the path, a numeric document ID (`__id7__`) as an `id`, any other as a `name`;
7//! - fields are properties in field order, after the empty arrays, which are "indexed"
8//!   properties of their own meaning; an array is one property per element, marked `multiple`;
9//! - a map is a nested entity without a key, stored as a string of meaning 19; a vector is a
10//!   nested entity whose elements are `multiple` properties named `__vector__`;
11//! - timestamps are microseconds (meaning 7), bytes are strings of meaning 14, geo points are
12//!   points of meaning 9; `-0.0` is written as `0.0`.
13//!
14//! Reading, a reference in a top-level indexed property (as a managed export of a Native mode
15//! database writes most fields) belongs to the importing project, whatever its app says: the
16//! official emulator imports it so. It is decoded with an empty project
17//! ([`ExportedDocument::fields_in`] fills it in); references anywhere else keep theirs.
18
19use super::{
20    ExportedDocument,
21    wire::{Field, Malformed, Reader, Writer},
22};
23use crate::{
24    order::{
25        VECTOR_TYPE, VECTOR_TYPE_KEY, VECTOR_VALUES_KEY, is_vector, numeric_id, vector_elements,
26    },
27    path::ResourcePath,
28    store::ReadTime,
29};
30use hidane_proto::google::{
31    firestore::v1::{ArrayValue, MapValue, Value, value::ValueType},
32    r#type::LatLng,
33};
34use hidane_proto::ordered::Fields;
35
36const DEFAULT_DATABASE: &str = "(default)";
37const VECTOR_PROPERTY: &str = "__vector__";
38
39// EntityProto
40const KEY: u32 = 13;
41const PROPERTY: u32 = 14;
42const RAW_PROPERTY: u32 = 15;
43const ENTITY_GROUP: u32 = 16;
44// Reference (the key)
45const APP: u32 = 13;
46const PATH: u32 = 14;
47const DATABASE_ID: u32 = 23;
48// Path
49const ELEMENT: u32 = 1;
50const ELEMENT_TYPE: u32 = 2;
51const ELEMENT_ID: u32 = 3;
52const ELEMENT_NAME: u32 = 4;
53// Property
54const MEANING: u32 = 1;
55const NAME: u32 = 3;
56const MULTIPLE: u32 = 4;
57const VALUE: u32 = 5;
58// PropertyValue
59const INT64: u32 = 1;
60const BOOLEAN: u32 = 2;
61const STRING: u32 = 3;
62const DOUBLE: u32 = 4;
63const POINT: u32 = 5;
64const POINT_X: u32 = 6;
65const POINT_Y: u32 = 7;
66const REFERENCE: u32 = 12;
67const REFERENCE_APP: u32 = 13;
68const REFERENCE_ELEMENT: u32 = 14;
69const REFERENCE_TYPE: u32 = 15;
70const REFERENCE_ID: u32 = 16;
71const REFERENCE_NAME: u32 = 17;
72const REFERENCE_DATABASE_ID: u32 = 23;
73// Meanings
74const GD_WHEN: u64 = 7;
75const GEORSS_POINT: u64 = 9;
76const BLOB: u64 = 14;
77const BYTESTRING: u64 = 16;
78const ENTITY_PROTO: u64 = 19;
79const EMPTY_LIST: u64 = 24;
80
81/// Encodes the document at `path` of `projects/{project}/databases/{database_id}`.
82pub fn encode(project: &str, database_id: &str, path: &ResourcePath, fields: &Fields) -> Vec<u8> {
83    let segments: Vec<&str> = path.segments().collect();
84    let mut w = Writer::default();
85    w.message(KEY, |key| {
86        key.bytes(APP, format!("dev~{project}").as_bytes());
87        key.message(PATH, |p| {
88            for pair in segments.chunks(2) {
89                p.group(ELEMENT, |e| {
90                    element(e, pair, ELEMENT_TYPE, ELEMENT_ID, ELEMENT_NAME)
91                });
92            }
93        });
94        if database_id != DEFAULT_DATABASE {
95            key.bytes(DATABASE_ID, database_id.as_bytes());
96        }
97    });
98    properties(&mut w, fields);
99    w.message(ENTITY_GROUP, |p| {
100        if let Some(root) = segments.chunks(2).next() {
101            p.group(ELEMENT, |e| {
102                element(e, root, ELEMENT_TYPE, ELEMENT_ID, ELEMENT_NAME)
103            });
104        }
105    });
106    w.into_bytes()
107}
108
109/// A collection ID and a document ID.
110fn element(w: &mut Writer, pair: &[&str], kind: u32, id: u32, name: u32) {
111    w.bytes(kind, pair[0].as_bytes());
112    let doc = pair.get(1).copied().unwrap_or_default();
113    match numeric_id(doc) {
114        Some(n) => w.int(id, n),
115        None => w.bytes(name, doc.as_bytes()),
116    }
117}
118
119fn array(value: &Value) -> Option<&[Value]> {
120    match &value.value_type {
121        Some(ValueType::ArrayValue(array)) => Some(&array.values),
122        _ => None,
123    }
124}
125
126fn properties(w: &mut Writer, fields: &Fields) {
127    for (name, value) in fields {
128        if array(value).is_some_and(<[Value]>::is_empty) {
129            w.message(PROPERTY, |p| {
130                p.uint(MEANING, EMPTY_LIST);
131                p.bytes(NAME, name.as_bytes());
132                p.uint(MULTIPLE, 0);
133                p.message(VALUE, |_| {});
134            });
135        }
136    }
137    for (name, value) in fields {
138        match array(value) {
139            Some(values) => {
140                for element in values {
141                    property(w, name, element, true);
142                }
143            }
144            None => property(w, name, value, false),
145        }
146    }
147}
148
149fn property(w: &mut Writer, name: &str, value: &Value, multiple: bool) {
150    let meaning = match &value.value_type {
151        Some(ValueType::TimestampValue(_)) => Some(GD_WHEN),
152        Some(ValueType::BytesValue(_)) => Some(BLOB),
153        Some(ValueType::GeoPointValue(_)) => Some(GEORSS_POINT),
154        Some(ValueType::MapValue(_)) => Some(ENTITY_PROTO),
155        _ => None,
156    };
157    w.message(RAW_PROPERTY, |p| {
158        if let Some(meaning) = meaning {
159            p.uint(MEANING, meaning);
160        }
161        p.bytes(NAME, name.as_bytes());
162        p.uint(MULTIPLE, u64::from(multiple));
163        p.message(VALUE, |v| property_value(v, value));
164    });
165}
166
167fn property_value(w: &mut Writer, value: &Value) {
168    match &value.value_type {
169        Some(ValueType::BooleanValue(b)) => w.uint(BOOLEAN, u64::from(*b)),
170        Some(ValueType::IntegerValue(i)) => w.int(INT64, *i),
171        Some(ValueType::DoubleValue(d)) => w.double(DOUBLE, if *d == 0.0 { 0.0 } else { *d }),
172        Some(ValueType::TimestampValue(ts)) => w.int(INT64, ReadTime::from_timestamp(ts).0),
173        Some(ValueType::StringValue(s)) => w.bytes(STRING, s.as_bytes()),
174        Some(ValueType::BytesValue(b)) => w.bytes(STRING, b),
175        Some(ValueType::ReferenceValue(name)) => reference(w, name),
176        Some(ValueType::GeoPointValue(point)) => w.group(POINT, |p| {
177            p.double(POINT_X, point.latitude);
178            p.double(POINT_Y, point.longitude);
179        }),
180        Some(ValueType::MapValue(map)) => w.bytes(STRING, &nested(map)),
181        // Null; arrays are expanded by the caller; pipeline expressions are never stored.
182        _ => {}
183    }
184}
185
186fn reference(w: &mut Writer, name: &str) {
187    let Some((project, database, path)) = name
188        .strip_prefix("projects/")
189        .and_then(|rest| rest.split_once("/databases/"))
190        .and_then(|(project, rest)| {
191            let (database, path) = rest.split_once("/documents/")?;
192            Some((project, database, path))
193        })
194    else {
195        return;
196    };
197    let segments: Vec<&str> = path.split('/').collect();
198    w.group(REFERENCE, |r| {
199        r.bytes(REFERENCE_APP, format!("dev~{project}").as_bytes());
200        for pair in segments.chunks(2) {
201            r.group(REFERENCE_ELEMENT, |e| {
202                element(e, pair, REFERENCE_TYPE, REFERENCE_ID, REFERENCE_NAME);
203            });
204        }
205        if database != DEFAULT_DATABASE {
206            r.bytes(REFERENCE_DATABASE_ID, database.as_bytes());
207        }
208    });
209}
210
211/// A map as a nested entity: an empty key, its properties, an empty entity group.
212fn nested(map: &MapValue) -> Vec<u8> {
213    let mut w = Writer::default();
214    w.message(KEY, |key| {
215        key.bytes(APP, b"");
216        key.message(PATH, |_| {});
217    });
218    if is_vector(map) {
219        for element in vector_elements(map) {
220            property(&mut w, VECTOR_PROPERTY, element, true);
221        }
222    } else {
223        properties(&mut w, &map.fields);
224    }
225    w.message(ENTITY_GROUP, |_| {});
226    w.into_bytes()
227}
228
229/// Decodes an entity, as written by the official emulator or by [`encode`]. Properties may
230/// be indexed or not, in any order; the key's app is ignored.
231pub fn decode(bytes: &[u8]) -> Result<ExportedDocument, Malformed> {
232    let mut database_id = DEFAULT_DATABASE.to_owned();
233    let mut segments = None;
234    let mut fields = Fields::new();
235    for field in Reader::new(bytes) {
236        let (number, value) = field?;
237        match number {
238            KEY => {
239                let mut path = Vec::new();
240                for field in value.fields().ok_or(Malformed)? {
241                    let (number, value) = field?;
242                    match number {
243                        PATH => {
244                            for field in value.fields().ok_or(Malformed)? {
245                                let (number, value) = field?;
246                                if number == ELEMENT {
247                                    path.extend(decode_element(
248                                        value,
249                                        ELEMENT_TYPE,
250                                        ELEMENT_ID,
251                                        ELEMENT_NAME,
252                                    )?);
253                                }
254                            }
255                        }
256                        DATABASE_ID => database_id = text(value)?,
257                        _ => {}
258                    }
259                }
260                segments = Some(path);
261            }
262            PROPERTY => decode_property(&mut fields, value, Relative::Yes)?,
263            RAW_PROPERTY => decode_property(&mut fields, value, Relative::No)?,
264            _ => {}
265        }
266    }
267    let segments = segments.ok_or(Malformed)?;
268    if segments.is_empty() {
269        return Err(Malformed);
270    }
271    if database_id.is_empty() {
272        DEFAULT_DATABASE.clone_into(&mut database_id);
273    }
274    Ok(ExportedDocument {
275        database_id,
276        path: ResourcePath::from_segments(segments),
277        fields,
278    })
279}
280
281fn text(field: Field<'_>) -> Result<String, Malformed> {
282    String::from_utf8(field.as_bytes().ok_or(Malformed)?.to_vec()).map_err(|_| Malformed)
283}
284
285fn decode_element(
286    field: Field<'_>,
287    kind: u32,
288    id: u32,
289    name: u32,
290) -> Result<[String; 2], Malformed> {
291    let (mut collection, mut document) = (None, None);
292    for field in field.fields().ok_or(Malformed)? {
293        let (number, value) = field?;
294        if number == kind {
295            collection = Some(text(value)?);
296        } else if number == id {
297            #[allow(clippy::cast_possible_wrap)]
298            let n = value.as_varint().ok_or(Malformed)? as i64;
299            document = Some(format!("__id{n}__"));
300        } else if number == name {
301            document = Some(text(value)?);
302        }
303    }
304    Ok([collection.ok_or(Malformed)?, document.ok_or(Malformed)?])
305}
306
307/// Whether references are relative to the importing project.
308#[derive(Clone, Copy, PartialEq, Eq)]
309enum Relative {
310    Yes,
311    No,
312}
313
314fn decode_property(
315    fields: &mut Fields,
316    field: Field<'_>,
317    relative: Relative,
318) -> Result<(), Malformed> {
319    let (mut meaning, mut name, mut multiple, mut value) = (0, None, false, &[][..]);
320    for field in field.fields().ok_or(Malformed)? {
321        let (number, field) = field?;
322        match number {
323            MEANING => meaning = field.as_varint().ok_or(Malformed)?,
324            NAME => name = Some(text(field)?),
325            MULTIPLE => multiple = field.as_varint().ok_or(Malformed)? != 0,
326            VALUE => value = field.as_bytes().ok_or(Malformed)?,
327            _ => {}
328        }
329    }
330    let name = name.ok_or(Malformed)?;
331    if meaning == EMPTY_LIST {
332        fields
333            .entry(name)
334            .or_insert_with(|| array_value(Vec::new()));
335        return Ok(());
336    }
337    let value = decode_value(value, meaning, relative)?;
338    if multiple {
339        let entry = fields
340            .entry(name)
341            .or_insert_with(|| array_value(Vec::new()));
342        match &mut entry.value_type {
343            Some(ValueType::ArrayValue(array)) => array.values.push(value),
344            _ => *entry = array_value(vec![value]),
345        }
346    } else {
347        fields.insert(name, value);
348    }
349    Ok(())
350}
351
352fn array_value(values: Vec<Value>) -> Value {
353    Value {
354        value_type: Some(ValueType::ArrayValue(ArrayValue { values })),
355    }
356}
357
358fn decode_value(bytes: &[u8], meaning: u64, relative: Relative) -> Result<Value, Malformed> {
359    let mut value_type = ValueType::NullValue(0);
360    for field in Reader::new(bytes) {
361        let (number, field) = field?;
362        value_type = match number {
363            INT64 => {
364                #[allow(clippy::cast_possible_wrap)]
365                let n = field.as_varint().ok_or(Malformed)? as i64;
366                if meaning == GD_WHEN {
367                    ValueType::TimestampValue(ReadTime(n).to_timestamp())
368                } else {
369                    ValueType::IntegerValue(n)
370                }
371            }
372            BOOLEAN => ValueType::BooleanValue(field.as_varint().ok_or(Malformed)? != 0),
373            STRING => {
374                let bytes = field.as_bytes().ok_or(Malformed)?;
375                match meaning {
376                    BLOB | BYTESTRING => ValueType::BytesValue(bytes.to_vec()),
377                    ENTITY_PROTO => return decode_nested(bytes),
378                    _ => match String::from_utf8(bytes.to_vec()) {
379                        Ok(s) => ValueType::StringValue(s),
380                        Err(err) => ValueType::BytesValue(err.into_bytes()),
381                    },
382                }
383            }
384            DOUBLE => ValueType::DoubleValue(f64::from_bits(field.as_fixed64().ok_or(Malformed)?)),
385            POINT => {
386                let mut point = LatLng::default();
387                for field in field.fields().ok_or(Malformed)? {
388                    let (number, field) = field?;
389                    let x = f64::from_bits(field.as_fixed64().ok_or(Malformed)?);
390                    match number {
391                        POINT_X => point.latitude = x,
392                        POINT_Y => point.longitude = x,
393                        _ => {}
394                    }
395                }
396                ValueType::GeoPointValue(point)
397            }
398            REFERENCE => ValueType::ReferenceValue(decode_reference(field, relative)?),
399            _ => continue,
400        };
401    }
402    Ok(Value {
403        value_type: Some(value_type),
404    })
405}
406
407fn decode_reference(field: Field<'_>, relative: Relative) -> Result<String, Malformed> {
408    let (mut project, mut database, mut path) =
409        (String::new(), DEFAULT_DATABASE.to_owned(), Vec::new());
410    for field in field.fields().ok_or(Malformed)? {
411        let (number, field) = field?;
412        match number {
413            REFERENCE_APP => {
414                let app = text(field)?;
415                // `dev~project` (`s~project` in production): the partition goes.
416                if relative == Relative::No {
417                    project = app
418                        .split_once('~')
419                        .map_or(app.clone(), |(_, p)| p.to_owned());
420                }
421            }
422            REFERENCE_ELEMENT => path.extend(decode_element(
423                field,
424                REFERENCE_TYPE,
425                REFERENCE_ID,
426                REFERENCE_NAME,
427            )?),
428            REFERENCE_DATABASE_ID => {
429                let id = text(field)?;
430                if !id.is_empty() {
431                    database = id;
432                }
433            }
434            _ => {}
435        }
436    }
437    Ok(format!(
438        "projects/{project}/databases/{database}/documents/{}",
439        path.join("/")
440    ))
441}
442
443/// A nested entity: a map, or a vector when its properties are `__vector__`.
444fn decode_nested(bytes: &[u8]) -> Result<Value, Malformed> {
445    let mut fields = Fields::new();
446    for field in Reader::new(bytes) {
447        let (number, field) = field?;
448        if number == PROPERTY || number == RAW_PROPERTY {
449            decode_property(&mut fields, field, Relative::No)?;
450        }
451    }
452    if let Some(elements) = fields.remove(VECTOR_PROPERTY) {
453        let elements = match elements.value_type {
454            Some(ValueType::ArrayValue(_)) => elements,
455            _ => array_value(vec![elements]),
456        };
457        fields = Fields::from_iter([
458            (
459                VECTOR_TYPE_KEY.to_owned(),
460                Value {
461                    value_type: Some(ValueType::StringValue(VECTOR_TYPE.to_owned())),
462                },
463            ),
464            (VECTOR_VALUES_KEY.to_owned(), elements),
465        ]);
466    }
467    Ok(Value {
468        value_type: Some(ValueType::MapValue(MapValue { fields })),
469    })
470}