pub mod datastore;
pub mod entity;
pub mod log;
mod wire;
use hidane_proto::google::firestore::v1::{Value, value::ValueType};
use hidane_proto::ordered::Fields;
pub use wire::Malformed;
use crate::path::ResourcePath;
use wire::{Reader, Writer};
pub const VERSION: u8 = 0x33;
pub const KIND_METADATA: &str = "all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata";
pub const OUTPUT: &str = "output-0";
#[derive(Debug, Clone, PartialEq)]
pub struct ExportedDocument {
pub database_id: String,
pub path: ResourcePath,
pub fields: Fields,
}
impl ExportedDocument {
pub fn fields_in(&self, project: &str) -> Fields {
fn resolve(value: &mut Value, project: &str) {
match &mut value.value_type {
Some(ValueType::ReferenceValue(name)) => {
if let Some(rest) = name.strip_prefix("projects//") {
*name = format!("projects/{project}/{rest}");
}
}
Some(ValueType::ArrayValue(array)) => {
array.values.iter_mut().for_each(|v| resolve(v, project));
}
_ => {}
}
}
let mut fields = self.fields.clone();
fields.values_mut().for_each(|v| resolve(v, project));
fields
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pointer {
pub path: String,
pub entities: u64,
pub bytes: u64,
}
pub fn overall_metadata(pointer: Option<&Pointer>) -> Vec<u8> {
let mut w = Writer::default();
if let Some(pointer) = pointer {
w.message(1, |p| {
p.message(1, |format| {
format.uint(1, 2);
format.uint(3, 3);
});
p.bytes(2, pointer.path.as_bytes());
p.uint(3, pointer.entities);
p.uint(4, pointer.bytes);
});
}
log::write_all([&[VERSION][..], &w.into_bytes()])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverallError {
Version,
Malformed,
}
pub fn parse_overall_metadata(data: &[u8]) -> Result<Vec<String>, OverallError> {
let mut records = log::records(data);
match records.next() {
Some(Ok(version)) if *version == [VERSION] => {}
_ => return Err(OverallError::Version),
}
let Some(Ok(metadata)) = records.next() else {
return Err(OverallError::Malformed);
};
let mut paths = Vec::new();
for field in Reader::new(&metadata) {
let (number, pointer) = field.map_err(|_| OverallError::Malformed)?;
if number != 1 {
continue;
}
for field in pointer.fields().ok_or(OverallError::Malformed)? {
let (number, value) = field.map_err(|_| OverallError::Malformed)?;
if number == 2 {
let path = value.as_bytes().ok_or(OverallError::Malformed)?;
paths.push(String::from_utf8(path.to_vec()).map_err(|_| OverallError::Malformed)?);
}
}
}
Ok(paths)
}
pub fn export_metadata(name: &str, start_micros: i64, end_micros: i64) -> Vec<u8> {
let mut w = Writer::default();
w.message(1, |m| {
m.bytes(1, name.as_bytes());
m.int(2, start_micros);
m.int(3, end_micros);
});
w.message(2, |files| {
files.bytes(1, b"");
files.bytes(2, OUTPUT.as_bytes());
});
w.into_bytes()
}
pub fn parse_export_metadata(data: &[u8]) -> Result<Vec<String>, Malformed> {
let mut outputs = Vec::new();
for field in Reader::new(data) {
let (number, files) = field?;
if number != 2 {
continue;
}
let mut prefix = String::new();
for field in files.fields().ok_or(Malformed)? {
let (number, value) = field?;
let text = String::from_utf8(value.as_bytes().ok_or(Malformed)?.to_vec())
.map_err(|_| Malformed)?;
match number {
1 => prefix = text,
2 if prefix.is_empty() => outputs.push(text),
2 => outputs.push(format!("{}/{text}", prefix.trim_end_matches('/'))),
_ => {}
}
}
}
Ok(outputs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_export_points_at_nothing() {
let data = overall_metadata(None);
assert_eq!(parse_overall_metadata(&data), Ok(vec![]));
assert_eq!(data.len(), 15);
}
#[test]
fn metadata_round_trips() {
let pointer = Pointer {
path: KIND_METADATA.to_owned(),
entities: 9,
bytes: 4928,
};
assert_eq!(
parse_overall_metadata(&overall_metadata(Some(&pointer))),
Ok(vec![KIND_METADATA.to_owned()])
);
assert_eq!(
parse_export_metadata(&export_metadata("n", 1, 2)),
Ok(vec![OUTPUT.to_owned()])
);
}
#[test]
fn another_version_is_refused() {
assert_eq!(
parse_overall_metadata(&log::write_all([&[0x34u8][..], &[]])),
Err(OverallError::Version)
);
assert_eq!(
parse_overall_metadata(b"garbage"),
Err(OverallError::Version)
);
}
}