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