hidane_core/export/
mod.rs1pub mod entity;
16pub mod log;
17mod wire;
18
19use hidane_proto::google::firestore::v1::{Value, value::ValueType};
20use hidane_proto::ordered::Fields;
21
22pub use wire::Malformed;
23
24use crate::path::ResourcePath;
25use wire::{Reader, Writer};
26
27pub const VERSION: u8 = 0x33;
29
30pub const KIND_METADATA: &str = "all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata";
32
33pub const OUTPUT: &str = "output-0";
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct ExportedDocument {
39 pub database_id: String,
41 pub path: ResourcePath,
42 pub fields: Fields,
45}
46
47impl ExportedDocument {
48 pub fn fields_in(&self, project: &str) -> Fields {
51 fn resolve(value: &mut Value, project: &str) {
52 match &mut value.value_type {
53 Some(ValueType::ReferenceValue(name)) => {
54 if let Some(rest) = name.strip_prefix("projects//") {
55 *name = format!("projects/{project}/{rest}");
56 }
57 }
58 Some(ValueType::ArrayValue(array)) => {
59 array.values.iter_mut().for_each(|v| resolve(v, project));
60 }
61 _ => {}
62 }
63 }
64 let mut fields = self.fields.clone();
65 fields.values_mut().for_each(|v| resolve(v, project));
66 fields
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Pointer {
73 pub path: String,
75 pub entities: u64,
76 pub bytes: u64,
78}
79
80pub fn overall_metadata(pointer: Option<&Pointer>) -> Vec<u8> {
82 let mut w = Writer::default();
83 if let Some(pointer) = pointer {
84 w.message(1, |p| {
85 p.message(1, |format| {
87 format.uint(1, 2);
88 format.uint(3, 3);
89 });
90 p.bytes(2, pointer.path.as_bytes());
91 p.uint(3, pointer.entities);
92 p.uint(4, pointer.bytes);
93 });
94 }
95 log::write_all([&[VERSION][..], &w.into_bytes()])
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum OverallError {
101 Version,
103 Malformed,
104}
105
106pub fn parse_overall_metadata(data: &[u8]) -> Result<Vec<String>, OverallError> {
108 let mut records = log::records(data);
109 match records.next() {
110 Some(Ok(version)) if *version == [VERSION] => {}
111 _ => return Err(OverallError::Version),
112 }
113 let Some(Ok(metadata)) = records.next() else {
114 return Err(OverallError::Malformed);
115 };
116 let mut paths = Vec::new();
117 for field in Reader::new(&metadata) {
118 let (number, pointer) = field.map_err(|_| OverallError::Malformed)?;
119 if number != 1 {
120 continue;
121 }
122 for field in pointer.fields().ok_or(OverallError::Malformed)? {
123 let (number, value) = field.map_err(|_| OverallError::Malformed)?;
124 if number == 2 {
125 let path = value.as_bytes().ok_or(OverallError::Malformed)?;
126 paths.push(String::from_utf8(path.to_vec()).map_err(|_| OverallError::Malformed)?);
127 }
128 }
129 }
130 Ok(paths)
131}
132
133pub fn export_metadata(name: &str, start_micros: i64, end_micros: i64) -> Vec<u8> {
135 let mut w = Writer::default();
136 w.message(1, |m| {
137 m.bytes(1, name.as_bytes());
138 m.int(2, start_micros);
139 m.int(3, end_micros);
140 });
141 w.message(2, |files| {
142 files.bytes(1, b"");
143 files.bytes(2, OUTPUT.as_bytes());
144 });
145 w.into_bytes()
146}
147
148pub fn parse_export_metadata(data: &[u8]) -> Result<Vec<String>, Malformed> {
150 let mut outputs = Vec::new();
151 for field in Reader::new(data) {
152 let (number, files) = field?;
153 if number != 2 {
154 continue;
155 }
156 let mut prefix = String::new();
157 for field in files.fields().ok_or(Malformed)? {
158 let (number, value) = field?;
159 let text = String::from_utf8(value.as_bytes().ok_or(Malformed)?.to_vec())
160 .map_err(|_| Malformed)?;
161 match number {
162 1 => prefix = text,
163 2 if prefix.is_empty() => outputs.push(text),
164 2 => outputs.push(format!("{}/{text}", prefix.trim_end_matches('/'))),
165 _ => {}
166 }
167 }
168 }
169 Ok(outputs)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn an_empty_export_points_at_nothing() {
178 let data = overall_metadata(None);
179 assert_eq!(parse_overall_metadata(&data), Ok(vec![]));
180 assert_eq!(data.len(), 15);
181 }
182
183 #[test]
184 fn metadata_round_trips() {
185 let pointer = Pointer {
186 path: KIND_METADATA.to_owned(),
187 entities: 9,
188 bytes: 4928,
189 };
190 assert_eq!(
191 parse_overall_metadata(&overall_metadata(Some(&pointer))),
192 Ok(vec![KIND_METADATA.to_owned()])
193 );
194 assert_eq!(
195 parse_export_metadata(&export_metadata("n", 1, 2)),
196 Ok(vec![OUTPUT.to_owned()])
197 );
198 }
199
200 #[test]
201 fn another_version_is_refused() {
202 assert_eq!(
203 parse_overall_metadata(&log::write_all([&[0x34u8][..], &[]])),
204 Err(OverallError::Version)
205 );
206 assert_eq!(
207 parse_overall_metadata(b"garbage"),
208 Err(OverallError::Version)
209 );
210 }
211}