1use super::content_digest::ContentDigest;
17use super::event_path::{EventPath, base64_decode_standard};
19use super::json_record::{decode_fatal, number_equals, parse_object, safe_integer, string_field};
21use super::recovery_error::RecoveryError;
23use super::transaction_journal::{
25 AddedPath, Base, Conclusion, FileIdentity, IndexLockRecord, JOURNAL_SCHEMA_VERSION,
26 LandingOperation, LandingRecord, LockIdentity, PREPARED_FILENAME, PREPARING_FILENAME,
27 PreparedRecord, PreparingRecord, REF_UPDATED_FILENAME, RefFormat, SymbolicHead,
28 TransactionMode,
29};
30use serde_json::{Map, Value};
41
42pub struct RecordReader<'a> {
55 name: String,
58 map: &'a Map<String, Value>,
60}
61
62impl<'a> RecordReader<'a> {
65 pub fn new(name: &str, map: &'a Map<String, Value>) -> Self {
75 return Self {
76 name: String::from(name),
77 map,
78 };
79 }
80
81 pub fn malformed(&self, key: &str) -> RecoveryError {
91 return RecoveryError(format!(
92 "Transaction record {} has a malformed {key} field.",
93 self.name
94 ));
95 }
96
97 pub fn string(&self, key: &str) -> Result<String, RecoveryError> {
110 match string_field(self.map, key) {
111 Some(text) => return Ok(String::from(text)),
112 None => return Err(self.malformed(key)),
113 }
114 }
115
116 pub fn optional_string(&self, key: &str) -> Result<Option<String>, RecoveryError> {
128 if !self.map.contains_key(key) {
129 return Ok(None);
130 }
131 return Ok(Some(self.string(key)?));
132 }
133
134 pub fn boolean(&self, key: &str) -> Result<bool, RecoveryError> {
144 match self.map.get(key) {
145 Some(Value::Bool(flag)) => return Ok(*flag),
146 Some(_) | None => return Err(self.malformed(key)),
147 }
148 }
149
150 pub fn positive_integer(&self, key: &str) -> Result<i64, RecoveryError> {
160 match self.map.get(key).and_then(safe_integer) {
161 Some(whole) if whole >= 1 => return Ok(whole),
162 Some(_) | None => return Err(self.malformed(key)),
163 }
164 }
165
166 pub fn strings(&self, key: &str) -> Result<Vec<String>, RecoveryError> {
176 let Some(Value::Array(items)) = self.map.get(key) else {
177 return Err(self.malformed(key));
178 };
179 let mut texts: Vec<String> = Vec::with_capacity(items.len());
180 for item in items {
181 let Value::String(text) = item else {
182 return Err(self.malformed(key));
183 };
184 texts.push(text.clone());
185 }
186 return Ok(texts);
187 }
188
189 pub fn path(&self, key: &str, bytes_key: &str) -> Result<Vec<u8>, RecoveryError> {
202 let text: String = self.string(key)?;
203 if !self.map.contains_key(bytes_key) {
204 return Ok(text.into_bytes());
205 }
206 let encoded: String = self.string(bytes_key)?;
207 return self.exact_path(text.as_str(), encoded.as_str(), bytes_key);
208 }
209
210 pub fn paths(&self, key: &str, bytes_key: &str) -> Result<Vec<Vec<u8>>, RecoveryError> {
222 let texts: Vec<String> = self.strings(key)?;
223 if !self.map.contains_key(bytes_key) {
224 return Ok(texts.into_iter().map(String::into_bytes).collect());
225 }
226 let encoded: Vec<String> = self.strings(bytes_key)?;
227 if encoded.len() != texts.len() {
228 return Err(self.malformed(bytes_key));
229 }
230 let mut paths: Vec<Vec<u8>> = Vec::with_capacity(texts.len());
231 for (text, entry) in texts.iter().zip(encoded.iter()) {
232 paths.push(self.exact_path(text.as_str(), entry.as_str(), bytes_key)?);
233 }
234 return Ok(paths);
235 }
236
237 fn exact_path(&self, text: &str, encoded: &str, bytes_key: &str) -> Result<Vec<u8>, RecoveryError> {
248 let Some(bytes) = base64_decode_standard(encoded) else {
249 return Err(self.malformed(bytes_key));
250 };
251 if EventPath::from_git_bytes(bytes.as_slice()).text() != text {
252 return Err(self.malformed(bytes_key));
253 }
254 return Ok(bytes);
255 }
256
257 pub fn object(&self, key: &str) -> Result<RecordReader<'a>, RecoveryError> {
269 match self.map.get(key) {
270 Some(Value::Object(nested)) => {
271 return Ok(RecordReader {
272 name: format!("{}#{key}", self.name),
273 map: nested,
274 });
275 }
276 Some(_) | None => return Err(self.malformed(key)),
277 }
278 }
279
280 pub fn base(&self, key: &str) -> Result<Base, RecoveryError> {
292 let nested: RecordReader<'a> = self.object(key)?;
293 let kind: String = nested.string("kind")?;
294 if kind == "unborn" {
295 return Ok(Base::Unborn);
296 }
297 if kind != "commit" {
298 return Err(self.malformed(key));
299 }
300 return Ok(Base::Commit(nested.string("oid")?));
301 }
302
303 pub fn identity(&self, key: &str) -> Result<FileIdentity, RecoveryError> {
313 let nested: RecordReader<'a> = self.object(key)?;
314 return Ok(FileIdentity {
315 device: nested.string("device")?,
316 inode: nested.string("inode")?,
317 });
318 }
319
320 pub fn optional_digest(&self, key: &str) -> Result<Option<ContentDigest>, RecoveryError> {
333 if !self.map.contains_key(key) {
334 return Ok(None);
335 }
336 let nested: RecordReader<'a> = self.object(key)?;
337 let length: u64 = match nested.map.get("length").and_then(safe_integer) {
338 Some(whole) if whole >= 0 => whole as u64,
339 Some(_) | None => return Err(self.malformed(key)),
340 };
341 let hash_text: String = nested.string("fnv1a64")?;
342 match ContentDigest::from_record(length, hash_text.as_str()) {
343 Some(digest) => return Ok(Some(digest)),
344 None => return Err(self.malformed(key)),
345 }
346 }
347
348 pub fn lock(&self, key: &str) -> Result<LockIdentity, RecoveryError> {
358 return Ok(LockIdentity {
359 file: self.identity(key)?,
360 fs_id: self.object(key)?.string("fsId")?,
361 });
362 }
363
364 pub fn added_paths(&self, key: &str) -> Result<Vec<AddedPath>, RecoveryError> {
375 let Some(Value::Array(items)) = self.map.get(key) else {
376 return Err(self.malformed(key));
377 };
378 let mut records: Vec<AddedPath> = Vec::with_capacity(items.len());
379 for item in items {
380 let Value::Object(fields) = item else {
382 return Err(self.malformed(key));
383 };
384 let nested: RecordReader<'_> = RecordReader {
385 name: format!("{}#{key}", self.name),
386 map: fields,
387 };
388 let git_mode: String = nested.string("gitMode")?;
389 if git_mode != "100644" && git_mode != "100755" {
390 return Err(self.malformed(key));
391 }
392 records.push(AddedPath {
393 path: nested.path("path", "pathBytes")?,
394 git_mode,
395 original_oid: nested.string("originalOid")?,
396 intended_oid: nested.string("intendedOid")?,
397 });
398 }
399 return Ok(records);
400 }
401}
402
403pub fn open_record(
421 bytes: &[u8],
422 name: &str,
423 state: &str,
424) -> Result<Map<String, Value>, RecoveryError> {
425 let failure: RecoveryError = RecoveryError(format!(
426 "Transaction record {name} is not a schema-version-2 {state} record."
427 ));
428 let Some(text) = decode_fatal(bytes) else {
429 return Err(failure);
430 };
431 let Some(map) = parse_object(text) else {
432 return Err(failure);
433 };
434 if !number_equals(&map, "schemaVersion", JOURNAL_SCHEMA_VERSION)
435 || string_field(&map, "state") != Some(state)
436 {
437 return Err(failure);
438 }
439 return Ok(map);
440}
441
442pub fn parse_preparing(bytes: &[u8]) -> Result<PreparingRecord, RecoveryError> {
452 let map: Map<String, Value> = open_record(bytes, PREPARING_FILENAME, "preparing")?;
453 let read: RecordReader<'_> = RecordReader::new(PREPARING_FILENAME, &map);
454 let mode_text: String = read.string("mode")?;
455 let conclusion_text: String = read.string("conclusion")?;
456 let format_text: String = read.string("refFormat")?;
457 let (Some(mode), Some(conclusion), Some(ref_format)) = (
458 parse_mode(mode_text.as_str()),
459 parse_conclusion(conclusion_text.as_str()),
460 parse_ref_format(format_text.as_str()),
461 ) else {
462 return Err(RecoveryError(String::from(
463 "Transaction record preparing.json has a malformed mode, conclusion, or refFormat field.",
464 )));
465 };
466 let transaction_id: String = read.string("transactionId")?;
467 let base: Base = read.base("base")?;
468 let head: RecordReader<'_> = read.object("symbolicHead")?;
469 let head_kind: String = head.string("kind")?;
470 let symbolic_head: SymbolicHead = if head_kind == "detached" {
471 SymbolicHead::Detached
472 } else if head_kind == "branch" {
473 SymbolicHead::Branch(head.string("ref")?)
474 } else {
475 return Err(RecoveryError(String::from(
476 "Transaction record preparing.json has a malformed symbolicHead field.",
477 )));
478 };
479 return Ok(PreparingRecord {
480 transaction_id,
481 mode,
482 base,
483 symbolic_head,
484 target_ref: read.string("targetRef")?,
485 conclusion,
486 repository_root: read.string("repositoryRoot")?,
487 git_dir: read.string("gitDir")?,
488 common_dir: read.string("commonDir")?,
489 real_index_path: read.string("realIndexPath")?,
490 object_directory: read.string("objectDirectory")?,
491 ref_format,
492 empty_tree_oid: read.string("emptyTreeOid")?,
493 shadow_path: read.string("shadowPath")?,
494 selected_pathspecs: read.paths("selectedPathspecs", "selectedPathspecBytes")?,
495 invoked_at: read.string("invokedAt")?,
496 });
497}
498
499fn parse_mode(text: &str) -> Option<TransactionMode> {
509 match text {
510 "explicit-path" => return Some(TransactionMode::ExplicitPath),
511 "index" => return Some(TransactionMode::Index),
512 _ => return None,
513 }
514}
515
516fn parse_conclusion(text: &str) -> Option<Conclusion> {
526 match text {
527 "none" => return Some(Conclusion::None),
528 "amend" => return Some(Conclusion::Amend),
529 "merge" => return Some(Conclusion::Merge),
530 "cherry-pick" => return Some(Conclusion::CherryPick),
531 "revert" => return Some(Conclusion::Revert),
532 _ => return None,
533 }
534}
535
536fn parse_ref_format(text: &str) -> Option<RefFormat> {
546 match text {
547 "files" => return Some(RefFormat::Files),
548 "reftable" => return Some(RefFormat::Reftable),
549 _ => return None,
550 }
551}
552
553pub fn parse_prepared(bytes: &[u8]) -> Result<PreparedRecord, RecoveryError> {
563 let map: Map<String, Value> = open_record(bytes, PREPARED_FILENAME, "prepared")?;
564 let read: RecordReader<'_> = RecordReader::new(PREPARED_FILENAME, &map);
565 return Ok(PreparedRecord {
566 shadow_path: read.string("shadowPath")?,
567 prepared_oid: read.string("preparedOid")?,
568 signed: read.boolean("signed")?,
569 intended_tree_oid: read.string("intendedTreeOid")?,
570 committed_paths: read.paths("committedPaths", "committedPathBytes")?,
571 added_paths: read.added_paths("addedPaths")?,
572 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
573 });
574}
575
576pub fn parse_index_lock(bytes: &[u8]) -> Result<IndexLockRecord, RecoveryError> {
586 let name: &str = "index-lock record";
587 let map: Map<String, Value> = open_record(bytes, name, "index-locked")?;
588 let read: RecordReader<'_> = RecordReader::new(name, &map);
589 return Ok(IndexLockRecord {
590 attempt: read.positive_integer("attempt")?,
591 lock: read.lock("lock")?,
592 });
593}
594
595pub fn parse_landing(bytes: &[u8]) -> Result<LandingRecord, RecoveryError> {
606 let name: &str = "landing record";
607 let map: Map<String, Value> = open_record(bytes, name, "landing")?;
608 let read: RecordReader<'_> = RecordReader::new(name, &map);
609 let operation_text: String = read.string("operation")?;
610 let new_oid: Option<String> = read.optional_string("newOid")?;
611 let pack_name: Option<String> = read.optional_string("packName")?;
612 let operation: LandingOperation = match operation_text.as_str() {
613 "commit" if new_oid.is_some() => LandingOperation::Commit,
614 "normalize-only" => LandingOperation::NormalizeOnly,
615 _ => {
616 return Err(RecoveryError(String::from(
617 "Transaction record landing record has a malformed operation or newOid field.",
618 )));
619 }
620 };
621 return Ok(LandingRecord {
622 attempt: read.positive_integer("attempt")?,
623 operation,
624 expected_old: read.base("expectedOld")?,
625 new_oid,
626 landed_tree_oid: read.string("landedTreeOid")?,
627 pre_landing_index: read.identity("preLandingIndex")?,
628 post_index: read.identity("postIndex")?,
629 lock: read.lock("lock")?,
630 pack_name,
631 added_paths: read.added_paths("addedPaths")?,
632 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
633 pre_landing_digest: read.optional_digest("preLandingDigest")?,
634 post_digest: read.optional_digest("postDigest")?,
635 });
636}
637
638pub fn parse_ref_updated(bytes: &[u8]) -> Result<String, RecoveryError> {
648 let map: Map<String, Value> = open_record(bytes, REF_UPDATED_FILENAME, "ref-updated")?;
649 return RecordReader::new(REF_UPDATED_FILENAME, &map).string("landedOid");
650}
651
652#[cfg(test)]
654#[path = "transaction_journal_parse_tests.rs"]
655mod tests;