1use super::event_path::{EventPath, base64_decode_standard};
17use super::json_record::{decode_fatal, number_equals, parse_object, safe_integer, string_field};
19use super::recovery_error::RecoveryError;
21use super::transaction_journal::{
23 AddedPath, Base, Conclusion, FileIdentity, IndexLockRecord, JOURNAL_SCHEMA_VERSION,
24 LandingOperation, LandingRecord, LockIdentity, PREPARED_FILENAME, PREPARING_FILENAME,
25 PreparedRecord, PreparingRecord, REF_UPDATED_FILENAME, RefFormat, SymbolicHead,
26 TransactionMode,
27};
28use serde_json::{Map, Value};
39
40pub struct RecordReader<'a> {
53 name: String,
56 map: &'a Map<String, Value>,
58}
59
60impl<'a> RecordReader<'a> {
63 pub fn new(name: &str, map: &'a Map<String, Value>) -> Self {
73 return Self {
74 name: String::from(name),
75 map,
76 };
77 }
78
79 pub fn malformed(&self, key: &str) -> RecoveryError {
89 return RecoveryError(format!(
90 "Transaction record {} has a malformed {key} field.",
91 self.name
92 ));
93 }
94
95 pub fn string(&self, key: &str) -> Result<String, RecoveryError> {
108 match string_field(self.map, key) {
109 Some(text) => return Ok(String::from(text)),
110 None => return Err(self.malformed(key)),
111 }
112 }
113
114 pub fn optional_string(&self, key: &str) -> Result<Option<String>, RecoveryError> {
126 if !self.map.contains_key(key) {
127 return Ok(None);
128 }
129 return Ok(Some(self.string(key)?));
130 }
131
132 pub fn boolean(&self, key: &str) -> Result<bool, RecoveryError> {
142 match self.map.get(key) {
143 Some(Value::Bool(flag)) => return Ok(*flag),
144 Some(_) | None => return Err(self.malformed(key)),
145 }
146 }
147
148 pub fn positive_integer(&self, key: &str) -> Result<i64, RecoveryError> {
158 match self.map.get(key).and_then(safe_integer) {
159 Some(whole) if whole >= 1 => return Ok(whole),
160 Some(_) | None => return Err(self.malformed(key)),
161 }
162 }
163
164 pub fn strings(&self, key: &str) -> Result<Vec<String>, RecoveryError> {
174 let Some(Value::Array(items)) = self.map.get(key) else {
175 return Err(self.malformed(key));
176 };
177 let mut texts: Vec<String> = Vec::with_capacity(items.len());
178 for item in items {
179 let Value::String(text) = item else {
180 return Err(self.malformed(key));
181 };
182 texts.push(text.clone());
183 }
184 return Ok(texts);
185 }
186
187 pub fn path(&self, key: &str, bytes_key: &str) -> Result<Vec<u8>, RecoveryError> {
200 let text: String = self.string(key)?;
201 if !self.map.contains_key(bytes_key) {
202 return Ok(text.into_bytes());
203 }
204 let encoded: String = self.string(bytes_key)?;
205 return self.exact_path(text.as_str(), encoded.as_str(), bytes_key);
206 }
207
208 pub fn paths(&self, key: &str, bytes_key: &str) -> Result<Vec<Vec<u8>>, RecoveryError> {
220 let texts: Vec<String> = self.strings(key)?;
221 if !self.map.contains_key(bytes_key) {
222 return Ok(texts.into_iter().map(String::into_bytes).collect());
223 }
224 let encoded: Vec<String> = self.strings(bytes_key)?;
225 if encoded.len() != texts.len() {
226 return Err(self.malformed(bytes_key));
227 }
228 let mut paths: Vec<Vec<u8>> = Vec::with_capacity(texts.len());
229 for (text, entry) in texts.iter().zip(encoded.iter()) {
230 paths.push(self.exact_path(text.as_str(), entry.as_str(), bytes_key)?);
231 }
232 return Ok(paths);
233 }
234
235 fn exact_path(&self, text: &str, encoded: &str, bytes_key: &str) -> Result<Vec<u8>, RecoveryError> {
246 let Some(bytes) = base64_decode_standard(encoded) else {
247 return Err(self.malformed(bytes_key));
248 };
249 if EventPath::from_git_bytes(bytes.as_slice()).text() != text {
250 return Err(self.malformed(bytes_key));
251 }
252 return Ok(bytes);
253 }
254
255 pub fn object(&self, key: &str) -> Result<RecordReader<'a>, RecoveryError> {
267 match self.map.get(key) {
268 Some(Value::Object(nested)) => {
269 return Ok(RecordReader {
270 name: format!("{}#{key}", self.name),
271 map: nested,
272 });
273 }
274 Some(_) | None => return Err(self.malformed(key)),
275 }
276 }
277
278 pub fn base(&self, key: &str) -> Result<Base, RecoveryError> {
290 let nested: RecordReader<'a> = self.object(key)?;
291 let kind: String = nested.string("kind")?;
292 if kind == "unborn" {
293 return Ok(Base::Unborn);
294 }
295 if kind != "commit" {
296 return Err(self.malformed(key));
297 }
298 return Ok(Base::Commit(nested.string("oid")?));
299 }
300
301 pub fn identity(&self, key: &str) -> Result<FileIdentity, RecoveryError> {
311 let nested: RecordReader<'a> = self.object(key)?;
312 return Ok(FileIdentity {
313 device: nested.string("device")?,
314 inode: nested.string("inode")?,
315 });
316 }
317
318 pub fn lock(&self, key: &str) -> Result<LockIdentity, RecoveryError> {
328 return Ok(LockIdentity {
329 file: self.identity(key)?,
330 fs_id: self.object(key)?.string("fsId")?,
331 });
332 }
333
334 pub fn added_paths(&self, key: &str) -> Result<Vec<AddedPath>, RecoveryError> {
345 let Some(Value::Array(items)) = self.map.get(key) else {
346 return Err(self.malformed(key));
347 };
348 let mut records: Vec<AddedPath> = Vec::with_capacity(items.len());
349 for item in items {
350 let Value::Object(fields) = item else {
352 return Err(self.malformed(key));
353 };
354 let nested: RecordReader<'_> = RecordReader {
355 name: format!("{}#{key}", self.name),
356 map: fields,
357 };
358 let git_mode: String = nested.string("gitMode")?;
359 if git_mode != "100644" && git_mode != "100755" {
360 return Err(self.malformed(key));
361 }
362 records.push(AddedPath {
363 path: nested.path("path", "pathBytes")?,
364 git_mode,
365 original_oid: nested.string("originalOid")?,
366 intended_oid: nested.string("intendedOid")?,
367 });
368 }
369 return Ok(records);
370 }
371}
372
373pub fn open_record(
391 bytes: &[u8],
392 name: &str,
393 state: &str,
394) -> Result<Map<String, Value>, RecoveryError> {
395 let failure: RecoveryError = RecoveryError(format!(
396 "Transaction record {name} is not a schema-version-2 {state} record."
397 ));
398 let Some(text) = decode_fatal(bytes) else {
399 return Err(failure);
400 };
401 let Some(map) = parse_object(text) else {
402 return Err(failure);
403 };
404 if !number_equals(&map, "schemaVersion", JOURNAL_SCHEMA_VERSION)
405 || string_field(&map, "state") != Some(state)
406 {
407 return Err(failure);
408 }
409 return Ok(map);
410}
411
412pub fn parse_preparing(bytes: &[u8]) -> Result<PreparingRecord, RecoveryError> {
422 let map: Map<String, Value> = open_record(bytes, PREPARING_FILENAME, "preparing")?;
423 let read: RecordReader<'_> = RecordReader::new(PREPARING_FILENAME, &map);
424 let mode_text: String = read.string("mode")?;
425 let conclusion_text: String = read.string("conclusion")?;
426 let format_text: String = read.string("refFormat")?;
427 let (Some(mode), Some(conclusion), Some(ref_format)) = (
428 parse_mode(mode_text.as_str()),
429 parse_conclusion(conclusion_text.as_str()),
430 parse_ref_format(format_text.as_str()),
431 ) else {
432 return Err(RecoveryError(String::from(
433 "Transaction record preparing.json has a malformed mode, conclusion, or refFormat field.",
434 )));
435 };
436 let transaction_id: String = read.string("transactionId")?;
437 let base: Base = read.base("base")?;
438 let head: RecordReader<'_> = read.object("symbolicHead")?;
439 let head_kind: String = head.string("kind")?;
440 let symbolic_head: SymbolicHead = if head_kind == "detached" {
441 SymbolicHead::Detached
442 } else if head_kind == "branch" {
443 SymbolicHead::Branch(head.string("ref")?)
444 } else {
445 return Err(RecoveryError(String::from(
446 "Transaction record preparing.json has a malformed symbolicHead field.",
447 )));
448 };
449 return Ok(PreparingRecord {
450 transaction_id,
451 mode,
452 base,
453 symbolic_head,
454 target_ref: read.string("targetRef")?,
455 conclusion,
456 repository_root: read.string("repositoryRoot")?,
457 git_dir: read.string("gitDir")?,
458 common_dir: read.string("commonDir")?,
459 real_index_path: read.string("realIndexPath")?,
460 object_directory: read.string("objectDirectory")?,
461 ref_format,
462 empty_tree_oid: read.string("emptyTreeOid")?,
463 shadow_path: read.string("shadowPath")?,
464 selected_pathspecs: read.paths("selectedPathspecs", "selectedPathspecBytes")?,
465 invoked_at: read.string("invokedAt")?,
466 });
467}
468
469fn parse_mode(text: &str) -> Option<TransactionMode> {
479 match text {
480 "explicit-path" => return Some(TransactionMode::ExplicitPath),
481 "index" => return Some(TransactionMode::Index),
482 _ => return None,
483 }
484}
485
486fn parse_conclusion(text: &str) -> Option<Conclusion> {
496 match text {
497 "none" => return Some(Conclusion::None),
498 "amend" => return Some(Conclusion::Amend),
499 "merge" => return Some(Conclusion::Merge),
500 "cherry-pick" => return Some(Conclusion::CherryPick),
501 "revert" => return Some(Conclusion::Revert),
502 _ => return None,
503 }
504}
505
506fn parse_ref_format(text: &str) -> Option<RefFormat> {
516 match text {
517 "files" => return Some(RefFormat::Files),
518 "reftable" => return Some(RefFormat::Reftable),
519 _ => return None,
520 }
521}
522
523pub fn parse_prepared(bytes: &[u8]) -> Result<PreparedRecord, RecoveryError> {
533 let map: Map<String, Value> = open_record(bytes, PREPARED_FILENAME, "prepared")?;
534 let read: RecordReader<'_> = RecordReader::new(PREPARED_FILENAME, &map);
535 return Ok(PreparedRecord {
536 shadow_path: read.string("shadowPath")?,
537 prepared_oid: read.string("preparedOid")?,
538 signed: read.boolean("signed")?,
539 intended_tree_oid: read.string("intendedTreeOid")?,
540 committed_paths: read.paths("committedPaths", "committedPathBytes")?,
541 added_paths: read.added_paths("addedPaths")?,
542 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
543 });
544}
545
546pub fn parse_index_lock(bytes: &[u8]) -> Result<IndexLockRecord, RecoveryError> {
556 let name: &str = "index-lock record";
557 let map: Map<String, Value> = open_record(bytes, name, "index-locked")?;
558 let read: RecordReader<'_> = RecordReader::new(name, &map);
559 return Ok(IndexLockRecord {
560 attempt: read.positive_integer("attempt")?,
561 lock: read.lock("lock")?,
562 });
563}
564
565pub fn parse_landing(bytes: &[u8]) -> Result<LandingRecord, RecoveryError> {
576 let name: &str = "landing record";
577 let map: Map<String, Value> = open_record(bytes, name, "landing")?;
578 let read: RecordReader<'_> = RecordReader::new(name, &map);
579 let operation_text: String = read.string("operation")?;
580 let new_oid: Option<String> = read.optional_string("newOid")?;
581 let pack_name: Option<String> = read.optional_string("packName")?;
582 let operation: LandingOperation = match operation_text.as_str() {
583 "commit" if new_oid.is_some() => LandingOperation::Commit,
584 "normalize-only" => LandingOperation::NormalizeOnly,
585 _ => {
586 return Err(RecoveryError(String::from(
587 "Transaction record landing record has a malformed operation or newOid field.",
588 )));
589 }
590 };
591 return Ok(LandingRecord {
592 attempt: read.positive_integer("attempt")?,
593 operation,
594 expected_old: read.base("expectedOld")?,
595 new_oid,
596 landed_tree_oid: read.string("landedTreeOid")?,
597 pre_landing_index: read.identity("preLandingIndex")?,
598 post_index: read.identity("postIndex")?,
599 lock: read.lock("lock")?,
600 pack_name,
601 added_paths: read.added_paths("addedPaths")?,
602 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
603 });
604}
605
606pub fn parse_ref_updated(bytes: &[u8]) -> Result<String, RecoveryError> {
616 let map: Map<String, Value> = open_record(bytes, REF_UPDATED_FILENAME, "ref-updated")?;
617 return RecordReader::new(REF_UPDATED_FILENAME, &map).string("landedOid");
618}
619
620#[cfg(test)]
622#[path = "transaction_journal_parse_tests.rs"]
623mod tests;