1use super::json_record::{decode_fatal, number_equals, parse_object, safe_integer, string_field};
17use super::recovery_error::RecoveryError;
19use super::transaction_journal::{
21 AddedPath, Base, Conclusion, FileIdentity, IndexLockRecord, JOURNAL_SCHEMA_VERSION,
22 LandingOperation, LandingRecord, LockIdentity, PREPARED_FILENAME, PREPARING_FILENAME,
23 PreparedRecord, PreparingRecord, REF_UPDATED_FILENAME, RefFormat, SymbolicHead,
24 TransactionMode,
25};
26use serde_json::{Map, Value};
37
38pub struct RecordReader<'a> {
51 name: String,
54 map: &'a Map<String, Value>,
56}
57
58impl<'a> RecordReader<'a> {
61 pub fn new(name: &str, map: &'a Map<String, Value>) -> Self {
71 return Self {
72 name: String::from(name),
73 map,
74 };
75 }
76
77 pub fn malformed(&self, key: &str) -> RecoveryError {
87 return RecoveryError(format!(
88 "Transaction record {} has a malformed {key} field.",
89 self.name
90 ));
91 }
92
93 pub fn string(&self, key: &str) -> Result<String, RecoveryError> {
106 match string_field(self.map, key) {
107 Some(text) => return Ok(String::from(text)),
108 None => return Err(self.malformed(key)),
109 }
110 }
111
112 pub fn optional_string(&self, key: &str) -> Result<Option<String>, RecoveryError> {
124 if !self.map.contains_key(key) {
125 return Ok(None);
126 }
127 return Ok(Some(self.string(key)?));
128 }
129
130 pub fn boolean(&self, key: &str) -> Result<bool, RecoveryError> {
140 match self.map.get(key) {
141 Some(Value::Bool(flag)) => return Ok(*flag),
142 Some(_) | None => return Err(self.malformed(key)),
143 }
144 }
145
146 pub fn positive_integer(&self, key: &str) -> Result<i64, RecoveryError> {
156 match self.map.get(key).and_then(safe_integer) {
157 Some(whole) if whole >= 1 => return Ok(whole),
158 Some(_) | None => return Err(self.malformed(key)),
159 }
160 }
161
162 pub fn strings(&self, key: &str) -> Result<Vec<String>, RecoveryError> {
172 let Some(Value::Array(items)) = self.map.get(key) else {
173 return Err(self.malformed(key));
174 };
175 let mut texts: Vec<String> = Vec::with_capacity(items.len());
176 for item in items {
177 let Value::String(text) = item else {
178 return Err(self.malformed(key));
179 };
180 texts.push(text.clone());
181 }
182 return Ok(texts);
183 }
184
185 pub fn object(&self, key: &str) -> Result<RecordReader<'a>, RecoveryError> {
197 match self.map.get(key) {
198 Some(Value::Object(nested)) => {
199 return Ok(RecordReader {
200 name: format!("{}#{key}", self.name),
201 map: nested,
202 });
203 }
204 Some(_) | None => return Err(self.malformed(key)),
205 }
206 }
207
208 pub fn base(&self, key: &str) -> Result<Base, RecoveryError> {
220 let nested: RecordReader<'a> = self.object(key)?;
221 let kind: String = nested.string("kind")?;
222 if kind == "unborn" {
223 return Ok(Base::Unborn);
224 }
225 if kind != "commit" {
226 return Err(self.malformed(key));
227 }
228 return Ok(Base::Commit(nested.string("oid")?));
229 }
230
231 pub fn identity(&self, key: &str) -> Result<FileIdentity, RecoveryError> {
241 let nested: RecordReader<'a> = self.object(key)?;
242 return Ok(FileIdentity {
243 device: nested.string("device")?,
244 inode: nested.string("inode")?,
245 });
246 }
247
248 pub fn lock(&self, key: &str) -> Result<LockIdentity, RecoveryError> {
258 return Ok(LockIdentity {
259 file: self.identity(key)?,
260 fs_id: self.object(key)?.string("fsId")?,
261 });
262 }
263
264 pub fn added_paths(&self, key: &str) -> Result<Vec<AddedPath>, RecoveryError> {
275 let Some(Value::Array(items)) = self.map.get(key) else {
276 return Err(self.malformed(key));
277 };
278 let mut records: Vec<AddedPath> = Vec::with_capacity(items.len());
279 for item in items {
280 let Value::Object(fields) = item else {
282 return Err(self.malformed(key));
283 };
284 let nested: RecordReader<'_> = RecordReader {
285 name: format!("{}#{key}", self.name),
286 map: fields,
287 };
288 let git_mode: String = nested.string("gitMode")?;
289 if git_mode != "100644" && git_mode != "100755" {
290 return Err(self.malformed(key));
291 }
292 records.push(AddedPath {
293 path: nested.string("path")?,
294 git_mode,
295 original_oid: nested.string("originalOid")?,
296 intended_oid: nested.string("intendedOid")?,
297 });
298 }
299 return Ok(records);
300 }
301}
302
303pub fn open_record(
321 bytes: &[u8],
322 name: &str,
323 state: &str,
324) -> Result<Map<String, Value>, RecoveryError> {
325 let failure: RecoveryError = RecoveryError(format!(
326 "Transaction record {name} is not a schema-version-2 {state} record."
327 ));
328 let Some(text) = decode_fatal(bytes) else {
329 return Err(failure);
330 };
331 let Some(map) = parse_object(text) else {
332 return Err(failure);
333 };
334 if !number_equals(&map, "schemaVersion", JOURNAL_SCHEMA_VERSION)
335 || string_field(&map, "state") != Some(state)
336 {
337 return Err(failure);
338 }
339 return Ok(map);
340}
341
342pub fn parse_preparing(bytes: &[u8]) -> Result<PreparingRecord, RecoveryError> {
352 let map: Map<String, Value> = open_record(bytes, PREPARING_FILENAME, "preparing")?;
353 let read: RecordReader<'_> = RecordReader::new(PREPARING_FILENAME, &map);
354 let mode_text: String = read.string("mode")?;
355 let conclusion_text: String = read.string("conclusion")?;
356 let format_text: String = read.string("refFormat")?;
357 let (Some(mode), Some(conclusion), Some(ref_format)) = (
358 parse_mode(mode_text.as_str()),
359 parse_conclusion(conclusion_text.as_str()),
360 parse_ref_format(format_text.as_str()),
361 ) else {
362 return Err(RecoveryError(String::from(
363 "Transaction record preparing.json has a malformed mode, conclusion, or refFormat field.",
364 )));
365 };
366 let transaction_id: String = read.string("transactionId")?;
367 let base: Base = read.base("base")?;
368 let head: RecordReader<'_> = read.object("symbolicHead")?;
369 let head_kind: String = head.string("kind")?;
370 let symbolic_head: SymbolicHead = if head_kind == "detached" {
371 SymbolicHead::Detached
372 } else if head_kind == "branch" {
373 SymbolicHead::Branch(head.string("ref")?)
374 } else {
375 return Err(RecoveryError(String::from(
376 "Transaction record preparing.json has a malformed symbolicHead field.",
377 )));
378 };
379 return Ok(PreparingRecord {
380 transaction_id,
381 mode,
382 base,
383 symbolic_head,
384 target_ref: read.string("targetRef")?,
385 conclusion,
386 repository_root: read.string("repositoryRoot")?,
387 git_dir: read.string("gitDir")?,
388 common_dir: read.string("commonDir")?,
389 real_index_path: read.string("realIndexPath")?,
390 object_directory: read.string("objectDirectory")?,
391 ref_format,
392 empty_tree_oid: read.string("emptyTreeOid")?,
393 shadow_path: read.string("shadowPath")?,
394 selected_pathspecs: read.strings("selectedPathspecs")?,
395 invoked_at: read.string("invokedAt")?,
396 });
397}
398
399fn parse_mode(text: &str) -> Option<TransactionMode> {
409 match text {
410 "explicit-path" => return Some(TransactionMode::ExplicitPath),
411 "index" => return Some(TransactionMode::Index),
412 _ => return None,
413 }
414}
415
416fn parse_conclusion(text: &str) -> Option<Conclusion> {
426 match text {
427 "none" => return Some(Conclusion::None),
428 "amend" => return Some(Conclusion::Amend),
429 "merge" => return Some(Conclusion::Merge),
430 "cherry-pick" => return Some(Conclusion::CherryPick),
431 "revert" => return Some(Conclusion::Revert),
432 _ => return None,
433 }
434}
435
436fn parse_ref_format(text: &str) -> Option<RefFormat> {
446 match text {
447 "files" => return Some(RefFormat::Files),
448 "reftable" => return Some(RefFormat::Reftable),
449 _ => return None,
450 }
451}
452
453pub fn parse_prepared(bytes: &[u8]) -> Result<PreparedRecord, RecoveryError> {
463 let map: Map<String, Value> = open_record(bytes, PREPARED_FILENAME, "prepared")?;
464 let read: RecordReader<'_> = RecordReader::new(PREPARED_FILENAME, &map);
465 return Ok(PreparedRecord {
466 shadow_path: read.string("shadowPath")?,
467 prepared_oid: read.string("preparedOid")?,
468 signed: read.boolean("signed")?,
469 intended_tree_oid: read.string("intendedTreeOid")?,
470 committed_paths: read.strings("committedPaths")?,
471 added_paths: read.added_paths("addedPaths")?,
472 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
473 });
474}
475
476pub fn parse_index_lock(bytes: &[u8]) -> Result<IndexLockRecord, RecoveryError> {
486 let name: &str = "index-lock record";
487 let map: Map<String, Value> = open_record(bytes, name, "index-locked")?;
488 let read: RecordReader<'_> = RecordReader::new(name, &map);
489 return Ok(IndexLockRecord {
490 attempt: read.positive_integer("attempt")?,
491 lock: read.lock("lock")?,
492 });
493}
494
495pub fn parse_landing(bytes: &[u8]) -> Result<LandingRecord, RecoveryError> {
506 let name: &str = "landing record";
507 let map: Map<String, Value> = open_record(bytes, name, "landing")?;
508 let read: RecordReader<'_> = RecordReader::new(name, &map);
509 let operation_text: String = read.string("operation")?;
510 let new_oid: Option<String> = read.optional_string("newOid")?;
511 let pack_name: Option<String> = read.optional_string("packName")?;
512 let operation: LandingOperation = match operation_text.as_str() {
513 "commit" if new_oid.is_some() => LandingOperation::Commit,
514 "normalize-only" => LandingOperation::NormalizeOnly,
515 _ => {
516 return Err(RecoveryError(String::from(
517 "Transaction record landing record has a malformed operation or newOid field.",
518 )));
519 }
520 };
521 return Ok(LandingRecord {
522 attempt: read.positive_integer("attempt")?,
523 operation,
524 expected_old: read.base("expectedOld")?,
525 new_oid,
526 landed_tree_oid: read.string("landedTreeOid")?,
527 pre_landing_index: read.identity("preLandingIndex")?,
528 post_index: read.identity("postIndex")?,
529 lock: read.lock("lock")?,
530 pack_name,
531 added_paths: read.added_paths("addedPaths")?,
532 selected_worktree_paths: read.added_paths("selectedWorktreePaths")?,
533 });
534}
535
536pub fn parse_ref_updated(bytes: &[u8]) -> Result<String, RecoveryError> {
546 let map: Map<String, Value> = open_record(bytes, REF_UPDATED_FILENAME, "ref-updated")?;
547 return RecordReader::new(REF_UPDATED_FILENAME, &map).string("landedOid");
548}
549
550#[cfg(test)]
552#[path = "transaction_journal_parse_tests.rs"]
553mod tests;