1use std::collections::{BTreeMap, BTreeSet};
29use std::fs;
30use std::io::{Read, Write};
31use std::path::{Path, PathBuf};
32use std::thread;
33use std::time::{Duration, Instant};
34
35use crate::config::ConfigSet;
36use crate::error::{Error, Result};
37use crate::objects::ObjectId;
38
39const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
45
46const HEADER_SIZE: usize = 24;
49
50const FOOTER_V1_SIZE: usize = 68;
52
53const BLOCK_TYPE_REF: u8 = b'r';
55const BLOCK_TYPE_INDEX: u8 = b'i';
57const BLOCK_TYPE_LOG: u8 = b'g';
59const BLOCK_TYPE_OBJ: u8 = b'o';
61
62const VALUE_DELETION: u8 = 0;
64const VALUE_ONE_OID: u8 = 1;
65const VALUE_TWO_OID: u8 = 2;
66const VALUE_SYMREF: u8 = 3;
67
68const HASH_SIZE: usize = 20;
70
71const DEFAULT_BLOCK_SIZE: u32 = 4096;
73
74const RESTART_INTERVAL: usize = 16;
76
77fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
83 let mut buf = [0u8; 10];
85 let mut i = 0;
86 buf[i] = (val & 0x7f) as u8;
87 i += 1;
88 val >>= 7;
89 while val > 0 {
90 val -= 1;
91 buf[i] = (val & 0x7f) as u8;
92 i += 1;
93 val >>= 7;
94 }
95 let len = i;
97 for j in (1..len).rev() {
98 out.push(buf[j] | 0x80);
99 }
100 out.push(buf[0]);
101 len
102}
103
104fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
106 if pos >= data.len() {
107 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
108 }
109 let mut val = (data[pos] & 0x7f) as u64;
110 while data[pos] & 0x80 != 0 {
111 pos += 1;
112 if pos >= data.len() {
113 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
114 }
115 val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
116 }
117 Ok((val, pos + 1))
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum RefValue {
127 Deletion,
129 Val1(ObjectId),
131 Val2(ObjectId, ObjectId),
133 Symref(String),
135}
136
137#[derive(Debug, Clone)]
139pub struct RefRecord {
140 pub name: String,
142 pub update_index: u64,
144 pub value: RefValue,
146}
147
148#[derive(Debug, Clone)]
150pub struct LogRecord {
151 pub refname: String,
153 pub update_index: u64,
155 pub old_id: ObjectId,
157 pub new_id: ObjectId,
159 pub name: String,
161 pub email: String,
163 pub time_seconds: u64,
165 pub tz_offset: i16,
167 pub message: String,
169}
170
171#[derive(Debug, Clone)]
173pub struct WriteOptions {
174 pub block_size: u32,
176 pub restart_interval: usize,
178 pub write_log: bool,
180 pub skip_index_objects: bool,
182 pub unpadded: bool,
184 pub hash_size: usize,
187}
188
189impl Default for WriteOptions {
190 fn default() -> Self {
191 Self {
192 block_size: DEFAULT_BLOCK_SIZE,
193 restart_interval: RESTART_INTERVAL,
194 write_log: true,
195 skip_index_objects: false,
196 unpadded: false,
197 hash_size: HASH_SIZE,
198 }
199 }
200}
201
202#[derive(Debug, Clone)]
209pub struct ReftableTransactionUpdate {
210 pub refname: String,
212 pub value: RefValue,
214 pub log: Option<LogRecord>,
216}
217
218pub struct ReftableWriter {
232 opts: WriteOptions,
233 min_update_index: u64,
234 max_update_index: u64,
235
236 refs: Vec<RefRecord>,
238 logs: Vec<LogRecord>,
240}
241
242impl ReftableWriter {
243 pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
245 Self {
246 opts,
247 min_update_index,
248 max_update_index,
249 refs: Vec::new(),
250 logs: Vec::new(),
251 }
252 }
253
254 pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
256 if let Some(last) = self.refs.last() {
257 if rec.name <= last.name {
258 return Err(Error::InvalidRef(format!(
259 "reftable: refs must be sorted, got '{}' after '{}'",
260 rec.name, last.name
261 )));
262 }
263 }
264 self.refs.push(rec);
265 Ok(())
266 }
267
268 pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
270 self.logs.push(rec);
271 Ok(())
272 }
273
274 pub fn finish(self) -> Result<Vec<u8>> {
280 let refs = self.refs;
281 let logs = self.logs;
282 let opts = self.opts;
283 let mut w = WriterState::new(opts, self.min_update_index, self.max_update_index);
284
285 for rec in &refs {
287 w.add_ref(rec)?;
288 }
289
290 let mut logs = logs;
293 logs.sort_by(|a, b| {
294 a.refname
295 .cmp(&b.refname)
296 .then_with(|| b.update_index.cmp(&a.update_index))
297 });
298 if w.opts.write_log {
299 for log in &logs {
300 w.add_log(log)?;
301 }
302 }
303
304 w.close()
305 }
306}
307
308const REFTABLE_DEFAULT_BLOCK_SIZE: u32 = 4096;
314const MAX_RESTARTS: usize = (1 << 16) - 1;
316
317enum EncRecord<'a> {
319 Ref(&'a RefRecord, u64),
320 Log(&'a LogRecord),
321 Obj { prefix: Vec<u8>, offsets: Vec<u64> },
322 Index { last_key: Vec<u8>, offset: u64 },
323}
324
325impl EncRecord<'_> {
326 fn block_type(&self) -> u8 {
327 match self {
328 EncRecord::Ref(..) => BLOCK_TYPE_REF,
329 EncRecord::Log(_) => BLOCK_TYPE_LOG,
330 EncRecord::Obj { .. } => BLOCK_TYPE_OBJ,
331 EncRecord::Index { .. } => BLOCK_TYPE_INDEX,
332 }
333 }
334
335 fn key(&self) -> Vec<u8> {
337 match self {
338 EncRecord::Ref(r, _) => r.name.as_bytes().to_vec(),
339 EncRecord::Log(l) => {
340 let mut k = Vec::with_capacity(l.refname.len() + 9);
341 k.extend_from_slice(l.refname.as_bytes());
342 k.push(0);
343 let ts = u64::MAX - l.update_index;
344 k.extend_from_slice(&ts.to_be_bytes());
345 k
346 }
347 EncRecord::Obj { prefix, .. } => prefix.clone(),
348 EncRecord::Index { last_key, .. } => last_key.clone(),
349 }
350 }
351
352 fn val_type(&self) -> u8 {
354 match self {
355 EncRecord::Ref(r, _) => match r.value {
356 RefValue::Deletion => VALUE_DELETION,
357 RefValue::Val1(_) => VALUE_ONE_OID,
358 RefValue::Val2(..) => VALUE_TWO_OID,
359 RefValue::Symref(_) => VALUE_SYMREF,
360 },
361 EncRecord::Log(_) => 1,
364 EncRecord::Obj { offsets, .. } => {
365 if !offsets.is_empty() && offsets.len() < 8 {
366 offsets.len() as u8
367 } else {
368 0
369 }
370 }
371 EncRecord::Index { .. } => 0,
372 }
373 }
374
375 fn encode_value(&self, opts: &WriteOptions, out: &mut Vec<u8>) {
377 match self {
378 EncRecord::Ref(r, update_index_delta) => {
379 put_varint(*update_index_delta, out);
380 match &r.value {
381 RefValue::Deletion => {}
382 RefValue::Val1(oid) => out.extend_from_slice(oid.as_bytes()),
383 RefValue::Val2(oid, peeled) => {
384 out.extend_from_slice(oid.as_bytes());
385 out.extend_from_slice(peeled.as_bytes());
386 }
387 RefValue::Symref(target) => {
388 put_varint(target.len() as u64, out);
389 out.extend_from_slice(target.as_bytes());
390 }
391 }
392 }
393 EncRecord::Log(l) => {
394 out.extend_from_slice(l.old_id.as_bytes());
395 out.extend_from_slice(l.new_id.as_bytes());
396 put_varint(l.name.len() as u64, out);
397 out.extend_from_slice(l.name.as_bytes());
398 put_varint(l.email.len() as u64, out);
399 out.extend_from_slice(l.email.as_bytes());
400 put_varint(l.time_seconds, out);
401 out.extend_from_slice(&l.tz_offset.to_be_bytes());
402 let msg = clean_log_message(&l.message, opts);
403 put_varint(msg.len() as u64, out);
404 out.extend_from_slice(&msg);
405 }
406 EncRecord::Obj { offsets, .. } => {
407 if offsets.is_empty() || offsets.len() >= 8 {
408 put_varint(offsets.len() as u64, out);
409 }
410 if offsets.is_empty() {
411 return;
412 }
413 put_varint(offsets[0], out);
414 let mut last = offsets[0];
415 for &o in &offsets[1..] {
416 put_varint(o - last, out);
417 last = o;
418 }
419 }
420 EncRecord::Index { offset, .. } => {
421 put_varint(*offset, out);
422 }
423 }
424 }
425}
426
427fn clean_log_message(msg: &str, opts: &WriteOptions) -> Vec<u8> {
438 let limit = (opts.block_size as usize / 2).max(1);
445 let msg = if msg.len() > limit {
446 let mut end = limit;
447 while end > 0 && !msg.is_char_boundary(end) {
448 end -= 1;
449 }
450 &msg[..end]
451 } else {
452 msg
453 };
454 let trimmed = msg.trim_end_matches('\n');
455 let mut out = trimmed.as_bytes().to_vec();
456 out.push(b'\n');
457 out
458}
459
460fn encode_key(prev: &[u8], key: &[u8], extra: u8, out: &mut Vec<u8>) -> bool {
463 let prefix_len = common_prefix_len(prev, key);
464 let suffix_len = key.len() - prefix_len;
465 put_varint(prefix_len as u64, out);
466 put_varint(((suffix_len as u64) << 3) | (extra as u64), out);
467 out.extend_from_slice(&key[prefix_len..]);
468 prefix_len == 0
469}
470
471struct BlockWriter {
473 typ: u8,
474 buf: Vec<u8>,
477 header_off: usize,
478 block_size: usize,
479 restart_interval: usize,
480 restarts: Vec<u32>,
481 last_key: Vec<u8>,
482 entries: usize,
483}
484
485impl BlockWriter {
486 fn new(typ: u8, block_size: usize, header_off: usize, restart_interval: usize) -> Self {
487 let mut buf = vec![0u8; header_off + 4];
489 buf[header_off] = typ;
490 Self {
491 typ,
492 buf,
493 header_off,
494 block_size,
495 restart_interval,
496 restarts: Vec::new(),
497 last_key: Vec::new(),
498 entries: 0,
499 }
500 }
501
502 fn next(&self) -> usize {
505 self.buf.len()
506 }
507
508 fn add(&mut self, rec: &EncRecord, opts: &WriteOptions) -> Result<bool> {
511 let key = rec.key();
512 if key.is_empty() {
513 return Err(Error::InvalidRef("reftable: empty record key".into()));
514 }
515 let restart = self.entries.is_multiple_of(self.restart_interval);
516 let prev: &[u8] = if restart { &[] } else { &self.last_key };
517
518 let mut encoded = Vec::new();
519 let is_restart = encode_key(prev, &key, rec.val_type(), &mut encoded);
520 rec.encode_value(opts, &mut encoded);
521 let n = encoded.len();
522
523 let mut rlen = self.restarts.len();
525 let mut is_restart = is_restart;
526 if rlen >= MAX_RESTARTS {
527 is_restart = false;
528 }
529 if is_restart {
530 rlen += 1;
531 }
532 if self.block_size > 0 && 2 + 3 * rlen + n > self.block_size - self.next() {
533 return Ok(false);
534 }
535
536 if is_restart {
537 self.restarts.push(self.next() as u32);
538 }
539 self.buf.extend_from_slice(&encoded);
540 self.last_key = key;
541 self.entries += 1;
542 Ok(true)
543 }
544
545 fn finish(&mut self) -> Result<usize> {
549 for &r in &self.restarts {
550 self.buf.push(((r >> 16) & 0xff) as u8);
551 self.buf.push(((r >> 8) & 0xff) as u8);
552 self.buf.push((r & 0xff) as u8);
553 }
554 let rc = self.restarts.len() as u16;
555 self.buf.push((rc >> 8) as u8);
556 self.buf.push((rc & 0xff) as u8);
557
558 let block_len = self.buf.len();
560 self.buf[self.header_off + 1] = ((block_len >> 16) & 0xff) as u8;
561 self.buf[self.header_off + 2] = ((block_len >> 8) & 0xff) as u8;
562 self.buf[self.header_off + 3] = (block_len & 0xff) as u8;
563
564 if self.typ == BLOCK_TYPE_LOG {
565 use flate2::write::DeflateEncoder;
566 use flate2::Compression;
567 let skip = 4 + self.header_off;
568 let mut enc = DeflateEncoder::new(Vec::new(), Compression::new(9));
569 enc.write_all(&self.buf[skip..])
570 .map_err(|e| Error::Zlib(e.to_string()))?;
571 let compressed = enc.finish().map_err(|e| Error::Zlib(e.to_string()))?;
572 self.buf.truncate(skip);
573 self.buf.extend_from_slice(&compressed);
574 }
575 Ok(self.buf.len())
576 }
577}
578
579#[derive(Default, Clone)]
581struct SectionStats {
582 blocks: usize,
583 index_blocks: usize,
584 offset: u64,
585 index_offset: u64,
586}
587
588struct ObjEntry {
590 hash: Vec<u8>,
591 offsets: Vec<u64>,
592}
593
594struct WriterState {
596 opts: WriteOptions,
597 min_update_index: u64,
598 max_update_index: u64,
599
600 out: Vec<u8>,
601 next: u64,
602 pending_padding: usize,
603
604 block: Option<BlockWriter>,
605 block_type: u8,
606
607 index: Vec<(Vec<u8>, u64)>,
609
610 obj_entries: Vec<ObjEntry>,
612 object_id_len: usize,
613
614 ref_stats: SectionStats,
615 obj_stats: SectionStats,
616 log_stats: SectionStats,
617 idx_blocks_total: usize,
618}
619
620impl WriterState {
621 fn new(mut opts: WriteOptions, min: u64, max: u64) -> Self {
622 if opts.restart_interval == 0 {
623 opts.restart_interval = RESTART_INTERVAL;
624 }
625 if opts.block_size == 0 {
626 opts.block_size = REFTABLE_DEFAULT_BLOCK_SIZE;
627 }
628 Self {
629 opts,
630 min_update_index: min,
631 max_update_index: max,
632 out: Vec::new(),
633 next: 0,
634 pending_padding: 0,
635 block: None,
636 block_type: 0,
637 index: Vec::new(),
638 obj_entries: Vec::new(),
639 object_id_len: 0,
640 ref_stats: SectionStats::default(),
641 obj_stats: SectionStats::default(),
642 log_stats: SectionStats::default(),
643 idx_blocks_total: 0,
644 }
645 }
646
647 fn header_size(&self) -> usize {
648 if self.opts.hash_size == 32 {
651 28
652 } else {
653 24
654 }
655 }
656
657 fn write_header(&self, dest: &mut [u8]) {
658 dest[0..4].copy_from_slice(REFTABLE_MAGIC);
659 dest[4] = if self.opts.hash_size == 32 { 2 } else { 1 };
660 dest[5] = ((self.opts.block_size >> 16) & 0xff) as u8;
661 dest[6] = ((self.opts.block_size >> 8) & 0xff) as u8;
662 dest[7] = (self.opts.block_size & 0xff) as u8;
663 dest[8..16].copy_from_slice(&self.min_update_index.to_be_bytes());
664 dest[16..24].copy_from_slice(&self.max_update_index.to_be_bytes());
665 if self.opts.hash_size == 32 {
668 dest[24..28].copy_from_slice(b"s256");
669 }
670 }
671
672 fn stats_mut(&mut self, typ: u8) -> &mut SectionStats {
673 match typ {
674 BLOCK_TYPE_REF => &mut self.ref_stats,
675 BLOCK_TYPE_OBJ => &mut self.obj_stats,
676 BLOCK_TYPE_LOG => &mut self.log_stats,
677 _ => &mut self.ref_stats,
679 }
680 }
681
682 fn padded_write(&mut self, data: &[u8], padding: usize) {
685 if self.pending_padding > 0 {
686 self.out
687 .extend(std::iter::repeat_n(0u8, self.pending_padding));
688 self.pending_padding = 0;
689 }
690 self.pending_padding = padding;
691 self.out.extend_from_slice(data);
692 }
693
694 fn reinit_block(&mut self, typ: u8) {
695 let header_off = if self.next == 0 {
696 self.header_size()
697 } else {
698 0
699 };
700 self.block = Some(BlockWriter::new(
701 typ,
702 self.opts.block_size as usize,
703 header_off,
704 self.opts.restart_interval,
705 ));
706 self.block_type = typ;
707 }
708
709 fn add_record(&mut self, rec: &EncRecord) -> Result<()> {
710 let typ = rec.block_type();
711 if self.block.is_none() {
712 self.reinit_block(typ);
713 }
714 let opts = self.opts.clone();
716 let fit = {
717 let bw = self
718 .block
719 .as_mut()
720 .ok_or_else(|| Error::InvalidRef("reftable: no active block writer".into()))?;
721 bw.add(rec, &opts)?
722 };
723 if fit {
724 return Ok(());
725 }
726 self.flush_block()?;
728 self.reinit_block(typ);
729 let opts = self.opts.clone();
730 let bw = self
731 .block
732 .as_mut()
733 .ok_or_else(|| Error::InvalidRef("reftable: no active block writer".into()))?;
734 if !bw.add(rec, &opts)? {
735 return Err(Error::InvalidRef(
736 "reftable: transaction failure: entry too large".into(),
737 ));
738 }
739 Ok(())
740 }
741
742 fn add_ref(&mut self, r: &RefRecord) -> Result<()> {
743 let delta = r.update_index.saturating_sub(self.min_update_index);
744 self.add_record(&EncRecord::Ref(r, delta))?;
745
746 if !self.opts.skip_index_objects {
747 match &r.value {
748 RefValue::Val1(oid) => self.index_hash(oid.as_bytes()),
749 RefValue::Val2(oid, peeled) => {
750 self.index_hash(oid.as_bytes());
751 self.index_hash(peeled.as_bytes());
752 }
753 _ => {}
754 }
755 }
756 Ok(())
757 }
758
759 fn add_log(&mut self, l: &LogRecord) -> Result<()> {
760 if matches!(&self.block, Some(b) if b.typ == BLOCK_TYPE_REF) {
762 self.finish_public_section()?;
763 }
764 self.next -= self.pending_padding as u64;
766 self.pending_padding = 0;
767 self.add_record(&EncRecord::Log(l))
768 }
769
770 fn index_hash(&mut self, hash: &[u8]) {
771 let off = self.next;
772 match self
773 .obj_entries
774 .binary_search_by(|e| e.hash.as_slice().cmp(hash))
775 {
776 Ok(idx) => {
777 let e = &mut self.obj_entries[idx];
778 if e.offsets.last() != Some(&off) {
779 e.offsets.push(off);
780 }
781 }
782 Err(idx) => {
783 self.obj_entries.insert(
784 idx,
785 ObjEntry {
786 hash: hash.to_vec(),
787 offsets: vec![off],
788 },
789 );
790 }
791 }
792 }
793
794 fn flush_block(&mut self) -> Result<()> {
796 let Some(mut bw) = self.block.take() else {
797 return Ok(());
798 };
799 if bw.entries == 0 {
800 self.block = Some(bw);
801 return Ok(());
802 }
803 let typ = bw.typ;
804 let raw_bytes = bw.finish()?;
805
806 let mut padding = 0;
807 if !self.opts.unpadded && typ != BLOCK_TYPE_LOG {
808 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
809 }
810
811 let block_typ_off = if self.stats_mut(typ).blocks == 0 {
812 self.next
813 } else {
814 0
815 };
816 {
817 let next = self.next;
818 let st = self.stats_mut(typ);
819 if block_typ_off > 0 {
820 st.offset = next;
821 }
822 st.blocks += 1;
823 }
824
825 if self.next == 0 {
826 let hs = self.header_size();
828 self.write_header_into_block(&mut bw, hs);
829 }
830
831 let data = bw.buf.clone();
832 self.padded_write(&data, padding);
833
834 self.index.push((bw.last_key.clone(), self.next));
836
837 self.next += (padding + raw_bytes) as u64;
838 self.block = None;
839 Ok(())
840 }
841
842 fn write_header_into_block(&self, bw: &mut BlockWriter, hs: usize) {
843 let mut hdr = vec![0u8; hs];
844 self.write_header(&mut hdr);
845 bw.buf[..hs].copy_from_slice(&hdr);
846 }
847
848 fn flush_block_if_nonempty(&mut self) -> Result<()> {
849 if matches!(&self.block, Some(b) if b.entries == 0) {
850 return Ok(());
851 }
852 self.flush_block()
853 }
854
855 fn finish_section(&mut self) -> Result<()> {
857 let typ = self.block_type;
858 let threshold = if self.opts.unpadded { 1 } else { 3 };
859 let before_blocks = self.idx_blocks_total;
860
861 self.flush_block_if_nonempty()?;
862
863 let mut max_level = 0;
864 let mut index_start = 0u64;
865
866 while self.index.len() > threshold {
867 max_level += 1;
868 index_start = self.next;
869 self.reinit_block(BLOCK_TYPE_INDEX);
870
871 let idx = std::mem::take(&mut self.index);
872 for (last_key, offset) in &idx {
873 self.add_record(&EncRecord::Index {
874 last_key: last_key.clone(),
875 offset: *offset,
876 })?;
877 }
878 let blocks_before = self.count_index_blocks_marker();
880 self.flush_index_block()?;
881 let _ = blocks_before;
882 }
883
884 self.index.clear();
885
886 let index_blocks = self.idx_blocks_total - before_blocks;
887 {
888 let st = self.stats_mut(typ);
889 st.index_blocks = index_blocks;
890 st.index_offset = index_start;
891 }
892 let _ = max_level;
893 Ok(())
894 }
895
896 fn count_index_blocks_marker(&self) -> usize {
897 self.idx_blocks_total
898 }
899
900 fn flush_index_block(&mut self) -> Result<()> {
904 let Some(mut bw) = self.block.take() else {
905 return Ok(());
906 };
907 if bw.entries == 0 {
908 self.block = Some(bw);
909 return Ok(());
910 }
911 let raw_bytes = bw.finish()?;
912 let mut padding = 0;
913 if !self.opts.unpadded {
914 padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
915 }
916 if self.next == 0 {
917 let hs = self.header_size();
918 self.write_header_into_block(&mut bw, hs);
919 }
920 let data = bw.buf.clone();
921 self.padded_write(&data, padding);
922 self.index.push((bw.last_key.clone(), self.next));
923 self.next += (padding + raw_bytes) as u64;
924 self.idx_blocks_total += 1;
925 self.block = None;
926 Ok(())
927 }
928
929 fn dump_object_index(&mut self) -> Result<()> {
931 let mut max_common = 1usize;
933 for w in self.obj_entries.windows(2) {
934 let n = common_prefix_len(&w[0].hash, &w[1].hash);
935 if n > max_common {
936 max_common = n;
937 }
938 }
939 self.object_id_len = max_common + 1;
940 let id_len = self.object_id_len;
941
942 self.reinit_block(BLOCK_TYPE_OBJ);
943 let entries = std::mem::take(&mut self.obj_entries);
944 for e in &entries {
945 let prefix = e.hash[..id_len.min(e.hash.len())].to_vec();
946 self.add_obj_record(prefix, &e.offsets)?;
947 }
948 self.obj_entries = entries;
949 self.finish_section()
950 }
951
952 fn add_obj_record(&mut self, prefix: Vec<u8>, offsets: &[u64]) -> Result<()> {
953 let typ = BLOCK_TYPE_OBJ;
955 if self.block.is_none() {
956 self.reinit_block(typ);
957 }
958 let opts = self.opts.clone();
959 let rec = EncRecord::Obj {
960 prefix: prefix.clone(),
961 offsets: offsets.to_vec(),
962 };
963 let fit = {
964 let bw = self
965 .block
966 .as_mut()
967 .ok_or_else(|| Error::InvalidRef("reftable: no active block writer".into()))?;
968 bw.add(&rec, &opts)?
969 };
970 if fit {
971 return Ok(());
972 }
973 self.flush_block()?;
974 self.reinit_block(typ);
975 let opts = self.opts.clone();
976 let fit = {
977 let bw = self
978 .block
979 .as_mut()
980 .ok_or_else(|| Error::InvalidRef("reftable: no active block writer".into()))?;
981 bw.add(&rec, &opts)?
982 };
983 if fit {
984 return Ok(());
985 }
986 let rec = EncRecord::Obj {
988 prefix,
989 offsets: Vec::new(),
990 };
991 let opts = self.opts.clone();
992 let bw = self
993 .block
994 .as_mut()
995 .ok_or_else(|| Error::InvalidRef("reftable: no active block writer".into()))?;
996 bw.add(&rec, &opts)?;
997 Ok(())
998 }
999
1000 fn finish_public_section(&mut self) -> Result<()> {
1002 let Some(bw) = &self.block else {
1003 return Ok(());
1004 };
1005 let typ = bw.typ;
1006 self.finish_section()?;
1007 if typ == BLOCK_TYPE_REF && !self.opts.skip_index_objects && self.ref_stats.index_blocks > 0
1008 {
1009 self.dump_object_index()?;
1010 }
1011 self.obj_entries.clear();
1012 self.block = None;
1013 self.block_type = 0;
1014 Ok(())
1015 }
1016
1017 fn close(mut self) -> Result<Vec<u8>> {
1019 self.finish_public_section()?;
1020 let empty_table = self.next == 0;
1021 self.pending_padding = 0;
1022
1023 if empty_table {
1024 let hs = self.header_size();
1025 let mut header = vec![0u8; hs];
1026 self.write_header(&mut header);
1027 self.padded_write(&header, 0);
1028 }
1029
1030 let mut footer = vec![0u8; self.header_size()];
1031 self.write_header(&mut footer);
1032 footer.extend_from_slice(&self.ref_stats.index_offset.to_be_bytes());
1033 let obj_field = (self.obj_stats.offset << 5) | (self.object_id_len as u64);
1034 footer.extend_from_slice(&obj_field.to_be_bytes());
1035 footer.extend_from_slice(&self.obj_stats.index_offset.to_be_bytes());
1036 footer.extend_from_slice(&self.log_stats.offset.to_be_bytes());
1037 footer.extend_from_slice(&self.log_stats.index_offset.to_be_bytes());
1038 let crc = crc32(&footer);
1039 footer.extend_from_slice(&crc.to_be_bytes());
1040
1041 self.pending_padding = 0;
1043 self.out.extend_from_slice(&footer);
1044
1045 Ok(self.out)
1046 }
1047}
1048
1049pub struct ReftableReader {
1055 data: Vec<u8>,
1056 version: u8,
1057 block_size: u32,
1058 min_update_index: u64,
1059 max_update_index: u64,
1060 ref_index_position: u64,
1061 log_position: u64,
1062}
1063
1064#[derive(Debug)]
1066#[allow(dead_code)]
1067struct Footer {
1068 version: u8,
1069 block_size: u32,
1070 min_update_index: u64,
1071 max_update_index: u64,
1072 ref_index_position: u64,
1073 obj_position_and_id_len: u64,
1074 obj_index_position: u64,
1075 log_position: u64,
1076 log_index_position: u64,
1077}
1078
1079impl ReftableReader {
1080 pub fn new(data: Vec<u8>) -> Result<Self> {
1082 if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
1083 if data.len() < HEADER_SIZE {
1085 return Err(Error::InvalidRef("reftable: file too small".into()));
1086 }
1087 }
1088
1089 if &data[0..4] != REFTABLE_MAGIC {
1091 return Err(Error::InvalidRef("reftable: bad magic".into()));
1092 }
1093 let version = data[4];
1094 if version != 1 && version != 2 {
1095 return Err(Error::InvalidRef(format!(
1096 "reftable: unsupported version {version}"
1097 )));
1098 }
1099 let _block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
1100 let _min_update_index = u64::from_be_bytes(
1101 data[8..16]
1102 .try_into()
1103 .map_err(|_| Error::InvalidRef("reftable: truncated header".into()))?,
1104 );
1105 let _max_update_index = u64::from_be_bytes(
1106 data[16..24]
1107 .try_into()
1108 .map_err(|_| Error::InvalidRef("reftable: truncated header".into()))?,
1109 );
1110
1111 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
1113 if data.len() < footer_size {
1114 return Err(Error::InvalidRef(
1115 "reftable: file too small for footer".into(),
1116 ));
1117 }
1118 let footer_start = data.len() - footer_size;
1119 let footer = parse_footer(&data[footer_start..], version)?;
1120
1121 Ok(Self {
1122 data,
1123 version,
1124 block_size: footer.block_size,
1125 min_update_index: footer.min_update_index,
1126 max_update_index: footer.max_update_index,
1127 ref_index_position: footer.ref_index_position,
1128 log_position: footer.log_position,
1129 })
1130 }
1131
1132 fn hash_size(&self) -> usize {
1135 if self.version == 2 {
1136 32
1137 } else {
1138 20
1139 }
1140 }
1141
1142 fn header_len(&self) -> usize {
1144 if self.version == 2 {
1145 28
1146 } else {
1147 HEADER_SIZE
1148 }
1149 }
1150
1151 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1153 let mut refs = Vec::new();
1154 let footer_size = if self.version == 2 {
1155 72
1156 } else {
1157 FOOTER_V1_SIZE
1158 };
1159 let file_end = self.data.len() - footer_size;
1160
1161 let ref_end = if self.ref_index_position > 0 {
1163 self.ref_index_position as usize
1164 } else if self.log_position > 0 {
1165 self.log_position as usize
1166 } else {
1167 file_end
1168 };
1169
1170 let mut pos = 0usize;
1171 let header_len = self.header_len();
1174 if pos < header_len {
1175 pos = header_len;
1176 }
1177
1178 while pos < ref_end {
1179 if pos >= self.data.len() {
1180 break;
1181 }
1182 let block_type = self.data[pos];
1183 if block_type == 0 {
1184 if self.block_size > 0 {
1186 let bs = self.block_size as usize;
1187 pos = ((pos / bs) + 1) * bs;
1188 continue;
1189 } else {
1190 break;
1191 }
1192 }
1193 if block_type != BLOCK_TYPE_REF {
1194 break;
1195 }
1196
1197 let block_len = read_u24(&self.data, pos + 1);
1198 let block_data_start = pos + 4; let is_first = pos == header_len;
1203 let records_end = if is_first {
1204 block_len
1206 } else {
1207 pos + block_len
1208 };
1209
1210 if records_end > ref_end {
1211 break;
1212 }
1213
1214 let rc = read_u16(&self.data, records_end - 2);
1216 let restart_table_start = records_end - 2 - (rc * 3);
1218
1219 let mut rpos = block_data_start;
1221 let mut prev_name = Vec::<u8>::new();
1222
1223 while rpos < restart_table_start {
1224 let (rec, new_pos) = decode_ref_record(
1225 &self.data,
1226 rpos,
1227 &prev_name,
1228 self.min_update_index,
1229 self.hash_size(),
1230 )?;
1231 prev_name = rec.name.as_bytes().to_vec();
1232 refs.push(rec);
1233 rpos = new_pos;
1234 }
1235
1236 if self.block_size > 0 {
1238 let bs = self.block_size as usize;
1239 if is_first {
1240 pos = bs;
1241 } else {
1242 pos += bs;
1243 }
1244 } else {
1245 pos = records_end;
1246 }
1247 }
1248
1249 Ok(refs)
1250 }
1251
1252 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1254 let refs = self.read_refs()?;
1256 Ok(refs.into_iter().find(|r| r.name == name))
1257 }
1258
1259 pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
1261 let footer_size = if self.version == 2 {
1262 72
1263 } else {
1264 FOOTER_V1_SIZE
1265 };
1266 let file_end = self.data.len() - footer_size;
1267
1268 let mut pos = if self.log_position > 0 {
1276 self.log_position as usize
1277 } else if self.data.len() > self.header_len()
1278 && self.data[self.header_len()] == BLOCK_TYPE_LOG
1279 {
1280 self.header_len()
1282 } else {
1283 return Ok(Vec::new());
1284 };
1285 let mut logs = Vec::new();
1286
1287 while pos < file_end {
1288 if pos >= self.data.len() {
1289 break;
1290 }
1291 let block_type = self.data[pos];
1292 if block_type != BLOCK_TYPE_LOG {
1293 break;
1294 }
1295 let is_first = pos == self.header_len() && self.log_position == 0;
1300 let block_len = read_u24(&self.data, pos + 1);
1301 let compressed_start = pos + 4;
1302
1303 let header_prefix = if is_first { self.header_len() } else { 0 };
1306 let inflated_size = block_len.saturating_sub(4 + header_prefix);
1307
1308 use flate2::read::DeflateDecoder;
1310 let remaining = &self.data[compressed_start..file_end];
1311 let mut decoder = DeflateDecoder::new(remaining);
1312 let mut inflated = vec![0u8; inflated_size];
1313 decoder
1314 .read_exact(&mut inflated)
1315 .map_err(|e| Error::Zlib(e.to_string()))?;
1316
1317 let consumed = decoder.total_in() as usize;
1319
1320 if inflated.len() < 2 {
1323 break;
1324 }
1325 let rc = read_u16(&inflated, inflated.len() - 2);
1326 let restart_table_start = inflated.len() - 2 - (rc * 3);
1327
1328 let mut rpos = 0usize;
1329 let mut prev_key = Vec::<u8>::new();
1330
1331 while rpos < restart_table_start {
1332 let (log, new_pos) =
1333 decode_log_record(&inflated, rpos, &prev_key, self.hash_size())?;
1334 let mut key = Vec::new();
1336 key.extend_from_slice(log.refname.as_bytes());
1337 key.push(0);
1338 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
1339 prev_key = key;
1340 logs.push(log);
1341 rpos = new_pos;
1342 }
1343
1344 pos = compressed_start + consumed;
1345 }
1346
1347 Ok(logs)
1348 }
1349
1350 pub fn block_size(&self) -> u32 {
1352 self.block_size
1353 }
1354
1355 pub fn min_update_index(&self) -> u64 {
1357 self.min_update_index
1358 }
1359
1360 pub fn max_update_index(&self) -> u64 {
1362 self.max_update_index
1363 }
1364}
1365
1366fn decode_ref_record(
1371 data: &[u8],
1372 pos: usize,
1373 prev_name: &[u8],
1374 min_update_index: u64,
1375 hash_size: usize,
1376) -> Result<(RefRecord, usize)> {
1377 let (prefix_len, p) = get_varint(data, pos)?;
1378 let (suffix_and_type, mut p) = get_varint(data, p)?;
1379 let suffix_len = (suffix_and_type >> 3) as usize;
1380 let value_type = (suffix_and_type & 0x7) as u8;
1381
1382 let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
1384 if prefix_len > 0 {
1385 if (prefix_len as usize) > prev_name.len() {
1386 return Err(Error::InvalidRef(
1387 "reftable: prefix_len exceeds prev name".into(),
1388 ));
1389 }
1390 name.extend_from_slice(&prev_name[..prefix_len as usize]);
1391 }
1392 if p + suffix_len > data.len() {
1393 return Err(Error::InvalidRef("reftable: suffix overflows block".into()));
1394 }
1395 name.extend_from_slice(&data[p..p + suffix_len]);
1396 p += suffix_len;
1397
1398 let name_str = String::from_utf8(name)
1399 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in ref name".into()))?;
1400
1401 let (update_index_delta, mut p) = get_varint(data, p)?;
1402 let update_index = min_update_index + update_index_delta;
1403
1404 let value = match value_type {
1405 VALUE_DELETION => RefValue::Deletion,
1406 VALUE_ONE_OID => {
1407 if p + hash_size > data.len() {
1408 return Err(Error::InvalidRef("reftable: truncated OID".into()));
1409 }
1410 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1411 p += hash_size;
1412 RefValue::Val1(oid)
1413 }
1414 VALUE_TWO_OID => {
1415 if p + 2 * hash_size > data.len() {
1416 return Err(Error::InvalidRef("reftable: truncated OID pair".into()));
1417 }
1418 let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
1419 p += hash_size;
1420 let peeled = ObjectId::from_bytes(&data[p..p + hash_size])?;
1421 p += hash_size;
1422 RefValue::Val2(oid, peeled)
1423 }
1424 VALUE_SYMREF => {
1425 let (target_len, p2) = get_varint(data, p)?;
1426 p = p2;
1427 let target_len = target_len as usize;
1428 if p + target_len > data.len() {
1429 return Err(Error::InvalidRef(
1430 "reftable: truncated symref target".into(),
1431 ));
1432 }
1433 let target = String::from_utf8(data[p..p + target_len].to_vec())
1434 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in symref".into()))?;
1435 p += target_len;
1436 RefValue::Symref(target)
1437 }
1438 _ => {
1439 return Err(Error::InvalidRef(format!(
1440 "reftable: unknown value_type {value_type}"
1441 )));
1442 }
1443 };
1444
1445 Ok((
1446 RefRecord {
1447 name: name_str,
1448 update_index,
1449 value,
1450 },
1451 p,
1452 ))
1453}
1454
1455fn decode_log_record(
1456 data: &[u8],
1457 pos: usize,
1458 prev_key: &[u8],
1459 hash_size: usize,
1460) -> Result<(LogRecord, usize)> {
1461 let (prefix_len, p) = get_varint(data, pos)?;
1462 let (suffix_and_type, mut p) = get_varint(data, p)?;
1463 let suffix_len = (suffix_and_type >> 3) as usize;
1464 let log_type = (suffix_and_type & 0x7) as u8;
1465
1466 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
1468 if prefix_len > 0 {
1469 if (prefix_len as usize) > prev_key.len() {
1470 return Err(Error::InvalidRef(
1471 "reftable: log prefix_len exceeds prev key".into(),
1472 ));
1473 }
1474 key.extend_from_slice(&prev_key[..prefix_len as usize]);
1475 }
1476 if p + suffix_len > data.len() {
1477 return Err(Error::InvalidRef("reftable: log suffix overflows".into()));
1478 }
1479 key.extend_from_slice(&data[p..p + suffix_len]);
1480 p += suffix_len;
1481
1482 let null_pos = key
1484 .iter()
1485 .position(|&b| b == 0)
1486 .ok_or_else(|| Error::InvalidRef("reftable: log key missing null separator".into()))?;
1487 let refname = String::from_utf8(key[..null_pos].to_vec())
1488 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log refname".into()))?;
1489 if null_pos + 9 > key.len() {
1490 return Err(Error::InvalidRef("reftable: log key too short".into()));
1491 }
1492 let reversed_idx = u64::from_be_bytes(
1493 key[null_pos + 1..null_pos + 9]
1494 .try_into()
1495 .map_err(|_| Error::InvalidRef("reftable: log key too short".into()))?,
1496 );
1497 let update_index = 0xffffffffffffffffu64 - reversed_idx;
1498
1499 if log_type == 0 {
1500 let zero_oid = ObjectId::from_bytes(&vec![0u8; hash_size])?;
1502 return Ok((
1503 LogRecord {
1504 refname,
1505 update_index,
1506 old_id: zero_oid,
1507 new_id: zero_oid,
1508 name: String::new(),
1509 email: String::new(),
1510 time_seconds: 0,
1511 tz_offset: 0,
1512 message: String::new(),
1513 },
1514 p,
1515 ));
1516 }
1517
1518 if p + 2 * hash_size > data.len() {
1520 return Err(Error::InvalidRef("reftable: truncated log OIDs".into()));
1521 }
1522 let old_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1523 p += hash_size;
1524 let new_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
1525 p += hash_size;
1526
1527 let (name_len, p2) = get_varint(data, p)?;
1528 p = p2;
1529 let name_len = name_len as usize;
1530 if p + name_len > data.len() {
1531 return Err(Error::InvalidRef("reftable: truncated log name".into()));
1532 }
1533 let name = String::from_utf8(data[p..p + name_len].to_vec())
1534 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log name".into()))?;
1535 p += name_len;
1536
1537 let (email_len, p2) = get_varint(data, p)?;
1538 p = p2;
1539 let email_len = email_len as usize;
1540 if p + email_len > data.len() {
1541 return Err(Error::InvalidRef("reftable: truncated log email".into()));
1542 }
1543 let email = String::from_utf8(data[p..p + email_len].to_vec())
1544 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log email".into()))?;
1545 p += email_len;
1546
1547 let (time_seconds, p2) = get_varint(data, p)?;
1548 p = p2;
1549
1550 if p + 2 > data.len() {
1551 return Err(Error::InvalidRef("reftable: truncated tz_offset".into()));
1552 }
1553 let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1554 p += 2;
1555
1556 let (msg_len, p2) = get_varint(data, p)?;
1557 p = p2;
1558 let msg_len = msg_len as usize;
1559 if p + msg_len > data.len() {
1560 return Err(Error::InvalidRef("reftable: truncated log message".into()));
1561 }
1562 let message = String::from_utf8(data[p..p + msg_len].to_vec())
1563 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log message".into()))?;
1564 p += msg_len;
1565
1566 Ok((
1567 LogRecord {
1568 refname,
1569 update_index,
1570 old_id,
1571 new_id,
1572 name,
1573 email,
1574 time_seconds,
1575 tz_offset,
1576 message,
1577 },
1578 p,
1579 ))
1580}
1581
1582fn widen_oid_to(oid: ObjectId, hash_size: usize) -> ObjectId {
1590 if oid.is_zero() && oid.as_bytes().len() != hash_size {
1591 ObjectId::from_bytes(&vec![0u8; hash_size]).unwrap_or(oid)
1592 } else {
1593 oid
1594 }
1595}
1596
1597fn reftable_hash_size_for_git_dir(git_dir: &Path) -> usize {
1600 let cfg = crate::config::ConfigSet::load(Some(git_dir), true).unwrap_or_default();
1601 match cfg
1602 .get("extensions.objectformat")
1603 .and_then(|v| crate::objects::HashAlgo::from_name(&v))
1604 {
1605 Some(crate::objects::HashAlgo::Sha256) => 32,
1606 _ => 20,
1607 }
1608}
1609
1610pub struct ReftableStack {
1615 reftable_dir: PathBuf,
1617 table_names: Vec<String>,
1619}
1620
1621struct TablesListLock {
1624 path: PathBuf,
1625 armed: std::cell::Cell<bool>,
1626}
1627
1628impl TablesListLock {
1629 fn new(path: PathBuf) -> Self {
1630 Self {
1631 path,
1632 armed: std::cell::Cell::new(true),
1633 }
1634 }
1635
1636 fn disarm(&self) {
1639 self.armed.set(false);
1640 }
1641}
1642
1643impl Drop for TablesListLock {
1644 fn drop(&mut self) {
1645 if self.armed.get() {
1646 let _ = fs::remove_file(&self.path);
1647 }
1648 }
1649}
1650
1651impl ReftableStack {
1652 fn hash_size(&self) -> usize {
1655 match self.reftable_dir.parent() {
1656 Some(git_dir) => reftable_hash_size_for_git_dir(git_dir),
1657 None => 20,
1658 }
1659 }
1660
1661 pub fn open(git_dir: &Path) -> Result<Self> {
1663 let reftable_dir = git_dir.join("reftable");
1664 let tables_list = reftable_dir.join("tables.list");
1665 let content = fs::read_to_string(&tables_list).map_err(Error::Io)?;
1666 let table_names: Vec<String> = content
1667 .lines()
1668 .filter(|l| !l.is_empty())
1669 .map(|l| l.to_owned())
1670 .collect();
1671 Ok(Self {
1672 reftable_dir,
1673 table_names,
1674 })
1675 }
1676
1677 fn inject_head_ref(&self, refs: &mut Vec<RefRecord>, min_idx: u64) -> Option<LogRecord> {
1683 let git_dir = self.reftable_dir.parent()?;
1684 let head_path = git_dir.join("HEAD");
1685 let content = fs::read_to_string(&head_path).ok()?;
1686 let target = content.strip_prefix("ref: ")?.trim();
1687 if target.is_empty() || target == "refs/heads/.invalid" {
1688 return None;
1689 }
1690 if refs.iter().any(|r| r.name == "HEAD") {
1692 return None;
1693 }
1694 refs.push(RefRecord {
1696 name: "HEAD".to_owned(),
1697 update_index: min_idx,
1698 value: RefValue::Symref(target.to_owned()),
1699 });
1700 refs.sort_by(|a, b| a.name.cmp(&b.name));
1701
1702 if self
1709 .read_logs_for_ref("HEAD")
1710 .map(|logs| !logs.is_empty())
1711 .unwrap_or(false)
1712 {
1713 return None;
1714 }
1715
1716 let target_logs = self.read_logs_for_ref(target).ok()?;
1718 let newest = target_logs.into_iter().next()?;
1719 Some(LogRecord {
1720 refname: "HEAD".to_owned(),
1721 update_index: newest.update_index,
1722 old_id: newest.old_id,
1723 new_id: newest.new_id,
1724 name: newest.name,
1725 email: newest.email,
1726 time_seconds: newest.time_seconds,
1727 tz_offset: newest.tz_offset,
1728 message: newest.message,
1729 })
1730 }
1731
1732 fn write_options(&self) -> WriteOptions {
1734 let git_dir = self
1735 .reftable_dir
1736 .parent()
1737 .map(|p| p.to_path_buf())
1738 .unwrap_or_else(|| self.reftable_dir.clone());
1739 read_write_options(&git_dir)
1740 }
1741
1742 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1747 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
1748
1749 for name in &self.table_names {
1750 let path = self.reftable_dir.join(name);
1751 let data = match fs::read(&path) {
1752 Ok(data) => data,
1753 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
1754 Err(err) => return Err(Error::Io(err)),
1755 };
1756 let reader = ReftableReader::new(data)?;
1757 for rec in reader.read_refs()? {
1758 match &rec.value {
1759 RefValue::Deletion => {
1760 merged.remove(&rec.name);
1761 }
1762 _ => {
1763 merged.insert(rec.name.clone(), rec);
1764 }
1765 }
1766 }
1767 }
1768
1769 Ok(merged.into_values().collect())
1770 }
1771
1772 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1774 for table_name in self.table_names.iter().rev() {
1776 let path = self.reftable_dir.join(table_name);
1777 let data = match fs::read(&path) {
1778 Ok(data) => data,
1779 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
1780 Err(err) => return Err(Error::Io(err)),
1781 };
1782 let reader = ReftableReader::new(data)?;
1783 if let Some(rec) = reader.lookup_ref(name)? {
1784 return match rec.value {
1785 RefValue::Deletion => Ok(None),
1786 _ => Ok(Some(rec)),
1787 };
1788 }
1789 }
1790 Ok(None)
1791 }
1792
1793 pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
1795 let mut logs = Vec::new();
1796 for table_name in &self.table_names {
1797 let path = self.reftable_dir.join(table_name);
1798 let data = fs::read(&path).map_err(Error::Io)?;
1799 let reader = ReftableReader::new(data)?;
1800 for log in reader.read_logs()? {
1801 if log.refname == refname {
1802 logs.push(log);
1803 }
1804 }
1805 }
1806 logs.sort_by(|a, b| b.update_index.cmp(&a.update_index));
1808 Ok(logs)
1809 }
1810
1811 pub fn replace_logs_for_ref(
1813 &mut self,
1814 refname: &str,
1815 entries: &[crate::reflog::ReflogEntry],
1816 ) -> Result<()> {
1817 let refs = self.read_refs()?;
1818 let mut logs: Vec<LogRecord> = self
1819 .read_all_logs()?
1820 .into_iter()
1821 .filter(|log| log.refname != refname)
1822 .collect();
1823 let mut next_update_index = self.max_update_index()? + 1;
1824 let hash_size = self.hash_size();
1825 for entry in entries {
1826 let (name, email, time_secs, tz) = parse_identity_string(&entry.identity);
1827 logs.push(LogRecord {
1828 refname: refname.to_owned(),
1829 update_index: next_update_index,
1830 old_id: widen_oid_to(entry.old_oid, hash_size),
1831 new_id: widen_oid_to(entry.new_oid, hash_size),
1832 name,
1833 email,
1834 time_seconds: time_secs,
1835 tz_offset: tz,
1836 message: entry.message.clone(),
1837 });
1838 next_update_index += 1;
1839 }
1840
1841 let mut min_idx = u64::MAX;
1842 let mut max_idx = 0u64;
1843 for name in &self.table_names {
1844 let path = self.reftable_dir.join(name);
1845 let data = fs::read(&path).map_err(Error::Io)?;
1846 let reader = ReftableReader::new(data)?;
1847 min_idx = min_idx.min(reader.min_update_index());
1848 max_idx = max_idx.max(reader.max_update_index());
1849 }
1850 if min_idx == u64::MAX {
1851 min_idx = 0;
1852 }
1853 max_idx = max_idx.max(next_update_index.saturating_sub(1));
1854
1855 let mut wopts = WriteOptions::default();
1856 wopts.hash_size = self.hash_size();
1857 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
1858 for rec in refs {
1859 writer.add_ref(rec)?;
1860 }
1861 for log in logs {
1862 writer.add_log(log)?;
1863 }
1864 let data = writer.finish()?;
1865 let old_names = self.table_names.clone();
1866 let name = self.write_table_file(&data, max_idx)?;
1867 self.table_names = vec![name];
1868 self.write_tables_list()?;
1869 for old in &old_names {
1870 let _ = fs::remove_file(self.reftable_dir.join(old));
1871 }
1872 Ok(())
1873 }
1874
1875 pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
1877 let mut logs = Vec::new();
1878 for table_name in &self.table_names {
1879 let path = self.reftable_dir.join(table_name);
1880 let data = fs::read(&path).map_err(Error::Io)?;
1881 let reader = ReftableReader::new(data)?;
1882 logs.extend(reader.read_logs()?);
1883 }
1884 logs.sort_by(|a, b| {
1885 a.refname
1886 .cmp(&b.refname)
1887 .then_with(|| b.update_index.cmp(&a.update_index))
1888 });
1889 Ok(logs)
1890 }
1891
1892 pub fn max_update_index(&self) -> Result<u64> {
1900 let names: Vec<String> = match fs::read_to_string(self.reftable_dir.join("tables.list")) {
1901 Ok(content) => content
1902 .lines()
1903 .filter(|line| !line.is_empty())
1904 .map(ToOwned::to_owned)
1905 .collect(),
1906 Err(_) => self.table_names.clone(),
1907 };
1908 let mut max_idx = 0u64;
1909 for name in &names {
1910 let path = self.reftable_dir.join(name);
1911 let data = match fs::read(&path) {
1912 Ok(data) => data,
1913 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
1914 Err(err) => return Err(Error::Io(err)),
1915 };
1916 let reader = ReftableReader::new(data)?;
1917 max_idx = max_idx.max(reader.max_update_index());
1918 }
1919 Ok(max_idx)
1920 }
1921
1922 pub fn add_table(&mut self, data: &[u8], update_index: u64) -> Result<String> {
1927 let table_has_deletion = ReftableReader::new(data.to_vec())
1928 .and_then(|reader| reader.read_refs())
1929 .map(|records| {
1930 records
1931 .iter()
1932 .any(|record| matches!(record.value, RefValue::Deletion))
1933 })
1934 .unwrap_or(false);
1935 let random: u64 = {
1936 let mut buf = [0u8; 8];
1938 if let Ok(mut f) = fs::File::open("/dev/urandom") {
1939 let _ = f.read(&mut buf);
1940 }
1941 u64::from_le_bytes(buf)
1942 };
1943 let filename = format!(
1944 "{:08x}-{:08x}-{:08x}.ref",
1945 update_index, update_index, random as u32
1946 );
1947 let path = self.reftable_dir.join(&filename);
1948 fs::write(&path, data).map_err(Error::Io)?;
1949
1950 {
1955 let guard = self.acquire_tables_list_lock()?;
1956 self.reload_table_names();
1957 self.table_names.push(filename.clone());
1958 self.write_tables_list_locked(&guard)?;
1959 }
1960
1961 if table_has_deletion && self.table_names.len() > 2 {
1965 self.compact_prefix_preserving_newest()?;
1966 } else if self.table_names.len() > 3
1967 && std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1968 .map(|value| value != "false")
1969 .unwrap_or(true)
1970 {
1971 if self
1972 .table_names
1973 .iter()
1974 .any(|name| self.table_is_locked(name))
1975 {
1976 self.compact_unlocked_suffix()?;
1977 } else {
1978 self.compact()?;
1979 }
1980 }
1981
1982 Ok(filename)
1983 }
1984
1985 fn compact_prefix_preserving_newest(&mut self) -> Result<()> {
1986 if std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1987 .map(|value| value == "false")
1988 .unwrap_or(false)
1989 {
1990 return Ok(());
1991 }
1992 let guard = self.acquire_tables_list_lock()?;
1993 self.reload_table_names();
1994 if self.table_names.len() <= 2 {
1995 return Ok(());
1996 }
1997 let newest =
1998 self.table_names.last().cloned().ok_or_else(|| {
1999 Error::InvalidRef("reftable: table stack unexpectedly empty".into())
2000 })?;
2001 let old_names: Vec<String> = self.table_names[..self.table_names.len() - 1].to_vec();
2002 let prefix_stack = Self {
2003 reftable_dir: self.reftable_dir.clone(),
2004 table_names: old_names.clone(),
2005 };
2006 let refs = prefix_stack.read_refs()?;
2007 let logs = prefix_stack.read_all_logs()?;
2008
2009 let mut min_idx = u64::MAX;
2010 let mut max_idx = 0u64;
2011 for name in &old_names {
2012 let path = self.reftable_dir.join(name);
2013 let data = fs::read(&path).map_err(Error::Io)?;
2014 let reader = ReftableReader::new(data)?;
2015 min_idx = min_idx.min(reader.min_update_index());
2016 max_idx = max_idx.max(reader.max_update_index());
2017 }
2018 if min_idx == u64::MAX {
2019 min_idx = 0;
2020 }
2021
2022 let mut wopts = WriteOptions::default();
2023 wopts.hash_size = self.hash_size();
2024 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2025 for rec in refs {
2026 writer.add_ref(rec)?;
2027 }
2028 for log in logs {
2029 writer.add_log(log)?;
2030 }
2031 let data = writer.finish()?;
2032 let filename = self.write_table_file(&data, max_idx)?;
2033 let keep: Vec<String> = vec![filename.clone(), newest.clone()];
2034 self.table_names = keep;
2035 self.write_tables_list_locked(&guard)?;
2036 for old in &old_names {
2037 if old == &filename || old == &newest {
2038 continue;
2039 }
2040 let _ = fs::remove_file(self.reftable_dir.join(old));
2041 }
2042 Ok(())
2043 }
2044
2045 fn table_is_locked(&self, name: &str) -> bool {
2046 self.reftable_dir.join(format!("{name}.lock")).exists()
2047 }
2048
2049 fn compact_unlocked_suffix(&mut self) -> Result<()> {
2050 let guard = self.acquire_tables_list_lock()?;
2051 self.reload_table_names();
2052 let first_unlocked = self
2053 .table_names
2054 .iter()
2055 .position(|name| !self.table_is_locked(name))
2056 .unwrap_or(self.table_names.len());
2057 if self.table_names.len().saturating_sub(first_unlocked) <= 1 {
2058 return Ok(());
2059 }
2060
2061 let locked_prefix: Vec<String> = self.table_names[..first_unlocked].to_vec();
2062 let old_suffix: Vec<String> = self.table_names[first_unlocked..].to_vec();
2063 let suffix_stack = Self {
2064 reftable_dir: self.reftable_dir.clone(),
2065 table_names: old_suffix.clone(),
2066 };
2067 let refs = suffix_stack.read_refs()?;
2068 let logs = suffix_stack.read_all_logs()?;
2069
2070 let mut min_idx = u64::MAX;
2071 let mut max_idx = 0u64;
2072 for name in &old_suffix {
2073 let path = self.reftable_dir.join(name);
2074 let data = fs::read(&path).map_err(Error::Io)?;
2075 let reader = ReftableReader::new(data)?;
2076 min_idx = min_idx.min(reader.min_update_index());
2077 max_idx = max_idx.max(reader.max_update_index());
2078 }
2079 if min_idx == u64::MAX {
2080 min_idx = 0;
2081 }
2082
2083 let mut wopts = WriteOptions::default();
2084 wopts.hash_size = self.hash_size();
2085 let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
2086 for rec in refs {
2087 writer.add_ref(rec)?;
2088 }
2089 for log in logs {
2090 writer.add_log(log)?;
2091 }
2092 let data = writer.finish()?;
2093 let compacted = self.write_table_file(&data, max_idx)?;
2094
2095 self.table_names = locked_prefix;
2096 self.table_names.push(compacted.clone());
2097 self.write_tables_list_locked(&guard)?;
2098 for old in &old_suffix {
2099 if old == &compacted {
2100 continue;
2101 }
2102 let _ = fs::remove_file(self.reftable_dir.join(old));
2103 }
2104 Ok(())
2105 }
2106
2107 pub fn write_ref(
2111 &mut self,
2112 refname: &str,
2113 value: RefValue,
2114 log: Option<LogRecord>,
2115 opts: &WriteOptions,
2116 ) -> Result<()> {
2117 {
2124 let guard = self.acquire_tables_list_lock()?;
2125 self.reload_table_names();
2126 let update_index = self.max_update_index_unlocked()? + 1;
2127 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
2128 writer.add_ref(RefRecord {
2129 name: refname.to_owned(),
2130 update_index,
2131 value,
2132 })?;
2133 if let Some(log_rec) = log {
2134 let mut log_rec = log_rec;
2135 log_rec.update_index = update_index;
2136 writer.add_log(log_rec)?;
2137 }
2138 let data = writer.finish()?;
2139 let filename = self.write_table_file(&data, update_index)?;
2140 self.table_names.push(filename);
2141 self.write_tables_list_locked(&guard)?;
2142 }
2143
2144 self.maybe_auto_compact()?;
2147 Ok(())
2148 }
2149
2150 pub fn write_transaction(
2156 &mut self,
2157 updates: Vec<ReftableTransactionUpdate>,
2158 opts: &WriteOptions,
2159 ) -> Result<()> {
2160 if updates.is_empty() {
2161 return Ok(());
2162 }
2163
2164 {
2165 let guard = self.acquire_tables_list_lock()?;
2166 self.reload_table_names();
2167 let update_index = self.max_update_index_unlocked()? + 1;
2168 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
2169
2170 let mut updates = updates;
2171 updates.sort_by(|a, b| a.refname.cmp(&b.refname));
2172 for update in &updates {
2173 writer.add_ref(RefRecord {
2174 name: update.refname.clone(),
2175 update_index,
2176 value: update.value.clone(),
2177 })?;
2178 }
2179 for update in updates {
2180 if let Some(mut log) = update.log {
2181 log.update_index = update_index;
2182 writer.add_log(log)?;
2183 }
2184 }
2185
2186 let data = writer.finish()?;
2187 let filename = self.write_table_file(&data, update_index)?;
2188 self.table_names.push(filename);
2189 self.write_tables_list_locked(&guard)?;
2190 }
2191
2192 self.maybe_auto_compact()?;
2193 Ok(())
2194 }
2195
2196 fn max_update_index_unlocked(&self) -> Result<u64> {
2200 let mut max_idx = 0u64;
2201 for name in &self.table_names {
2202 let path = self.reftable_dir.join(name);
2203 let data = match fs::read(&path) {
2204 Ok(data) => data,
2205 Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
2206 Err(err) => return Err(Error::Io(err)),
2207 };
2208 let reader = ReftableReader::new(data)?;
2209 max_idx = max_idx.max(reader.max_update_index());
2210 }
2211 Ok(max_idx)
2212 }
2213
2214 fn maybe_auto_compact(&mut self) -> Result<()> {
2217 self.reload_table_names();
2218 let has_locked = self
2219 .table_names
2220 .iter()
2221 .any(|name| self.table_is_locked(name));
2222 if self.table_names.len() > 3
2223 && std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
2224 .map(|value| value != "false")
2225 .unwrap_or(true)
2226 {
2227 if has_locked {
2228 self.compact_unlocked_suffix()?;
2229 } else {
2230 self.compact()?;
2231 }
2232 }
2233 Ok(())
2234 }
2235
2236 pub fn compact(&mut self) -> Result<()> {
2242 let guard = self.acquire_tables_list_lock()?;
2247 self.reload_table_names();
2248 if self.table_names.is_empty() {
2249 return Ok(());
2250 }
2251
2252 let refs = self.read_refs()?;
2254 let logs = self.read_all_logs()?;
2255
2256 let mut min_idx = u64::MAX;
2258 let mut max_idx = 0u64;
2259 for name in &self.table_names {
2260 let path = self.reftable_dir.join(name);
2261 let data = fs::read(&path).map_err(Error::Io)?;
2262 let reader = ReftableReader::new(data)?;
2263 min_idx = min_idx.min(reader.min_update_index());
2264 max_idx = max_idx.max(reader.max_update_index());
2265 }
2266 if min_idx == u64::MAX {
2267 min_idx = 0;
2268 }
2269
2270 let opts = self.write_options();
2273
2274 let mut refs = refs;
2279 let head_log = self.inject_head_ref(&mut refs, min_idx);
2280
2281 let mut writer = ReftableWriter::new(opts.clone(), min_idx, max_idx);
2282 for rec in refs {
2283 writer.add_ref(rec)?;
2284 }
2285 if opts.write_log {
2286 let mut logs = logs;
2287 if let Some(hl) = head_log {
2288 logs.push(hl);
2289 }
2290 for log in logs {
2291 writer.add_log(log)?;
2292 }
2293 }
2294
2295 let data = writer.finish()?;
2296
2297 let old_names = self.table_names.clone();
2299 self.table_names.clear();
2300 let name = self.write_table_file(&data, max_idx)?;
2301 self.table_names.push(name.clone());
2302 self.write_tables_list_locked(&guard)?;
2303
2304 for old in &old_names {
2306 if old == &name {
2307 continue;
2308 }
2309 let path = self.reftable_dir.join(old);
2310 let _ = fs::remove_file(&path);
2311 }
2312
2313 Ok(())
2314 }
2315
2316 fn write_table_file(&self, data: &[u8], update_index: u64) -> Result<String> {
2317 let random: u64 = {
2318 let mut buf = [0u8; 8];
2319 if let Ok(mut f) = fs::File::open("/dev/urandom") {
2320 let _ = f.read(&mut buf);
2321 }
2322 u64::from_le_bytes(buf)
2323 };
2324 let filename = format!(
2325 "{:08x}-{:08x}-{:08x}.ref",
2326 update_index, update_index, random as u32
2327 );
2328 let path = self.reftable_dir.join(&filename);
2329 fs::write(&path, data).map_err(Error::Io)?;
2330 Ok(filename)
2331 }
2332
2333 fn write_tables_list(&self) -> Result<()> {
2341 let guard = self.acquire_tables_list_lock()?;
2342 self.write_tables_list_locked(&guard)
2343 }
2344
2345 fn write_tables_list_locked(&self, guard: &TablesListLock) -> Result<()> {
2347 let tables_list = self.reftable_dir.join("tables.list");
2348 let content = self.table_names.join("\n")
2349 + if self.table_names.is_empty() {
2350 ""
2351 } else {
2352 "\n"
2353 };
2354 fs::write(&guard.path, &content).map_err(Error::Io)?;
2355 fs::rename(&guard.path, &tables_list).map_err(Error::Io)?;
2358 guard.disarm();
2359 Ok(())
2360 }
2361
2362 fn lock_timeout_ms(&self) -> u64 {
2363 let git_dir = self
2364 .reftable_dir
2365 .parent()
2366 .unwrap_or(self.reftable_dir.as_path());
2367 let config = ConfigSet::load(Some(git_dir), true).unwrap_or_else(|_| ConfigSet::new());
2368 config
2369 .get("reftable.lockTimeout")
2370 .and_then(|value| value.parse::<u64>().ok())
2371 .unwrap_or(1000)
2372 }
2373
2374 fn acquire_tables_list_lock(&self) -> Result<TablesListLock> {
2379 let lock = self.reftable_dir.join("tables.list.lock");
2380 let timeout_ms = self.lock_timeout_ms();
2381 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
2382 loop {
2383 match fs::OpenOptions::new()
2384 .write(true)
2385 .create_new(true)
2386 .open(&lock)
2387 {
2388 Ok(_) => return Ok(TablesListLock::new(lock)),
2389 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2390 if timeout_ms == 0 || Instant::now() >= deadline {
2391 return Err(Error::InvalidRef(
2392 "cannot lock references: data is locked".to_owned(),
2393 ));
2394 }
2395 thread::sleep(Duration::from_millis(20));
2396 }
2397 Err(err) => return Err(Error::Io(err)),
2398 }
2399 }
2400 }
2401
2402 fn reload_table_names(&mut self) {
2406 if let Ok(content) = fs::read_to_string(self.reftable_dir.join("tables.list")) {
2407 self.table_names = content
2408 .lines()
2409 .filter(|line| !line.is_empty())
2410 .map(ToOwned::to_owned)
2411 .collect();
2412 }
2413 }
2414
2415 pub fn table_names(&self) -> &[String] {
2417 &self.table_names
2418 }
2419}
2420
2421pub fn is_reftable_repo(git_dir: &Path) -> bool {
2427 fn config_uses_reftable(config_path: &Path) -> bool {
2428 let Ok(content) = fs::read_to_string(config_path) else {
2429 return false;
2430 };
2431
2432 let mut in_extensions = false;
2433 for line in content.lines() {
2434 let trimmed = line.trim();
2435 if trimmed.starts_with('[') {
2436 in_extensions = trimmed.eq_ignore_ascii_case("[extensions]");
2437 continue;
2438 }
2439 if in_extensions {
2440 if let Some((key, value)) = trimmed.split_once('=') {
2441 if key.trim().eq_ignore_ascii_case("refstorage")
2442 && value.trim().eq_ignore_ascii_case("reftable")
2443 {
2444 return true;
2445 }
2446 }
2447 }
2448 }
2449 false
2450 }
2451
2452 let local_config = git_dir.join("config");
2453 if config_uses_reftable(&local_config) {
2454 return true;
2455 }
2456
2457 if let Ok(raw) = fs::read_to_string(git_dir.join("commondir")) {
2460 let rel = raw.trim();
2461 if !rel.is_empty() {
2462 let common = if Path::new(rel).is_absolute() {
2463 PathBuf::from(rel)
2464 } else {
2465 git_dir.join(rel)
2466 };
2467 let common_config = common.canonicalize().unwrap_or(common).join("config");
2468 if config_uses_reftable(&common_config) {
2469 return true;
2470 }
2471 }
2472 }
2473
2474 false
2475}
2476
2477pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
2479 reftable_resolve_ref_depth(git_dir, refname, 0)
2480}
2481
2482fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
2483 if let Some(rest) = refname.strip_prefix("worktrees/") {
2484 if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
2485 if per_worktree_ref.starts_with("refs/") {
2486 let common =
2487 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
2488 return (
2489 common.join("worktrees").join(worktree_id),
2490 per_worktree_ref.to_owned(),
2491 );
2492 }
2493 }
2494 }
2495
2496 if refname == "HEAD"
2497 || refname.starts_with("refs/worktree/")
2498 || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
2499 {
2500 return (git_dir.to_path_buf(), refname.to_owned());
2501 }
2502
2503 (
2504 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
2505 refname.to_owned(),
2506 )
2507}
2508
2509fn reftable_resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
2510 if depth > 10 {
2511 return Err(Error::InvalidRef(format!(
2512 "reftable: symlink too deep: {refname}"
2513 )));
2514 }
2515
2516 if refname == "HEAD" {
2518 let head_path = git_dir.join("HEAD");
2519 if head_path.exists() {
2520 let content = fs::read_to_string(&head_path).map_err(Error::Io)?;
2521 let content = content.trim();
2522 if let Some(target) = content.strip_prefix("ref: ") {
2523 if target.trim() == "refs/heads/.invalid" {
2524 return reftable_resolve_ref_depth(git_dir, "refs/worktree/HEAD", depth + 1);
2525 }
2526 return reftable_resolve_ref_depth(git_dir, target.trim(), depth + 1);
2527 }
2528 if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
2530 return content.parse();
2531 }
2532 }
2533 }
2534
2535 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2536 let stack = ReftableStack::open(&store_git_dir)?;
2537 match stack.lookup_ref(&storage_refname)? {
2538 Some(rec) => match rec.value {
2539 RefValue::Val1(oid) => Ok(oid),
2540 RefValue::Val2(oid, _) => Ok(oid),
2541 RefValue::Symref(target) => {
2542 reftable_resolve_ref_depth(&store_git_dir, &target, depth + 1)
2543 }
2544 RefValue::Deletion => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
2545 },
2546 None => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
2547 }
2548}
2549
2550pub fn reftable_write_ref(
2552 git_dir: &Path,
2553 refname: &str,
2554 oid: &ObjectId,
2555 log_identity: Option<&str>,
2556 log_message: Option<&str>,
2557) -> Result<()> {
2558 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2559 let mut stack = ReftableStack::open(&store_git_dir)?;
2560 let old_oid = match stack
2561 .lookup_ref(&storage_refname)?
2562 .and_then(|r| match r.value {
2563 RefValue::Val1(oid) => Some(oid),
2564 RefValue::Val2(oid, _) => Some(oid),
2565 _ => None,
2566 }) {
2567 Some(oid) => oid,
2568 None => ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?,
2569 };
2570
2571 let log = if let Some(identity) = log_identity {
2572 let (name, email, time_secs, tz) = parse_identity_string(identity);
2573 Some(LogRecord {
2574 refname: storage_refname.clone(),
2575 update_index: 0, old_id: old_oid,
2577 new_id: *oid,
2578 name,
2579 email,
2580 time_seconds: time_secs,
2581 tz_offset: tz,
2582 message: log_message.unwrap_or("").to_owned(),
2583 })
2584 } else {
2585 None
2586 };
2587
2588 let write_log = log.is_some() || should_log_ref_updates(&store_git_dir);
2590 let log = if write_log { log } else { None };
2591
2592 let opts = read_write_options(&store_git_dir);
2593 stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, &opts)
2594}
2595
2596pub fn reftable_write_symref(
2598 git_dir: &Path,
2599 refname: &str,
2600 target: &str,
2601 log_identity: Option<&str>,
2602 log_message: Option<&str>,
2603) -> Result<()> {
2604 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2605 let mut stack = ReftableStack::open(&store_git_dir)?;
2606 let opts = read_write_options(&store_git_dir);
2607
2608 let log = if let Some(identity) = log_identity {
2609 let (name, email, time_secs, tz) = parse_identity_string(identity);
2610 let zero_oid =
2611 ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?;
2612 Some(LogRecord {
2613 refname: storage_refname.clone(),
2614 update_index: 0,
2615 old_id: zero_oid,
2616 new_id: zero_oid,
2617 name,
2618 email,
2619 time_seconds: time_secs,
2620 tz_offset: tz,
2621 message: log_message.unwrap_or("").to_owned(),
2622 })
2623 } else {
2624 None
2625 };
2626
2627 stack.write_ref(
2628 &storage_refname,
2629 RefValue::Symref(target.to_owned()),
2630 log,
2631 &opts,
2632 )
2633}
2634
2635pub fn reftable_write_transaction(
2641 git_dir: &Path,
2642 updates: Vec<ReftableTransactionUpdate>,
2643) -> Result<()> {
2644 let mut grouped: BTreeMap<PathBuf, Vec<ReftableTransactionUpdate>> = BTreeMap::new();
2645 for mut update in updates {
2646 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, &update.refname);
2647 update.refname = storage_refname.clone();
2648 if let Some(log) = update.log.as_mut() {
2649 log.refname = storage_refname;
2650 }
2651 grouped.entry(store_git_dir).or_default().push(update);
2652 }
2653
2654 for (store_git_dir, updates) in grouped {
2655 let mut stack = ReftableStack::open(&store_git_dir)?;
2656 let opts = read_write_options(&store_git_dir);
2657 stack.write_transaction(updates, &opts)?;
2658 }
2659 Ok(())
2660}
2661
2662pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
2664 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2665 let mut stack = ReftableStack::open(&store_git_dir)?;
2666 let opts = read_write_options(&store_git_dir);
2667 stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
2668}
2669
2670pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
2672 if refname == "HEAD" {
2673 let head_path = git_dir.join("HEAD");
2674 let content = match fs::read_to_string(&head_path) {
2675 Ok(content) => content,
2676 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
2677 Err(err) => return Err(Error::Io(err)),
2678 };
2679 return Ok(content
2680 .trim()
2681 .strip_prefix("ref: ")
2682 .map(|target| target.trim().to_owned()));
2683 }
2684 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2685 let stack = ReftableStack::open(&store_git_dir)?;
2686 match stack.lookup_ref(&storage_refname)? {
2687 Some(rec) => match rec.value {
2688 RefValue::Symref(target) => Ok(Some(target)),
2689 _ => Ok(None),
2690 },
2691 None => Ok(None),
2692 }
2693}
2694
2695pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
2697 let stack = ReftableStack::open(git_dir)?;
2698 let refs = stack.read_refs()?;
2699 let mut result = Vec::new();
2700 for rec in refs {
2701 let matches_prefix = rec.name.starts_with(prefix)
2702 || (prefix.ends_with('/') && rec.name == prefix.trim_end_matches('/'));
2703 if matches_prefix {
2704 match rec.value {
2705 RefValue::Val1(oid) => result.push((rec.name, oid)),
2706 RefValue::Val2(oid, _) => result.push((rec.name, oid)),
2707 RefValue::Symref(target) => {
2708 if let Ok(oid) = reftable_resolve_ref(git_dir, &target) {
2710 result.push((rec.name, oid));
2711 }
2712 }
2713 RefValue::Deletion => {}
2714 }
2715 }
2716 }
2717 result.sort_by(|a, b| a.0.cmp(&b.0));
2718 Ok(result)
2719}
2720
2721pub fn reftable_read_reflog(
2723 git_dir: &Path,
2724 refname: &str,
2725) -> Result<Vec<crate::reflog::ReflogEntry>> {
2726 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2727 let stack = ReftableStack::open(&store_git_dir)?;
2728 let logs = stack.read_logs_for_ref(&storage_refname)?;
2729 let mut entries = Vec::new();
2730 for log in logs {
2731 let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
2733 let tz_abs = log.tz_offset.unsigned_abs();
2734 let tz_hours = tz_abs / 60;
2735 let tz_mins = tz_abs % 60;
2736 let identity = format!(
2737 "{} <{}> {} {}{:02}{:02}",
2738 log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
2739 );
2740 let message = log
2746 .message
2747 .strip_suffix('\n')
2748 .map(ToOwned::to_owned)
2749 .unwrap_or(log.message);
2750 entries.push(crate::reflog::ReflogEntry {
2751 old_oid: log.old_id,
2752 new_oid: log.new_id,
2753 identity,
2754 message,
2755 });
2756 }
2757 entries.reverse();
2758 Ok(entries)
2759}
2760
2761pub fn reftable_replace_reflog(
2763 git_dir: &Path,
2764 refname: &str,
2765 entries: &[crate::reflog::ReflogEntry],
2766) -> Result<()> {
2767 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2768 let mut markers = read_empty_reflog_markers(&store_git_dir);
2769 if entries.is_empty() {
2770 markers.insert(storage_refname.clone());
2771 } else {
2772 markers.remove(&storage_refname);
2773 }
2774 write_empty_reflog_markers(&store_git_dir, &markers)?;
2775 let mut stack = ReftableStack::open(&store_git_dir)?;
2776 stack.replace_logs_for_ref(&storage_refname, entries)
2777}
2778
2779enum LogRefsMode {
2787 Always,
2788 Normal,
2789 None,
2790}
2791
2792fn reftable_log_refs_mode(git_dir: &Path) -> LogRefsMode {
2793 let config = ConfigSet::load(Some(git_dir), true).ok();
2794 let value = config
2795 .as_ref()
2796 .and_then(|cfg| cfg.get("core.logAllRefUpdates"));
2797 match value.as_deref().map(str::to_ascii_lowercase).as_deref() {
2798 Some("always") => LogRefsMode::Always,
2799 Some("true") | Some("yes") | Some("on") | Some("1") => LogRefsMode::Normal,
2800 Some("false") | Some("no") | Some("off") | Some("0") | Some("never") => LogRefsMode::None,
2801 _ => {
2803 let bare = config
2804 .as_ref()
2805 .and_then(|cfg| cfg.get_bool("core.bare"))
2806 .and_then(std::result::Result::ok)
2807 .unwrap_or(false);
2808 if bare {
2809 LogRefsMode::None
2810 } else {
2811 LogRefsMode::Normal
2812 }
2813 }
2814 }
2815}
2816
2817fn reftable_should_write_log(git_dir: &Path, storage_refname: &str) -> bool {
2820 use crate::refs::should_autocreate_reflog_for_mode;
2821 match reftable_log_refs_mode(git_dir) {
2822 LogRefsMode::Always => true,
2823 LogRefsMode::Normal => {
2824 if should_autocreate_reflog_for_mode(
2825 storage_refname,
2826 crate::refs::LogRefsConfig::Normal,
2827 ) {
2828 true
2829 } else {
2830 reftable_reflog_exists(git_dir, storage_refname)
2831 }
2832 }
2833 LogRefsMode::None => reftable_reflog_exists(git_dir, storage_refname),
2834 }
2835}
2836
2837pub fn reftable_append_reflog(
2839 git_dir: &Path,
2840 refname: &str,
2841 old_oid: &ObjectId,
2842 new_oid: &ObjectId,
2843 identity: &str,
2844 message: &str,
2845 force_create: bool,
2846) -> Result<()> {
2847 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2848 if !force_create && !reftable_should_write_log(&store_git_dir, &storage_refname) {
2857 return Ok(());
2858 }
2859 let (name, email, time_secs, tz) = parse_identity_string(identity);
2860 let mut stack = ReftableStack::open(&store_git_dir)?;
2861 let update_index = stack.max_update_index()? + 1;
2862 let opts = read_write_options(&store_git_dir);
2863
2864 let hash_size = opts.hash_size;
2868 let mut writer = ReftableWriter::new(opts, update_index, update_index);
2869 writer.add_log(LogRecord {
2870 refname: storage_refname.clone(),
2871 update_index,
2872 old_id: widen_oid_to(*old_oid, hash_size),
2873 new_id: widen_oid_to(*new_oid, hash_size),
2874 name,
2875 email,
2876 time_seconds: time_secs,
2877 tz_offset: tz,
2878 message: message.to_owned(),
2879 })?;
2880
2881 let data = writer.finish()?;
2882 stack.add_table(&data, update_index)?;
2883 if storage_refname.starts_with("refs/heads/branch-") {
2884 stack.reload_table_names();
2885 let has_locked = stack
2886 .table_names
2887 .iter()
2888 .any(|name| stack.table_is_locked(name));
2889 if !has_locked && stack.table_names.len() <= 2 {
2890 stack.compact()?;
2891 }
2892 }
2893 Ok(())
2894}
2895
2896pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
2898 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2899 if read_empty_reflog_markers(&store_git_dir).contains(&storage_refname) {
2900 return true;
2901 }
2902 if let Ok(stack) = ReftableStack::open(&store_git_dir) {
2903 if let Ok(logs) = stack.read_logs_for_ref(&storage_refname) {
2904 return !logs.is_empty();
2905 }
2906 }
2907 false
2908}
2909
2910pub fn reftable_list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
2912 let stack = ReftableStack::open(git_dir)?;
2913 let mut refs: BTreeSet<String> = read_empty_reflog_markers(git_dir);
2914 for log in stack.read_all_logs()? {
2915 refs.insert(log.refname);
2916 }
2917 Ok(refs.into_iter().collect())
2918}
2919
2920fn empty_reflog_markers_path(git_dir: &Path) -> PathBuf {
2921 git_dir.join("reftable").join("empty-reflogs")
2922}
2923
2924fn read_empty_reflog_markers(git_dir: &Path) -> BTreeSet<String> {
2925 fs::read_to_string(empty_reflog_markers_path(git_dir))
2926 .map(|content| {
2927 content
2928 .lines()
2929 .filter(|line| !line.trim().is_empty())
2930 .map(ToOwned::to_owned)
2931 .collect()
2932 })
2933 .unwrap_or_default()
2934}
2935
2936fn write_empty_reflog_markers(git_dir: &Path, markers: &BTreeSet<String>) -> Result<()> {
2937 let path = empty_reflog_markers_path(git_dir);
2938 let content = markers.iter().cloned().collect::<Vec<_>>().join("\n");
2939 fs::write(
2940 path,
2941 if content.is_empty() {
2942 content
2943 } else {
2944 content + "\n"
2945 },
2946 )?;
2947 Ok(())
2948}
2949
2950pub fn reftable_create_reflog(git_dir: &Path, refname: &str) -> Result<()> {
2952 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2953 let mut markers = read_empty_reflog_markers(&store_git_dir);
2954 markers.insert(storage_refname);
2955 write_empty_reflog_markers(&store_git_dir, &markers)
2956}
2957
2958pub fn reftable_delete_reflog(git_dir: &Path, refname: &str) -> Result<()> {
2960 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2961 let mut markers = read_empty_reflog_markers(&store_git_dir);
2962 markers.remove(&storage_refname);
2963 write_empty_reflog_markers(&store_git_dir, &markers)?;
2964 let mut stack = ReftableStack::open(&store_git_dir)?;
2965 stack.replace_logs_for_ref(&storage_refname, &[])
2966}
2967
2968pub fn read_write_options(git_dir: &Path) -> WriteOptions {
2974 let mut opts = WriteOptions::default();
2975 opts.hash_size = reftable_hash_size_for_git_dir(git_dir);
2976
2977 if let Ok(config) = ConfigSet::load(Some(git_dir), true) {
2978 if let Some(value) = config.get("reftable.blockSize") {
2979 if let Ok(v) = value.parse::<u32>() {
2980 opts.block_size = v;
2981 }
2982 }
2983 if let Some(value) = config.get("reftable.restartInterval") {
2984 if let Ok(v) = value.parse::<usize>() {
2985 opts.restart_interval = v;
2986 }
2987 }
2988 if let Some(value) = config.get("reftable.indexObjects") {
2989 let value = value.to_lowercase();
2990 if value == "false" || value == "0" || value == "no" || value == "off" {
2991 opts.skip_index_objects = true;
2992 }
2993 }
2994 if let Some(value) = config.get("core.logAllRefUpdates") {
2995 let value = value.to_lowercase();
2996 if !(value == "true" || value == "always") {
2997 opts.write_log = false;
2998 }
2999 }
3000 return opts;
3001 }
3002
3003 let config_path = git_dir.join("config");
3004 if let Ok(content) = fs::read_to_string(&config_path) {
3005 let mut in_reftable = false;
3006 let mut in_core = false;
3007 let mut log_all_ref_updates: Option<bool> = None;
3008
3009 for line in content.lines() {
3010 let trimmed = line.trim();
3011 if trimmed.starts_with('[') {
3012 let section_lower = trimmed.to_lowercase();
3013 in_reftable = section_lower.starts_with("[reftable]");
3014 in_core = section_lower.starts_with("[core]");
3015 continue;
3016 }
3017 if in_reftable {
3018 if let Some((key, value)) = trimmed.split_once('=') {
3019 let key = key.trim().to_lowercase();
3020 let value = value.trim();
3021 match key.as_str() {
3022 "blocksize" => {
3023 if let Ok(v) = value.parse::<u32>() {
3024 opts.block_size = v;
3025 }
3026 }
3027 "restartinterval" => {
3028 if let Ok(v) = value.parse::<usize>() {
3029 opts.restart_interval = v;
3030 }
3031 }
3032 _ => {}
3033 }
3034 }
3035 }
3036 if in_core {
3037 if let Some((key, value)) = trimmed.split_once('=') {
3038 let key = key.trim().to_lowercase();
3039 let value = value.trim().to_lowercase();
3040 if key == "logallrefupdates" {
3041 log_all_ref_updates = Some(value == "true" || value == "always");
3042 }
3043 }
3044 }
3045 }
3046
3047 if let Some(false) = log_all_ref_updates {
3048 opts.write_log = false;
3049 }
3050 }
3051
3052 opts
3053}
3054
3055fn should_log_ref_updates(git_dir: &Path) -> bool {
3057 let config_path = git_dir.join("config");
3058 if let Ok(content) = fs::read_to_string(&config_path) {
3059 let mut in_core = false;
3060 for line in content.lines() {
3061 let trimmed = line.trim();
3062 if trimmed.starts_with('[') {
3063 in_core = trimmed.to_lowercase().starts_with("[core]");
3064 continue;
3065 }
3066 if in_core {
3067 if let Some((key, value)) = trimmed.split_once('=') {
3068 if key.trim().eq_ignore_ascii_case("logallrefupdates") {
3069 let v = value.trim().to_lowercase();
3070 return v == "true" || v == "always";
3071 }
3072 }
3073 }
3074 }
3075 }
3076 false
3077}
3078
3079pub fn dump_reftable_blocks(path: &Path) -> Result<String> {
3089 let data = fs::read(path).map_err(Error::Io)?;
3090 if data.len() < HEADER_SIZE {
3091 return Err(Error::InvalidRef("reftable: file too small".into()));
3092 }
3093 if &data[0..4] != REFTABLE_MAGIC {
3094 return Err(Error::InvalidRef("reftable: bad magic".into()));
3095 }
3096 let version = data[4];
3097 let header_size = if version == 2 { 28 } else { 24 };
3098 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
3099 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
3100
3101 let table_size = data.len().saturating_sub(footer_size);
3102
3103 let mut out = String::new();
3104 out.push_str("header:\n");
3105 out.push_str(&format!(" block_size: {block_size}\n"));
3106
3107 let mut section_type: u8 = 0;
3108 let mut block_off: u64 = 0;
3110 let mut first = true;
3111
3112 loop {
3113 if !first {
3114 }
3117 if block_off as usize >= table_size {
3118 break;
3119 }
3120 let header_off = if block_off == 0 { header_size } else { 0 };
3121 let pos = block_off as usize + header_off;
3122 if pos + 1 > data.len() {
3123 break;
3124 }
3125 let block_type = data[pos];
3126 if !is_block_type(block_type) {
3127 break;
3128 }
3129
3130 if pos + 4 > data.len() {
3132 break;
3133 }
3134 let blk_len =
3135 ((data[pos + 1] as u32) << 16) | ((data[pos + 2] as u32) << 8) | (data[pos + 3] as u32);
3136 let blk_len = blk_len as usize;
3137
3138 let (restart_off, restart_count, full_block_size) = if block_type == BLOCK_TYPE_LOG {
3140 let skip = 4 + header_off;
3143 let comp = &data[block_off as usize + skip..];
3144 let mut dec = flate2::read::DeflateDecoder::new(comp);
3145 let mut inflated = vec![0u8; blk_len.saturating_sub(skip)];
3146 read_exact_inflate(&mut dec, &mut inflated)?;
3148 let consumed = dec.total_in() as usize;
3149 let mut full = vec![0u8; skip];
3151 full.extend_from_slice(&inflated);
3152 let rc = be16(&full, blk_len - 2) as usize;
3153 let roff = blk_len - 2 - 3 * rc;
3154 let fbs = skip + consumed;
3155 (roff, rc, fbs)
3156 } else {
3157 let abs = block_off as usize;
3158 if abs + blk_len < 2 {
3159 break;
3160 }
3161 let rc = be16(&data, abs + blk_len - 2) as usize;
3162 let roff = blk_len - 2 - 3 * rc;
3163 let mut fbs = block_size as usize;
3166 if fbs == 0 {
3167 fbs = blk_len;
3168 } else if blk_len < fbs
3169 && abs + blk_len < data.len()
3170 && data.get(abs + blk_len) == Some(&0u8)
3171 {
3172 } else if blk_len < fbs {
3174 fbs = blk_len;
3175 }
3176 (roff, rc, fbs)
3177 };
3178
3179 if block_type != section_type {
3180 let section = match block_type {
3181 BLOCK_TYPE_LOG => "log",
3182 BLOCK_TYPE_REF => "ref",
3183 BLOCK_TYPE_OBJ => "obj",
3184 BLOCK_TYPE_INDEX => "idx",
3185 _ => return Err(Error::InvalidRef("reftable: bad block type".into())),
3186 };
3187 section_type = block_type;
3188 out.push_str(&format!("{section}:\n"));
3189 }
3190
3191 out.push_str(&format!(" - length: {restart_off}\n"));
3192 out.push_str(&format!(" restarts: {restart_count}\n"));
3193
3194 block_off += full_block_size as u64;
3195 first = false;
3196 if full_block_size == 0 {
3197 break;
3198 }
3199 }
3200
3201 Ok(out)
3202}
3203
3204fn is_block_type(t: u8) -> bool {
3205 t == BLOCK_TYPE_REF || t == BLOCK_TYPE_LOG || t == BLOCK_TYPE_OBJ || t == BLOCK_TYPE_INDEX
3206}
3207
3208fn be16(data: &[u8], off: usize) -> u16 {
3209 ((data[off] as u16) << 8) | (data[off + 1] as u16)
3210}
3211
3212fn read_exact_inflate<R: Read>(r: &mut R, buf: &mut [u8]) -> Result<()> {
3213 let mut filled = 0;
3214 while filled < buf.len() {
3215 match r.read(&mut buf[filled..]) {
3216 Ok(0) => break,
3217 Ok(n) => filled += n,
3218 Err(e) => return Err(Error::Zlib(e.to_string())),
3219 }
3220 }
3221 Ok(())
3222}
3223
3224fn crc32(data: &[u8]) -> u32 {
3230 let mut crc: u32 = 0xffffffff;
3231 for &byte in data {
3232 crc ^= byte as u32;
3233 for _ in 0..8 {
3234 if crc & 1 != 0 {
3235 crc = (crc >> 1) ^ 0xedb88320;
3236 } else {
3237 crc >>= 1;
3238 }
3239 }
3240 }
3241 !crc
3242}
3243
3244fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
3246 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
3247}
3248
3249fn read_u24(data: &[u8], pos: usize) -> usize {
3251 ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
3252}
3253
3254fn read_u16(data: &[u8], pos: usize) -> usize {
3256 ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
3257}
3258
3259fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
3261 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
3262 if data.len() < footer_size {
3263 return Err(Error::InvalidRef("reftable: footer too small".into()));
3264 }
3265
3266 if &data[0..4] != REFTABLE_MAGIC {
3268 return Err(Error::InvalidRef("reftable: bad footer magic".into()));
3269 }
3270 let fver = data[4];
3271 if fver != version {
3272 return Err(Error::InvalidRef(format!(
3273 "reftable: footer version mismatch: header={version}, footer={fver}"
3274 )));
3275 }
3276
3277 let read_u64 = |slice: &[u8]| -> Result<u64> {
3280 let bytes: [u8; 8] = slice
3281 .try_into()
3282 .map_err(|_| Error::InvalidRef("reftable: truncated footer field".into()))?;
3283 Ok(u64::from_be_bytes(bytes))
3284 };
3285
3286 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
3287 let min_update_index = read_u64(&data[8..16])?;
3288 let max_update_index = read_u64(&data[16..24])?;
3289
3290 let off = if version == 2 { 28 } else { 24 };
3293 let ref_index_position = read_u64(&data[off..off + 8])?;
3294 let obj_position_and_id_len = read_u64(&data[off + 8..off + 16])?;
3295 let obj_index_position = read_u64(&data[off + 16..off + 24])?;
3296 let log_position = read_u64(&data[off + 24..off + 32])?;
3297 let log_index_position = read_u64(&data[off + 32..off + 40])?;
3298
3299 let crc_bytes: [u8; 4] = data[footer_size - 4..footer_size]
3301 .try_into()
3302 .map_err(|_| Error::InvalidRef("reftable: truncated footer CRC".into()))?;
3303 let crc_stored = u32::from_be_bytes(crc_bytes);
3304 let crc_computed = crc32(&data[..footer_size - 4]);
3305 if crc_stored != crc_computed {
3306 return Err(Error::InvalidRef(format!(
3307 "reftable: footer CRC mismatch: stored={crc_stored:08x}, computed={crc_computed:08x}"
3308 )));
3309 }
3310
3311 Ok(Footer {
3312 version: fver,
3313 block_size,
3314 min_update_index,
3315 max_update_index,
3316 ref_index_position,
3317 obj_position_and_id_len,
3318 obj_index_position,
3319 log_position,
3320 log_index_position,
3321 })
3322}
3323
3324fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
3326 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
3328 if parts.len() < 3 {
3329 return (identity.to_owned(), String::new(), 0, 0);
3330 }
3331 let tz_str = parts[0]; let time_str = parts[1]; let name_email = parts[2]; let time_secs = time_str.parse::<u64>().unwrap_or(0);
3336
3337 let tz_minutes = if tz_str.len() >= 5 {
3339 let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
3340 let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
3341 let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
3342 sign * (hours * 60 + mins)
3343 } else {
3344 0
3345 };
3346
3347 let (name, email) = if let Some(lt_pos) = name_email.find('<') {
3349 let name = name_email[..lt_pos].trim().to_owned();
3350 let email = if let Some(gt_pos) = name_email.find('>') {
3351 name_email[lt_pos + 1..gt_pos].to_owned()
3352 } else {
3353 name_email[lt_pos + 1..].to_owned()
3354 };
3355 (name, email)
3356 } else {
3357 (name_email.to_owned(), String::new())
3358 };
3359
3360 (name, email, time_secs, tz_minutes)
3361}
3362
3363#[cfg(test)]
3368mod tests {
3369 use super::*;
3370
3371 #[test]
3372 fn test_varint_roundtrip() {
3373 for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
3374 let mut buf = Vec::new();
3375 put_varint(val, &mut buf);
3376 let (decoded, end) = get_varint(&buf, 0).unwrap();
3377 assert_eq!(decoded, val, "varint roundtrip failed for {val}");
3378 assert_eq!(end, buf.len());
3379 }
3380 }
3381
3382 #[test]
3383 fn test_crc32() {
3384 assert_eq!(crc32(b"123456789"), 0xCBF43926);
3386 }
3387
3388 #[test]
3389 fn test_empty_table() {
3390 let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
3391 let data = writer.finish().unwrap();
3392 let reader = ReftableReader::new(data).unwrap();
3393 let refs = reader.read_refs().unwrap();
3394 assert!(refs.is_empty());
3395 }
3396
3397 #[test]
3398 fn test_write_read_single_ref() {
3399 let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
3400 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
3401 writer
3402 .add_ref(RefRecord {
3403 name: "refs/heads/main".to_owned(),
3404 update_index: 1,
3405 value: RefValue::Val1(oid),
3406 })
3407 .unwrap();
3408 let data = writer.finish().unwrap();
3409
3410 let reader = ReftableReader::new(data).unwrap();
3411 let refs = reader.read_refs().unwrap();
3412 assert_eq!(refs.len(), 1);
3413 assert_eq!(refs[0].name, "refs/heads/main");
3414 assert_eq!(refs[0].value, RefValue::Val1(oid));
3415 assert_eq!(refs[0].update_index, 1);
3416 }
3417
3418 #[test]
3419 fn test_write_read_multiple_refs() {
3420 let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
3421 let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
3422 let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
3423
3424 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
3425 writer
3426 .add_ref(RefRecord {
3427 name: "refs/heads/a".to_owned(),
3428 update_index: 1,
3429 value: RefValue::Val1(oid1),
3430 })
3431 .unwrap();
3432 writer
3433 .add_ref(RefRecord {
3434 name: "refs/heads/b".to_owned(),
3435 update_index: 1,
3436 value: RefValue::Val1(oid2),
3437 })
3438 .unwrap();
3439 writer
3440 .add_ref(RefRecord {
3441 name: "refs/tags/v1.0".to_owned(),
3442 update_index: 1,
3443 value: RefValue::Val2(oid3, oid1),
3444 })
3445 .unwrap();
3446 let data = writer.finish().unwrap();
3447
3448 let reader = ReftableReader::new(data).unwrap();
3449 let refs = reader.read_refs().unwrap();
3450 assert_eq!(refs.len(), 3);
3451 assert_eq!(refs[0].name, "refs/heads/a");
3452 assert_eq!(refs[1].name, "refs/heads/b");
3453 assert_eq!(refs[2].name, "refs/tags/v1.0");
3454 assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
3455 }
3456
3457 #[test]
3458 fn test_symref_roundtrip() {
3459 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
3460 writer
3461 .add_ref(RefRecord {
3462 name: "refs/heads/sym".to_owned(),
3463 update_index: 1,
3464 value: RefValue::Symref("refs/heads/main".to_owned()),
3465 })
3466 .unwrap();
3467 let data = writer.finish().unwrap();
3468
3469 let reader = ReftableReader::new(data).unwrap();
3470 let refs = reader.read_refs().unwrap();
3471 assert_eq!(refs.len(), 1);
3472 assert_eq!(
3473 refs[0].value,
3474 RefValue::Symref("refs/heads/main".to_owned())
3475 );
3476 }
3477
3478 #[test]
3479 fn test_log_roundtrip() {
3480 let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
3481 let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
3482
3483 let mut opts = WriteOptions::default();
3484 opts.write_log = true;
3485 let mut writer = ReftableWriter::new(opts, 1, 1);
3486 writer
3487 .add_log(LogRecord {
3488 refname: "refs/heads/main".to_owned(),
3489 update_index: 1,
3490 old_id: old_oid,
3491 new_id: new_oid,
3492 name: "Test User".to_owned(),
3493 email: "test@example.com".to_owned(),
3494 time_seconds: 1700000000,
3495 tz_offset: -480,
3496 message: "initial commit".to_owned(),
3497 })
3498 .unwrap();
3499 let data = writer.finish().unwrap();
3500
3501 let reader = ReftableReader::new(data).unwrap();
3502 let logs = reader.read_logs().unwrap();
3503 assert_eq!(logs.len(), 1);
3504 assert_eq!(logs[0].refname, "refs/heads/main");
3505 assert_eq!(logs[0].old_id, old_oid);
3506 assert_eq!(logs[0].new_id, new_oid);
3507 assert_eq!(logs[0].name, "Test User");
3508 assert_eq!(logs[0].email, "test@example.com");
3509 assert_eq!(logs[0].time_seconds, 1700000000);
3510 assert_eq!(logs[0].tz_offset, -480);
3511 assert_eq!(logs[0].message, "initial commit\n");
3515 }
3516
3517 #[test]
3518 fn test_unaligned_table() {
3519 let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
3520 let opts = WriteOptions {
3521 unpadded: true,
3526 restart_interval: 16,
3527 write_log: false,
3528 ..WriteOptions::default()
3529 };
3530 let mut writer = ReftableWriter::new(opts, 1, 1);
3531 writer
3532 .add_ref(RefRecord {
3533 name: "refs/heads/main".to_owned(),
3534 update_index: 1,
3535 value: RefValue::Val1(oid),
3536 })
3537 .unwrap();
3538 let data = writer.finish().unwrap();
3539
3540 assert!(data.len() < DEFAULT_BLOCK_SIZE as usize);
3542
3543 let reader = ReftableReader::new(data).unwrap();
3544 let refs = reader.read_refs().unwrap();
3545 assert_eq!(refs.len(), 1);
3546 assert_eq!(refs[0].value, RefValue::Val1(oid));
3547 }
3548
3549 #[test]
3550 fn test_parse_identity() {
3551 let (name, email, ts, tz) =
3552 parse_identity_string("Test User <test@example.com> 1700000000 -0800");
3553 assert_eq!(name, "Test User");
3554 assert_eq!(email, "test@example.com");
3555 assert_eq!(ts, 1700000000);
3556 assert_eq!(tz, -480);
3557 }
3558
3559 #[test]
3560 fn test_deletion_record() {
3561 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
3562 writer
3563 .add_ref(RefRecord {
3564 name: "refs/heads/gone".to_owned(),
3565 update_index: 1,
3566 value: RefValue::Deletion,
3567 })
3568 .unwrap();
3569 let data = writer.finish().unwrap();
3570
3571 let reader = ReftableReader::new(data).unwrap();
3572 let refs = reader.read_refs().unwrap();
3573 assert_eq!(refs.len(), 1);
3574 assert_eq!(refs[0].value, RefValue::Deletion);
3575 }
3576}